tambahannya
This commit is contained in:
2
.env.example
Normal file
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
NAMESPACE=
|
||||
OPENAPI_URL=
|
||||
141
.gitignore
vendored
Normal file
141
.gitignore
vendored
Normal file
@@ -0,0 +1,141 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
.output
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Sveltekit cache directory
|
||||
.svelte-kit/
|
||||
|
||||
# vitepress build output
|
||||
**/.vitepress/dist
|
||||
|
||||
# vitepress cache directory
|
||||
**/.vitepress/cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# Firebase cache directory
|
||||
.firebase/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v3
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
# Vite files
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
.vite/
|
||||
142
.npmignore
Normal file
142
.npmignore
Normal file
@@ -0,0 +1,142 @@
|
||||
# Rules from: /Users/bip/tmp/test-mcp/.gitignore
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
.output
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Sveltekit cache directory
|
||||
.svelte-kit/
|
||||
|
||||
# vitepress build output
|
||||
**/.vitepress/dist
|
||||
|
||||
# vitepress cache directory
|
||||
**/.vitepress/cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# Firebase cache directory
|
||||
.firebase/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v3
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
# Vite files
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
.vite/
|
||||
86
bin/build.ts
Executable file
86
bin/build.ts
Executable file
@@ -0,0 +1,86 @@
|
||||
|
||||
import { execSync } from "node:child_process";
|
||||
import { readdir, rm, mkdir, cp } from "node:fs/promises";
|
||||
import { join, relative, dirname } from "node:path";
|
||||
|
||||
const SRC = "src";
|
||||
const DIST = "dist";
|
||||
|
||||
const ASSET_EXT = [
|
||||
".svg", ".png", ".jpg", ".jpeg", ".webp", ".gif",
|
||||
".json", ".html", ".css", ".txt", ".ico", ".md"
|
||||
];
|
||||
|
||||
// Recursively scan directory tree
|
||||
async function walk(dir: string): Promise<string[]> {
|
||||
const entries = await readdir(dir, { withFileTypes: true });
|
||||
const files: string[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
const full = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...await walk(full));
|
||||
} else {
|
||||
files.push(full);
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
async function build() {
|
||||
console.log("🧹 Cleaning dist/...");
|
||||
|
||||
await rm(DIST, { recursive: true, force: true });
|
||||
await mkdir(DIST, { recursive: true });
|
||||
|
||||
console.log("🔍 Scanning src/...");
|
||||
|
||||
const allFiles = await walk(SRC);
|
||||
|
||||
const tsFiles = allFiles.filter(f =>
|
||||
f.endsWith(".ts") || f.endsWith(".tsx") || f.endsWith(".js")
|
||||
);
|
||||
|
||||
const assets = allFiles.filter(f =>
|
||||
ASSET_EXT.some(ext => f.toLowerCase().endsWith(ext))
|
||||
);
|
||||
|
||||
console.log("⚡ Building & Minifying TypeScript...");
|
||||
|
||||
for (const file of tsFiles) {
|
||||
const rel = relative(SRC, file);
|
||||
const outDir = join(DIST, dirname(rel));
|
||||
|
||||
await mkdir(outDir, { recursive: true });
|
||||
|
||||
await Bun.build({
|
||||
entrypoints: [file],
|
||||
outdir: outDir,
|
||||
splitting: false,
|
||||
minify: true, // ← minify otomatis
|
||||
sourcemap: "external",
|
||||
target: "browser"
|
||||
});
|
||||
|
||||
console.log(" ✔ Built:", rel);
|
||||
}
|
||||
|
||||
console.log("📁 Copying assets...");
|
||||
|
||||
for (const file of assets) {
|
||||
const rel = relative(SRC, file);
|
||||
const dest = join(DIST, rel);
|
||||
|
||||
await mkdir(dirname(dest), { recursive: true });
|
||||
await cp(file, dest);
|
||||
|
||||
console.log(" ✔ Copied:", rel);
|
||||
}
|
||||
|
||||
console.log("🎉 Build complete!");
|
||||
}
|
||||
|
||||
execSync("cd src && npm version patch", { stdio: 'inherit' })
|
||||
build()
|
||||
|
||||
404
bin/generate.ts
Executable file
404
bin/generate.ts
Executable file
@@ -0,0 +1,404 @@
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import _ from 'lodash';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const NAMESPACE = process.env.NAMESPACE
|
||||
const OPENAPI_URL = process.env.OPENAPI_URL
|
||||
|
||||
if (!NAMESPACE || !OPENAPI_URL) {
|
||||
throw new Error('NAMESPACE and OPENAPI_URL are required')
|
||||
}
|
||||
|
||||
const namespaceCase = _.startCase(_.camelCase(NAMESPACE)).replace(/ /g, '');
|
||||
const OUT_DIR = path.join('src', 'nodes');
|
||||
const OUT_FILE = path.join(OUT_DIR, `${namespaceCase}.node.ts`);
|
||||
const CREDENTIAL_NAME = _.camelCase(NAMESPACE);
|
||||
|
||||
interface OpenAPI {
|
||||
paths: Record<string, any>;
|
||||
components?: any;
|
||||
tags?: { name: string }[];
|
||||
}
|
||||
|
||||
// helpers
|
||||
const safe = (s: string) => s.replace(/[^a-zA-Z0-9]/g, '_');
|
||||
|
||||
// load OpenAPI
|
||||
async function loadOpenAPI(): Promise<OpenAPI> {
|
||||
const url = OPENAPI_URL!
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error(`Failed to fetch OpenAPI: ${res.status} ${res.statusText}`);
|
||||
return res.json() as Promise<OpenAPI>;
|
||||
}
|
||||
|
||||
// convert operation to value
|
||||
function operationValue(tag: string, operationId: string) {
|
||||
return `${safe(tag)}_${safe(operationId)}`;
|
||||
}
|
||||
|
||||
// build properties for dropdown + dynamic inputs
|
||||
function buildPropertiesBlock(ops: Array<any>) {
|
||||
const options = ops.map((op) => {
|
||||
const value = operationValue(op.tag, op.operationId);
|
||||
const label = `${op.tag} ${_.kebabCase(op.operationId).replace(/-/g, ' ')}`;
|
||||
return `{ name: '${label}', value: '${value}', description: ${JSON.stringify(
|
||||
op.summary || op.description || '',
|
||||
)}, action: '${label}' }`;
|
||||
});
|
||||
|
||||
const dropdown = `
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
options: [
|
||||
${options.join(',\n ')}
|
||||
],
|
||||
default: '${operationValue(ops[0].tag, ops[0].operationId)}',
|
||||
description: 'Pilih endpoint yang akan dipanggil'
|
||||
}
|
||||
`;
|
||||
|
||||
const dynamicProps: string[] = [];
|
||||
|
||||
for (const op of ops) {
|
||||
const value = operationValue(op.tag, op.operationId);
|
||||
|
||||
// Query fields
|
||||
for (const name of op.query ?? []) {
|
||||
dynamicProps.push(`
|
||||
{
|
||||
displayName: 'Query ${name}',
|
||||
name: 'query_${name}',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: '${name}',
|
||||
description: '${name}',
|
||||
displayOptions: { show: { operation: ['${value}'] } }
|
||||
}`);
|
||||
}
|
||||
|
||||
// Body fields (required only)
|
||||
const bodyRequired = op.body?.required ?? [];
|
||||
const bodySchema = op.body?.schema ?? {};
|
||||
|
||||
for (const name of bodyRequired) {
|
||||
const schema = bodySchema[name] ?? {};
|
||||
let type = 'string';
|
||||
if (schema.type === 'number' || schema.type === 'integer') type = 'number';
|
||||
if (schema.type === 'boolean') type = 'boolean';
|
||||
|
||||
const defVal =
|
||||
type === 'string' ? "''" : type === 'number' ? '0' : type === 'boolean' ? 'false' : "''";
|
||||
|
||||
dynamicProps.push(`
|
||||
{
|
||||
displayName: 'Body ${name}',
|
||||
name: 'body_${name}',
|
||||
type: '${type}',
|
||||
default: ${defVal},
|
||||
placeholder: '${name}',
|
||||
description: '${schema?.description ?? name}',
|
||||
displayOptions: { show: { operation: ['${value}'] } }
|
||||
}`);
|
||||
}
|
||||
}
|
||||
|
||||
return `[
|
||||
${dropdown},
|
||||
${dynamicProps.join(',\n ')}
|
||||
]`;
|
||||
}
|
||||
|
||||
// build execute switch
|
||||
function buildExecuteSwitch(ops: Array<any>) {
|
||||
const cases: string[] = [];
|
||||
|
||||
for (const op of ops) {
|
||||
const val = operationValue(op.tag, op.operationId);
|
||||
const method = (op.method || 'get').toLowerCase();
|
||||
const url = op.path;
|
||||
const q = op.query ?? [];
|
||||
const bodyReq = op.body?.required ?? [];
|
||||
|
||||
const qLines =
|
||||
q
|
||||
.map(
|
||||
(name: string) =>
|
||||
`const query_${_.snakeCase(name)} = this.getNodeParameter('query_${_.snakeCase(name)}', i, '') as string;`,
|
||||
)
|
||||
.join('\n ') || '';
|
||||
|
||||
const bodyLines =
|
||||
bodyReq
|
||||
.map(
|
||||
(name: string) =>
|
||||
`const body_${_.snakeCase(name)} = this.getNodeParameter('body_${_.snakeCase(name)}', i, '') as any;`,
|
||||
)
|
||||
.join('\n ') || '';
|
||||
|
||||
const bodyObject =
|
||||
bodyReq.length > 0
|
||||
? `const body = { ${bodyReq.map((n: string) => `${_.snakeCase(n)}: body_${_.snakeCase(n)}`).join(', ')} };`
|
||||
: 'const body = undefined;';
|
||||
|
||||
const paramsObj =
|
||||
q.length > 0 ? `params: { ${q.map((n: string) => `${_.snakeCase(n)}: query_${_.snakeCase(n)}`).join(', ')} },` : '';
|
||||
|
||||
const dataLine = method === 'get' ? '' : 'data: body,';
|
||||
|
||||
cases.push(`
|
||||
case '${val}': {
|
||||
${qLines}
|
||||
${bodyLines}
|
||||
${bodyObject}
|
||||
url = baseUrl + '${url}';
|
||||
method = '${method}';
|
||||
axiosConfig = {
|
||||
headers: finalHeaders,
|
||||
${paramsObj}
|
||||
${dataLine}
|
||||
};
|
||||
break;
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
return `
|
||||
switch (operation) {
|
||||
${cases.join('\n')}
|
||||
default:
|
||||
throw new Error('Unknown operation: ' + operation);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function credentialsText() {
|
||||
const text = `
|
||||
import { ICredentialType, INodeProperties } from "n8n-workflow";
|
||||
|
||||
export class ${namespaceCase}Credentials implements ICredentialType {
|
||||
name = "${CREDENTIAL_NAME}";
|
||||
displayName = "${CREDENTIAL_NAME} (Bearer Token)";
|
||||
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: "Base URL",
|
||||
name: "baseUrl",
|
||||
type: "string",
|
||||
default: "",
|
||||
placeholder: "https://api.example.com",
|
||||
description: "Masukkan URL dasar API tanpa garis miring di akhir",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: "Bearer Token",
|
||||
name: "token",
|
||||
type: "string",
|
||||
default: "",
|
||||
typeOptions: { password: true },
|
||||
description: "Masukkan token autentikasi Bearer (tanpa 'Bearer ' di depannya)",
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
`
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
// top-level
|
||||
function generateNodeFile(ops: Array<any>) {
|
||||
const propertiesBlock = buildPropertiesBlock(ops);
|
||||
const executeSwitch = buildExecuteSwitch(ops);
|
||||
|
||||
return `import type { INodeType, INodeTypeDescription, IExecuteFunctions } from 'n8n-workflow';
|
||||
import axios from 'axios';
|
||||
|
||||
export class ${namespaceCase} implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: '${NAMESPACE}',
|
||||
name: '${namespaceCase}',
|
||||
icon: 'file:icon.svg',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
subtitle: '={{$parameter["operation"]}}',
|
||||
description: 'Universal node generated from OpenAPI - satu node memuat semua endpoint',
|
||||
defaults: { name: '${namespaceCase}' },
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{ name: '${CREDENTIAL_NAME}', required: true }
|
||||
],
|
||||
properties: ${propertiesBlock}
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions) {
|
||||
const items = this.getInputData();
|
||||
const returnData: any[] = [];
|
||||
const creds = await this.getCredentials('${CREDENTIAL_NAME}') as any;
|
||||
|
||||
const baseUrlRaw = creds?.baseUrl ?? '';
|
||||
const apiKeyRaw = creds?.token ?? '';
|
||||
const baseUrl = String(baseUrlRaw || '').replace(/\\/$/, '');
|
||||
const apiKey = String(apiKeyRaw || '').trim().replace(/^Bearer\\s+/i, '');
|
||||
|
||||
if (!baseUrl) throw new Error('Base URL tidak ditemukan');
|
||||
if (!apiKey) throw new Error('Token tidak ditemukan');
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const operation = this.getNodeParameter('operation', i) as string;
|
||||
|
||||
let url = '';
|
||||
let method: any = 'get';
|
||||
let axiosConfig: any = {};
|
||||
const finalHeaders: any = { Authorization: \`Bearer \${apiKey}\` };
|
||||
|
||||
${executeSwitch}
|
||||
|
||||
try {
|
||||
const response = await axios({ method, url, ...axiosConfig });
|
||||
returnData.push(response.data);
|
||||
} catch (err: any) {
|
||||
returnData.push({
|
||||
error: true,
|
||||
message: err.message,
|
||||
status: err.response?.status,
|
||||
data: err.response?.data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return [this.helpers.returnJsonArray(returnData)];
|
||||
}
|
||||
}
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
function iconText(text: string) {
|
||||
return `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128" aria-label="Icon AB square">
|
||||
<defs>
|
||||
<style>
|
||||
.letters {
|
||||
fill: #3b82f6; /* biru */
|
||||
font-family: "Inter", "Segoe UI", Roboto, sans-serif;
|
||||
font-weight: 800;
|
||||
font-size: 56px;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<text class="letters" x="64" y="78" text-anchor="middle" dominant-baseline="middle">${text}</text>
|
||||
</svg>
|
||||
|
||||
`
|
||||
}
|
||||
|
||||
function packageText({ name, className }: { name: string, className: string }) {
|
||||
return `
|
||||
{
|
||||
"name": "n8n-nodes-${name}",
|
||||
"version": "1.0.43",
|
||||
"keywords": [
|
||||
"n8n",
|
||||
"n8n-nodes"
|
||||
],
|
||||
"author": {
|
||||
"name": "makuro",
|
||||
"phone": "6289697338821"
|
||||
},
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"n8n": {
|
||||
"nodes": [
|
||||
"nodes/${className}.node.js"
|
||||
],
|
||||
"n8nNodesApiVersion": 1,
|
||||
"credentials": [
|
||||
"credentials/${className}Credentials.credentials.js"
|
||||
]
|
||||
}
|
||||
}`
|
||||
|
||||
}
|
||||
|
||||
async function run() {
|
||||
|
||||
console.log('💡 Loading OpenAPI...');
|
||||
const api = await loadOpenAPI();
|
||||
|
||||
const ops: Array<any> = [];
|
||||
|
||||
for (const pathStr of Object.keys(api.paths || {})) {
|
||||
const pathObj = api.paths[pathStr];
|
||||
|
||||
for (const method of Object.keys(pathObj)) {
|
||||
const operation = pathObj[method];
|
||||
const tags = operation.tags?.length ? operation.tags : ['default'];
|
||||
|
||||
console.log("✅", _.upperCase(method).padEnd(7), pathStr);
|
||||
|
||||
const operationId = operation.operationId || `${method}_${safe(pathStr)}`;
|
||||
const query = (operation.parameters ?? [])
|
||||
.filter((p: any) => p.in === 'query')
|
||||
.map((p: any) => p.name);
|
||||
|
||||
const requestBody =
|
||||
operation.requestBody?.content?.['application/json']?.schema ??
|
||||
operation.requestBody?.content?.['multipart/form-data']?.schema ??
|
||||
null;
|
||||
|
||||
const bodyRequired = requestBody?.required ?? [];
|
||||
const bodyProps = requestBody?.properties ?? {};
|
||||
|
||||
for (const tag of tags) {
|
||||
ops.push({
|
||||
tag,
|
||||
path: pathStr,
|
||||
method,
|
||||
operationId,
|
||||
summary: operation.summary || '',
|
||||
description: operation.description || '',
|
||||
query,
|
||||
body: {
|
||||
required: bodyRequired,
|
||||
schema: bodyProps,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ops.length === 0) throw new Error('No operations found');
|
||||
|
||||
const raw = generateNodeFile(ops);
|
||||
|
||||
console.log('[GEN] Generated single node file:', OUT_FILE);
|
||||
await fs.writeFile(OUT_FILE, raw, 'utf-8');
|
||||
|
||||
const credentialsRaw = credentialsText();
|
||||
await fs.writeFile(`src/credentials/${namespaceCase}Credentials.credentials.ts`, credentialsRaw, 'utf-8')
|
||||
|
||||
const iconRaw = iconText(_.upperCase(namespaceCase.substring(0, 3)));
|
||||
await fs.writeFile(`src/nodes/icon.svg`, iconRaw, 'utf-8')
|
||||
|
||||
const packageRaw = packageText({ name: NAMESPACE!, className: namespaceCase });
|
||||
await fs.writeFile(`src/package.json`, packageRaw, 'utf-8')
|
||||
|
||||
execSync('npx prettier --write src')
|
||||
|
||||
}
|
||||
|
||||
run()
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('✅ Generated node file:', OUT_FILE);
|
||||
process.exit(0);
|
||||
})
|
||||
56
bin/init.ts
Executable file
56
bin/init.ts
Executable file
@@ -0,0 +1,56 @@
|
||||
import fs from 'fs/promises';
|
||||
import { execSync } from 'child_process'
|
||||
|
||||
async function init() {
|
||||
console.log("[INIT] Initializing...")
|
||||
|
||||
try {
|
||||
await fs.access(".env")
|
||||
} catch (error) {
|
||||
let envText = ""
|
||||
envText += "NAMESPACE=\n"
|
||||
envText += "OPENAPI_URL=\n"
|
||||
// generate .env
|
||||
await fs.writeFile(".env", envText)
|
||||
console.log('[GEN] Generated .env');
|
||||
}
|
||||
|
||||
const NAMESPACE = process.env.NAMESPACE
|
||||
const OPENAPI_URL = process.env.OPENAPI_URL
|
||||
|
||||
if (!NAMESPACE || !OPENAPI_URL) {
|
||||
throw new Error('NAMESPACE and OPENAPI_URL are required')
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.access(".git")
|
||||
} catch (error) {
|
||||
execSync("git init")
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("[INIT] Gitignored...")
|
||||
await fs.access(".gitignore")
|
||||
} catch (e) {
|
||||
|
||||
execSync("npx -y gitignore node")
|
||||
console.log('[INIT] gitignored');
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("[INIT] Npmignored...")
|
||||
await fs.access(".npmignore")
|
||||
} catch (e) {
|
||||
|
||||
execSync("npx -y npmignore node")
|
||||
console.log('[INIT] npmignored');
|
||||
}
|
||||
|
||||
console.log('[INIT] Installing dependencies...');
|
||||
execSync("bun install", { stdio: 'inherit' })
|
||||
|
||||
console.log('[INIT] Done');
|
||||
|
||||
}
|
||||
|
||||
init()
|
||||
3
bin/publish.ts
Normal file
3
bin/publish.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { execSync } from "child_process";
|
||||
|
||||
execSync("cd dist && npm publish", { stdio: 'inherit' })
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "n8n-generator",
|
||||
"name": "n8n-starter",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"version": "1.0.1",
|
||||
|
||||
86
src/README.md
Normal file
86
src/README.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# n8n-nodes-wajs
|
||||
|
||||
Node n8n kustom untuk berinteraksi dengan API wajs. Ini adalah node universal yang memungkinkan Anda mengakses berbagai endpoint API wajs untuk mengelola apikey, WhatsApp, webhook, chatflow, dan lainnya.
|
||||
|
||||
## Instalasi
|
||||
|
||||
Untuk menginstal node ini di n8n Anda:
|
||||
|
||||
1. Buka instalasi n8n Anda.
|
||||
2. Pergi ke **Settings > Community Nodes**.
|
||||
3. Klik **Install a community node**.
|
||||
4. Masukkan `n8n-nodes-wajs` dan klik **Install**.
|
||||
|
||||
n8n akan menginstal node, dan setelah itu Anda dapat menggunakannya dalam workflow Anda.
|
||||
|
||||
## Konfigurasi
|
||||
|
||||
Sebelum menggunakan node, Anda perlu mengkonfigurasi kredensial.
|
||||
|
||||
1. Di n8n, pergi ke **Credentials** dan klik **Add credential**.
|
||||
2. Cari **wajs (Bearer Token)** dan pilih.
|
||||
3. Isi detail berikut:
|
||||
- **Base URL**: URL dasar API wajs Anda (misalnya, `https://api.example.com`).
|
||||
- **Bearer Token**: Token API Anda.
|
||||
|
||||
## Operasi yang Tersedia
|
||||
|
||||
Node ini menyediakan berbagai operasi yang dapat dipilih. Berikut adalah daftar operasi yang dikelompokkan berdasarkan kategori:
|
||||
|
||||
### API Key
|
||||
|
||||
- `apikey_postApiApikeyCreate`: Membuat API key baru.
|
||||
- `apikey_getApiApikeyList`: Mendapatkan daftar API key.
|
||||
- `apikey_deleteApiApikeyDelete`: Menghapus API key.
|
||||
|
||||
### WhatsApp
|
||||
|
||||
- `WhatsApp_postApiWaStart`: Memulai sesi WhatsApp.
|
||||
- `WhatsApp_getApiWaQr`: Mendapatkan kode QR untuk login.
|
||||
- `WhatsApp_getApiWaReady`: Memeriksa apakah sesi siap.
|
||||
- `WhatsApp_postApiWaRestart`: Memulai ulang sesi.
|
||||
- `WhatsApp_postApiWaForce_start`: Memaksa memulai sesi.
|
||||
- `WhatsApp_postApiWaStop`: Menghentikan sesi.
|
||||
- `WhatsApp_getApiWaState`: Mendapatkan status sesi.
|
||||
- `WhatsApp_postApiWaSend_text`: Mengirim pesan teks.
|
||||
- `WhatsApp_postApiWaSend_media`: Mengirim pesan media.
|
||||
|
||||
### Webhook
|
||||
|
||||
- `Webhook_postApiWebhookCreate`: Membuat webhook.
|
||||
- `Webhook_getApiWebhookList`: Mendapatkan daftar webhook.
|
||||
- `Webhook_getApiWebhookFindById`: Mencari webhook berdasarkan ID.
|
||||
- `Webhook_deleteApiWebhookRemoveById`: Menghapus webhook berdasarkan ID.
|
||||
- `Webhook_putApiWebhookUpdateById`: Memperbarui webhook berdasarkan ID.
|
||||
|
||||
### Chatflows
|
||||
|
||||
- `chatflows_getApiChatflowsSync`: Sinkronisasi chatflow.
|
||||
- `chatflows_getApiChatflowsFind`: Mencari chatflow.
|
||||
- `chatflows_getApiChatflowsDefault`: Mendapatkan chatflow default.
|
||||
- `chatflows_putApiChatflowsDefault`: Mengatur chatflow default.
|
||||
- `chatflows_postApiChatflowsQuery`: Mengirim query ke chatflow.
|
||||
- `chatflows_putApiChatflowsFlow_active`: Mengaktifkan/menonaktifkan flow.
|
||||
- `chatflows_getApiChatflowsUrl_token`: Mendapatkan URL dan token flow.
|
||||
- `chatflows_putApiChatflowsUrl_token`: Memperbarui URL dan token flow.
|
||||
|
||||
### Autentikasi
|
||||
|
||||
- `auth_postAuthLogin`: Login ke akun.
|
||||
- `auth_deleteAuthLogout`: Logout dari akun.
|
||||
|
||||
### WhatsApp Hook
|
||||
|
||||
- `WhatsApp_Hook_getWa_hookHook`: Verifikasi webhook.
|
||||
- `WhatsApp_Hook_postWa_hookHook`: Menerima pesan WhatsApp.
|
||||
- `WhatsApp_Hook_getWa_hookList`: Mendapatkan daftar hook.
|
||||
- `WhatsApp_Hook_postWa_hookReset`: Mereset hook.
|
||||
|
||||
### Logs
|
||||
|
||||
- `logs_getLogsShow`: Menampilkan log.
|
||||
- `logs_postLogsClear`: Membersihkan log.
|
||||
|
||||
## Lisensi
|
||||
|
||||
[ISC](LICENSE)
|
||||
28
src/credentials/WajsCredentials.credentials.ts
Normal file
28
src/credentials/WajsCredentials.credentials.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { ICredentialType, INodeProperties } from "n8n-workflow";
|
||||
|
||||
export class WajsCredentials implements ICredentialType {
|
||||
name = "wajs";
|
||||
displayName = "wajs (Bearer Token)";
|
||||
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: "Base URL",
|
||||
name: "baseUrl",
|
||||
type: "string",
|
||||
default: "",
|
||||
placeholder: "https://api.example.com",
|
||||
description: "Masukkan URL dasar API tanpa garis miring di akhir",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: "Bearer Token",
|
||||
name: "token",
|
||||
type: "string",
|
||||
default: "",
|
||||
typeOptions: { password: true },
|
||||
description:
|
||||
"Masukkan token autentikasi Bearer (tanpa 'Bearer ' di depannya)",
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
1429
src/nodes/Wajs.node.ts
Normal file
1429
src/nodes/Wajs.node.ts
Normal file
File diff suppressed because it is too large
Load Diff
17
src/nodes/icon.svg
Normal file
17
src/nodes/icon.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128" aria-label="Icon AB square">
|
||||
<defs>
|
||||
<style>
|
||||
.letters {
|
||||
fill: #3b82f6; /* biru */
|
||||
font-family: "Inter", "Segoe UI", Roboto, sans-serif;
|
||||
font-weight: 800;
|
||||
font-size: 56px;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<text class="letters" x="64" y="78" text-anchor="middle" dominant-baseline="middle">WAJ</text>
|
||||
</svg>
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 434 B |
23
src/package.json
Normal file
23
src/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "n8n-nodes-wajs",
|
||||
"version": "1.1.10",
|
||||
"keywords": [
|
||||
"n8n",
|
||||
"n8n-nodes"
|
||||
],
|
||||
"author": {
|
||||
"name": "makuro",
|
||||
"phone": "6289697338821"
|
||||
},
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"n8n": {
|
||||
"nodes": [
|
||||
"nodes/Wajs.node.js"
|
||||
],
|
||||
"n8nNodesApiVersion": 1,
|
||||
"credentials": [
|
||||
"credentials/WajsCredentials.credentials.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["es2021", "dom"],
|
||||
"module": "commonjs",
|
||||
"target": "es2017",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/nodes/**/*.svg"]
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user