update version

This commit is contained in:
bipproduction
2025-11-20 16:19:59 +08:00
parent b39bfd04db
commit 9c7319f130
2 changed files with 79 additions and 20 deletions

View File

@@ -5,6 +5,7 @@ import type {
ILoadOptionsFunctions, ILoadOptionsFunctions,
INodePropertyOptions, INodePropertyOptions,
} from "n8n-workflow"; } from "n8n-workflow";
import axios from "axios"; import axios from "axios";
interface OpenAPISpec { interface OpenAPISpec {
@@ -23,20 +24,6 @@ async function loadOpenAPISpecCached(url: string): Promise<OpenAPISpec> {
return spec; return spec;
} }
function getDefaultValue(type: string | undefined, schema?: any): any {
if (schema?.example !== undefined) return schema.example;
if (schema?.default !== undefined) return schema.default;
switch (type) {
case "string": return "";
case "number":
case "integer": return 0;
case "boolean": return false;
case "array": return [];
case "object": return {};
default: return null;
}
}
function resolveRef(obj: any, spec: OpenAPISpec): any { function resolveRef(obj: any, spec: OpenAPISpec): any {
if (!obj || typeof obj !== "object") return obj; if (!obj || typeof obj !== "object") return obj;
if (obj.$ref && typeof obj.$ref === "string" && obj.$ref.startsWith("#/")) { if (obj.$ref && typeof obj.$ref === "string" && obj.$ref.startsWith("#/")) {
@@ -71,17 +58,31 @@ export class OpenApiNode implements INodeType {
default: "", default: "",
required: true, required: true,
}, },
{
displayName: "Tag Filter",
name: "tagFilter",
type: "options",
default: "",
description: "Filter operations based on OpenAPI tags",
typeOptions: {
loadOptionsMethod: "loadTags",
loadOptionsDependsOn: ["openapiUrl"],
},
},
{ {
displayName: "Operation", displayName: "Operation",
name: "operation", name: "operation",
type: "options", type: "options",
typeOptions: { typeOptions: {
loadOptionsMethod: "loadOperations", loadOptionsMethod: "loadOperations",
loadOptionsDependsOn: ["openapiUrl"], loadOptionsDependsOn: ["openapiUrl", "tagFilter"],
}, },
default: "", default: "",
required: true, required: true,
}, },
{ {
displayName: "Parameters (Form)", displayName: "Parameters (Form)",
name: "parametersForm", name: "parametersForm",
@@ -131,33 +132,87 @@ export class OpenApiNode implements INodeType {
methods = { methods = {
loadOptions: { loadOptions: {
// ============================
// LOAD TAGS
// ============================
async loadTags(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const url = (this.getNodeParameter("openapiUrl", "") as string).trim();
if (!url) return [{ name: "Enter URL first", value: "" }];
try {
const spec = await loadOpenAPISpecCached(url);
const tags = new Set<string>();
for (const path of Object.keys(spec.paths || {})) {
const pathObj = spec.paths[path];
for (const method of Object.keys(pathObj || {})) {
const op = pathObj[method];
if (Array.isArray(op.tags)) {
for (const t of op.tags) tags.add(t);
}
}
}
return [...tags].map((t) => ({
name: t,
value: t,
description: `Filter only operations with tag: ${t}`,
}));
} catch (err: any) {
return [{ name: `Error: ${err.message}`, value: "" }];
}
},
// ============================
// LOAD OPERATIONS
// ============================
async loadOperations(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> { async loadOperations(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const url = (this.getNodeParameter("openapiUrl", "") as string).trim(); const url = (this.getNodeParameter("openapiUrl", "") as string).trim();
const tagFilter = (this.getNodeParameter("tagFilter", "") as string).trim();
if (!url) return [{ name: "Provide openapiUrl first", value: "" }]; if (!url) return [{ name: "Provide openapiUrl first", value: "" }];
try { try {
const spec = await loadOpenAPISpecCached(url); const spec = await loadOpenAPISpecCached(url);
const ops: INodePropertyOptions[] = []; const ops: INodePropertyOptions[] = [];
for (const path of Object.keys(spec.paths || {})) { for (const path of Object.keys(spec.paths || {})) {
const pathObj = spec.paths[path]; const pathObj = spec.paths[path];
for (const method of Object.keys(pathObj || {})) { for (const method of Object.keys(pathObj || {})) {
const op = pathObj[method]; const op = pathObj[method];
if (tagFilter && (!op.tags || !op.tags.includes(tagFilter))) {
continue; // apply tag filter
}
const opId = const opId =
op.operationId || op.operationId ||
`${method}_${path.replace(/[{}]/g, "").replace(/\//g, "_")}`; `${method}_${path.replace(/[{}]/g, "").replace(/\//g, "_")}`;
const title = (op.summary || opId).toString();
const methodLabel = method.toUpperCase();
const summary = op.summary || opId;
const description = op.description || "No description provided.";
ops.push({ ops.push({
name: `${title} ${method.toUpperCase()} ${path}`, name: `[${methodLabel}] ${path} ${summary}`,
value: opId, value: opId,
description: `${summary}\n\n${description}\n\nTags: ${
op.tags ? op.tags.join(", ") : "None"
}\nOperation ID: ${opId}\nMethod: ${methodLabel}\nPath: ${path}`,
}); });
} }
} }
return ops;
return ops.length ? ops : [{ name: "No operations found", value: "" }];
} catch (err: any) { } catch (err: any) {
return [{ name: `Error: ${err.message}`, value: "" }]; return [{ name: `Error: ${err.message}`, value: "" }];
} }
}, },
// ============================
// LOAD PARAMETER NAMES
// ============================
async loadParameterNames(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> { async loadParameterNames(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const url = (this.getNodeParameter("openapiUrl", "") as string).trim(); const url = (this.getNodeParameter("openapiUrl", "") as string).trim();
const opId = (this.getNodeParameter("operation", "") as string).trim(); const opId = (this.getNodeParameter("operation", "") as string).trim();
@@ -193,6 +248,7 @@ export class OpenApiNode implements INodeType {
} }
} }
// Body fields
const bodySchema = const bodySchema =
op.requestBody?.content?.["application/json"]?.schema; op.requestBody?.content?.["application/json"]?.schema;
if (bodySchema) { if (bodySchema) {
@@ -221,6 +277,9 @@ export class OpenApiNode implements INodeType {
}, },
}; };
// ============================
// EXECUTE METHOD
// ============================
async execute(this: IExecuteFunctions) { async execute(this: IExecuteFunctions) {
const items = this.getInputData(); const items = this.getInputData();
const returnData: any[] = []; const returnData: any[] = [];
@@ -306,7 +365,7 @@ export class OpenApiNode implements INodeType {
timeout: 30000, timeout: 30000,
}; };
// === FIX: GET & HEAD tidak boleh punya body === // GET/HEAD must not have body
if (!(foundMethod === "get" || foundMethod === "head")) { if (!(foundMethod === "get" || foundMethod === "head")) {
axiosConfig.data = Object.keys(bodyParams).length ? bodyParams : undefined; axiosConfig.data = Object.keys(bodyParams).length ? bodyParams : undefined;
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "n8n-nodes-openapi-node", "name": "n8n-nodes-openapi-node",
"version": "1.0.6", "version": "1.0.7",
"keywords": [ "keywords": [
"n8n", "n8n",
"n8n-nodes" "n8n-nodes"