- Introduced a new proxy endpoint for API calls during workflow execution to handle CORS issues and streamline API interactions. - Updated the authorization logic to support Basic Auth with both token and username/password options, improving flexibility in API authentication. - Enhanced the API request building process to accommodate new node data structures, including dynamic handling of headers, parameters, and body content. - Improved error handling and response management in the workflow execution process, ensuring better feedback and control over API call outcomes. - Refactored the workflow page to utilize the new API call structure, enhancing overall workflow execution reliability and user experience.
198 lines
6.7 KiB
JavaScript
198 lines
6.7 KiB
JavaScript
/**
|
|
* Workflow API Call Proxy Endpoint
|
|
*
|
|
* This endpoint acts as a proxy for API calls made during workflow execution.
|
|
* It handles the new API node structure with proper authorization and avoids CORS issues.
|
|
*/
|
|
|
|
// Helper function to substitute variables in a string
|
|
function substituteVariables(str, variables) {
|
|
if (typeof str !== 'string') return str;
|
|
|
|
// Replace {{variable}} first
|
|
str = str.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (match, varName) => {
|
|
const value = variables[varName];
|
|
if (value === undefined || value === null) return '';
|
|
if (typeof value === 'object') {
|
|
return JSON.stringify(value);
|
|
}
|
|
return String(value);
|
|
});
|
|
|
|
// Then replace {variable}
|
|
str = str.replace(/\{([a-zA-Z0-9_]+)\}/g, (match, varName) => {
|
|
const value = variables[varName];
|
|
if (value === undefined || value === null) return '';
|
|
if (typeof value === 'object') {
|
|
return JSON.stringify(value);
|
|
}
|
|
return String(value);
|
|
});
|
|
|
|
return str;
|
|
}
|
|
|
|
// Build API request from node data
|
|
function buildApiRequest(nodeData, variables) {
|
|
// 1. URL (with param substitution)
|
|
let url = substituteVariables(nodeData.apiUrl, variables);
|
|
|
|
// 2. Params (for GET, DELETE, etc.)
|
|
let params = Array.isArray(nodeData.params) ? nodeData.params : [];
|
|
if (params.length) {
|
|
const query = params
|
|
.filter(p => p.key)
|
|
.map(p => `${encodeURIComponent(substituteVariables(p.key, variables))}=${encodeURIComponent(substituteVariables(p.value, variables))}`)
|
|
.join('&');
|
|
if (query) {
|
|
url += (url.includes('?') ? '&' : '?') + query;
|
|
}
|
|
}
|
|
|
|
// 3. Headers
|
|
let headers = {};
|
|
if (Array.isArray(nodeData.headers)) {
|
|
nodeData.headers.forEach(h => {
|
|
if (h.key) headers[substituteVariables(h.key, variables)] = substituteVariables(h.value, variables);
|
|
});
|
|
} else if (typeof nodeData.headers === 'object') {
|
|
headers = { ...nodeData.headers };
|
|
}
|
|
|
|
// 4. Authorization
|
|
if (nodeData.authorization && nodeData.authorization.type && nodeData.authorization.type !== 'none') {
|
|
const auth = nodeData.authorization;
|
|
if (auth.type === 'bearer' && auth.token) {
|
|
headers['Authorization'] = `Bearer ${substituteVariables(auth.token, variables)}`;
|
|
} else if (auth.type === 'basic') {
|
|
if (auth.token) {
|
|
// Basic Auth with token (JWT or other token)
|
|
headers['Authorization'] = `Basic ${substituteVariables(auth.token, variables)}`;
|
|
} else if (auth.username && auth.password) {
|
|
// Basic Auth with username/password
|
|
const token = Buffer.from(`${substituteVariables(auth.username, variables)}:${substituteVariables(auth.password, variables)}`).toString('base64');
|
|
headers['Authorization'] = `Basic ${token}`;
|
|
}
|
|
} else if (auth.type === 'apiKey' && auth.key && auth.value) {
|
|
if (auth.in === 'header') {
|
|
headers[substituteVariables(auth.key, variables)] = substituteVariables(auth.value, variables);
|
|
} else if (auth.in === 'query') {
|
|
url += (url.includes('?') ? '&' : '?') + `${encodeURIComponent(substituteVariables(auth.key, variables))}=${encodeURIComponent(substituteVariables(auth.value, variables))}`;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 5. Body
|
|
let body;
|
|
if (nodeData.body && nodeData.body.type && nodeData.body.type !== 'none') {
|
|
if (['form-data', 'x-www-form-urlencoded'].includes(nodeData.body.type)) {
|
|
const dataArr = Array.isArray(nodeData.body.data) ? nodeData.body.data : [];
|
|
if (nodeData.body.type === 'form-data') {
|
|
// For server-side, we'll use URLSearchParams for form-data
|
|
const formData = new URLSearchParams();
|
|
dataArr.forEach(item => {
|
|
if (item.key) formData.append(substituteVariables(item.key, variables), substituteVariables(item.value, variables));
|
|
});
|
|
body = formData.toString();
|
|
headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
|
} else {
|
|
// x-www-form-urlencoded
|
|
body = dataArr
|
|
.filter(item => item.key)
|
|
.map(item => `${encodeURIComponent(substituteVariables(item.key, variables))}=${encodeURIComponent(substituteVariables(item.value, variables))}`)
|
|
.join('&');
|
|
headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
|
}
|
|
} else if (nodeData.body.type === 'raw') {
|
|
body = substituteVariables(nodeData.body.data, variables);
|
|
// Try to detect JSON
|
|
if (body && body.trim().startsWith('{')) {
|
|
headers['Content-Type'] = 'application/json';
|
|
}
|
|
}
|
|
}
|
|
|
|
return { url, headers, body };
|
|
}
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
try {
|
|
// Get request body
|
|
const body = await readBody(event);
|
|
|
|
// Extract node configuration and process variables
|
|
const { nodeData, processVariables } = body;
|
|
|
|
// Validate input
|
|
if (!nodeData || !nodeData.apiUrl) {
|
|
return {
|
|
success: false,
|
|
error: {
|
|
message: 'Invalid API node configuration. Missing apiUrl.'
|
|
}
|
|
};
|
|
}
|
|
|
|
// Build the API request
|
|
const { url, headers, body: requestBody } = buildApiRequest(nodeData, processVariables);
|
|
const apiMethod = nodeData.apiMethod || 'GET';
|
|
const outputVariable = nodeData.outputVariable || 'apiResponse';
|
|
const errorVariable = nodeData.errorVariable || 'apiError';
|
|
const continueOnError = nodeData.continueOnError || false;
|
|
|
|
|
|
|
|
// Prepare fetch options
|
|
const fetchOptions = {
|
|
method: apiMethod,
|
|
headers
|
|
};
|
|
|
|
// Add body for non-GET requests
|
|
if (!['GET', 'HEAD'].includes(apiMethod) && requestBody) {
|
|
fetchOptions.body = requestBody;
|
|
}
|
|
|
|
// Make the API call
|
|
const response = await fetch(url, fetchOptions);
|
|
|
|
// Get response data
|
|
let responseData;
|
|
const contentType = response.headers.get('content-type');
|
|
|
|
if (contentType && contentType.includes('application/json')) {
|
|
responseData = await response.json();
|
|
} else {
|
|
responseData = await response.text();
|
|
}
|
|
|
|
// Prepare result
|
|
const result = {
|
|
success: response.ok,
|
|
data: responseData,
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
headers: Object.fromEntries([...response.headers.entries()])
|
|
};
|
|
|
|
if (!response.ok) {
|
|
result.error = {
|
|
message: `API call failed with status ${response.status}`,
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
data: responseData
|
|
};
|
|
}
|
|
|
|
return result;
|
|
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: {
|
|
message: error.message || 'An error occurred while making the API call',
|
|
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
|
|
}
|
|
};
|
|
}
|
|
});
|