diff --git a/components/process-flow/ApiNodeConfiguration.vue b/components/process-flow/ApiNodeConfiguration.vue
new file mode 100644
index 0000000..e32c927
--- /dev/null
+++ b/components/process-flow/ApiNodeConfiguration.vue
@@ -0,0 +1,410 @@
+
+
+
API Call Configuration
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ You can use process variables with curly braces: https://example.com/api/users/{userId}
+
+
+
+
+
+
+
+ You can use process variables with curly braces: { "userId": "{userId}" }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Test API Call
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/components/process-flow/ProcessBuilderComponents.vue b/components/process-flow/ProcessBuilderComponents.vue
index 47f87d7..4b923de 100644
--- a/components/process-flow/ProcessBuilderComponents.vue
+++ b/components/process-flow/ProcessBuilderComponents.vue
@@ -99,6 +99,26 @@ const availableComponents = [
}
}
},
+ {
+ type: 'api',
+ name: 'API Call',
+ category: 'Core',
+ icon: 'material-symbols:api',
+ description: 'Make external API calls',
+ defaultProps: {
+ label: 'API Call',
+ data: {
+ description: 'External API call',
+ apiMethod: 'GET',
+ apiUrl: '',
+ requestBody: '',
+ headers: '{ "Content-Type": "application/json" }',
+ outputVariable: 'apiResponse',
+ continueOnError: false,
+ errorVariable: 'apiError'
+ }
+ }
+ },
{
type: 'gateway',
name: 'Decision Point',
diff --git a/components/process-flow/ProcessFlowNodes.js b/components/process-flow/ProcessFlowNodes.js
index bcac2e0..d522655 100644
--- a/components/process-flow/ProcessFlowNodes.js
+++ b/components/process-flow/ProcessFlowNodes.js
@@ -271,6 +271,54 @@ export const ScriptNode = markRaw({
}
});
+// API Call node
+export const ApiCallNode = markRaw({
+ props: ['id', 'type', 'label', 'selected', 'data'],
+ computed: {
+ nodeLabel() {
+ return this.label || (this.data && this.data.label) || 'API Call';
+ },
+ apiUrl() {
+ return this.data?.apiUrl || 'No URL specified';
+ },
+ apiMethod() {
+ return this.data?.apiMethod || 'GET';
+ }
+ },
+ render() {
+ const badgeContent = h('span', {
+ class: 'node-badge bg-indigo-100 text-indigo-600 px-1 text-xs rounded'
+ }, 'API');
+
+ return h(CustomNode, {
+ id: this.id,
+ type: 'api',
+ label: this.nodeLabel,
+ selected: this.selected,
+ data: this.data,
+ onClick: () => this.$emit('node-click', this.id)
+ }, {
+ icon: () => h('i', { class: 'material-icons text-indigo-500' }, 'api'),
+ badge: () => badgeContent,
+ default: () => h('div', { class: 'node-details' }, [
+ h('p', { class: 'node-description' }, this.data?.description || 'External API call'),
+ h('div', { class: 'node-api-info' }, [
+ h('span', { class: 'node-api-label' }, 'URL: '),
+ h('span', {
+ class: 'node-api-value text-indigo-600 font-medium'
+ }, this.apiUrl)
+ ]),
+ h('div', { class: 'node-api-method-info' }, [
+ h('span', { class: 'node-api-method-label' }, 'Method: '),
+ h('span', {
+ class: 'node-api-method-value'
+ }, this.apiMethod)
+ ])
+ ])
+ });
+ }
+});
+
// Export the node types object to use with Vue Flow
export const nodeTypes = markRaw({
task: TaskNode,
@@ -278,7 +326,8 @@ export const nodeTypes = markRaw({
end: EndNode,
gateway: GatewayNode,
form: FormNode,
- script: ScriptNode
+ script: ScriptNode,
+ api: ApiCallNode
});
// Default CSS for the nodes to be imported where needed
@@ -297,7 +346,7 @@ export const nodeStyles = `
}
/* Base styles for different node types */
-.node-task, .node-form, .node-script {
+.node-task, .node-form, .node-script, .node-api {
width: 180px;
background: white;
border-radius: 4px;
@@ -394,7 +443,8 @@ export const nodeStyles = `
.node-task .custom-node-title,
.node-form .custom-node-title,
-.node-script .custom-node-title {
+.node-script .custom-node-title,
+.node-api .custom-node-title {
font-weight: 500;
font-size: 11px;
display: flex;
@@ -461,6 +511,8 @@ export const nodeStyles = `
.node-assignee,
.node-form-info,
.node-script-info,
+.node-api-info,
+.node-api-method-info,
.node-conditions {
display: flex;
font-size: 10px;
@@ -470,13 +522,17 @@ export const nodeStyles = `
.node-assignee-label,
.node-form-label,
-.node-script-label {
+.node-script-label,
+.node-api-label,
+.node-api-method-label {
font-weight: 500;
margin-right: 4px;
}
.node-form-value,
.node-script-value,
+.node-api-value,
+.node-api-method-value,
.node-assignee-value {
white-space: nowrap;
overflow: hidden;
@@ -507,6 +563,7 @@ export const nodeStyles = `
.handle-task-input,
.handle-form-input,
.handle-script-input,
+.handle-api-input,
.handle-gateway-input {
top: -5px !important;
width: 8px !important;
@@ -517,6 +574,7 @@ export const nodeStyles = `
.handle-task-output,
.handle-form-output,
.handle-script-output,
+.handle-api-output,
.handle-gateway-output {
bottom: -5px !important;
width: 8px !important;
diff --git a/components/process-flow/VariableManager.vue b/components/process-flow/VariableManager.vue
index 5246015..8997110 100644
--- a/components/process-flow/VariableManager.vue
+++ b/components/process-flow/VariableManager.vue
@@ -56,14 +56,6 @@
{{
variable.name
}}
-
- {{ variable.scope }}
-
{{ variable.type }}
@@ -129,12 +121,11 @@
label="Type"
:options="[
{ label: 'String', value: 'string' },
- { label: 'Number', value: 'number' },
- { label: 'Boolean', value: 'boolean' },
+ { label: 'Int', value: 'int' },
+ { label: 'Decimal', value: 'decimal' },
{ label: 'Object', value: 'object' },
- { label: 'Array', value: 'array' },
- { label: 'Date', value: 'date' },
- { label: 'File', value: 'file' },
+ { label: 'DateTime', value: 'datetime' },
+ { label: 'Date', value: 'date' }
]"
validation="required"
:validation-messages="{
@@ -142,21 +133,6 @@
}"
/>
-
-
-
-
Cancel
@@ -199,19 +167,14 @@ const editingVariable = ref(null);
const variableForm = ref({
name: "",
type: "string",
- scope: "process",
+ scope: "global",
description: "",
- isRequired: false,
});
// Computed
const variables = computed(() => {
- // This was only returning process variables, let's fix it to return both process and global variables
- const allVars = [
- ...variableStore.getAllVariables.process,
- ...variableStore.getAllVariables.global,
- ];
- return allVars;
+ // Only return global variables
+ return variableStore.getAllVariables.global;
});
// Methods
@@ -223,7 +186,7 @@ const editVariable = (variable) => {
const deleteVariable = (variable) => {
if (confirm(`Are you sure you want to delete ${variable.name}?`)) {
- variableStore.deleteVariable(variable.name, variable.scope);
+ variableStore.deleteVariable(variable.name, 'global');
}
};
@@ -231,9 +194,8 @@ const resetForm = () => {
variableForm.value = {
name: "",
type: "string",
- scope: "process",
- description: "",
- isRequired: false,
+ scope: "global",
+ description: ""
};
editingVariable.value = null;
};
@@ -249,9 +211,8 @@ const saveVariable = async (formData) => {
const newVariable = {
name: formData.name,
type: formData.type,
- scope: formData.scope,
- description: formData.description,
- isRequired: formData.isRequired,
+ scope: "global",
+ description: formData.description
};
if (editingVariable.value) {
@@ -259,7 +220,7 @@ const saveVariable = async (formData) => {
variableStore.updateVariable(
editingVariable.value.name,
newVariable,
- newVariable.scope
+ "global"
);
} else {
// Add new variable
diff --git a/pages/process-builder/index.vue b/pages/process-builder/index.vue
index dc43e03..44676bd 100644
--- a/pages/process-builder/index.vue
+++ b/pages/process-builder/index.vue
@@ -7,6 +7,7 @@ import ProcessFlowCanvas from '~/components/process-flow/ProcessFlowCanvas.vue';
import ProcessBuilderComponents from '~/components/process-flow/ProcessBuilderComponents.vue';
import FormSelector from '~/components/process-flow/FormSelector.vue';
import GatewayConditionManager from '~/components/process-flow/GatewayConditionManager.vue';
+import ApiNodeConfiguration from '~/components/process-flow/ApiNodeConfiguration.vue';
import VariableManager from '~/components/process-flow/VariableManager.vue';
import { onBeforeRouteLeave } from 'vue-router';
@@ -74,6 +75,22 @@ const components = [
iconColor: 'text-purple-500',
data: { description: 'Form submission', formName: '' }
},
+ {
+ type: 'api',
+ label: 'API Call',
+ icon: 'api',
+ iconColor: 'text-indigo-500',
+ data: {
+ description: 'External API call',
+ apiMethod: 'GET',
+ apiUrl: '',
+ requestBody: '',
+ headers: '{ "Content-Type": "application/json" }',
+ outputVariable: 'apiResponse',
+ continueOnError: false,
+ errorVariable: 'apiError'
+ }
+ },
{
type: 'gateway',
label: 'Decision Point',
@@ -724,6 +741,13 @@ const onConditionsUpdated = (conditions) => {
/>
+
+
+
{
+ try {
+ // Get request body
+ const body = await readBody(event);
+
+ // Extract node configuration and process variables
+ const { nodeConfig, processVariables } = body;
+
+ // Validate input
+ if (!nodeConfig || !nodeConfig.apiUrl) {
+ return {
+ success: false,
+ error: {
+ message: 'Invalid API node configuration. Missing apiUrl.'
+ }
+ };
+ }
+
+ // Structure the variables for the API call
+ const structuredVariables = {
+ global: {},
+ process: {},
+ ...processVariables
+ };
+
+ // Categorize variables by scope if they're not already structured
+ if (!processVariables.global && !processVariables.process) {
+ Object.entries(processVariables || {}).forEach(([name, value]) => {
+ // Determine which variables are global based on the presence of an actual variable in the store
+ // This would usually be handled by the process execution engine
+ if (name === nodeConfig.outputVariable || name === nodeConfig.errorVariable) {
+ structuredVariables.global[name] = value;
+ } else {
+ structuredVariables.process[name] = value;
+ }
+ });
+ }
+
+ // Execute the API call
+ const result = await executeApiCall(nodeConfig, structuredVariables);
+
+ // Update global variables with the result
+ if (result.success && nodeConfig.outputVariable) {
+ structuredVariables.global[nodeConfig.outputVariable] = result.data;
+ } else if (!result.success && nodeConfig.errorVariable) {
+ structuredVariables.global[nodeConfig.errorVariable] = result.error;
+ }
+
+ // Add the updated variables to the result
+ result.variables = structuredVariables;
+
+ // Return the result
+ return result;
+ } catch (error) {
+ // Handle errors
+ return {
+ success: false,
+ error: {
+ message: error.message || 'An error occurred while testing the API node',
+ stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
+ }
+ };
+ }
+});
\ No newline at end of file
diff --git a/services/apiNodeService.js b/services/apiNodeService.js
new file mode 100644
index 0000000..e94e014
--- /dev/null
+++ b/services/apiNodeService.js
@@ -0,0 +1,173 @@
+/**
+ * API Node Service
+ *
+ * This service handles the execution of API Call nodes in the process flow.
+ * It supports dynamic variable substitution in URLs, headers, and request bodies.
+ */
+
+// Helper function to substitute process variables in a string
+// Example: "Hello {name}" with {name: "World"} becomes "Hello World"
+function substituteVariables(text, variables) {
+ if (!text || typeof text !== 'string') return text;
+
+ return text.replace(/{([^{}]+)}/g, (match, variableName) => {
+ const variable = variables[variableName.trim()];
+ return variable !== undefined ? variable : match;
+ });
+}
+
+// Helper function to substitute variables in a JSON object or string
+function substituteVariablesInObject(obj, variables) {
+ if (typeof obj === 'string') {
+ try {
+ // If it's a JSON string, parse it, substitute, then stringify
+ const parsed = JSON.parse(obj);
+ return JSON.stringify(substituteVariablesInObject(parsed, variables));
+ } catch (e) {
+ // If it's not valid JSON, treat as regular string
+ return substituteVariables(obj, variables);
+ }
+ }
+
+ if (typeof obj !== 'object' || obj === null) {
+ return obj;
+ }
+
+ // Handle arrays
+ if (Array.isArray(obj)) {
+ return obj.map(item => substituteVariablesInObject(item, variables));
+ }
+
+ // Handle objects
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ result[key] = substituteVariablesInObject(value, variables);
+ }
+
+ return result;
+}
+
+// Execute an API call based on node configuration
+export async function executeApiCall(nodeConfig, processVariables) {
+ const {
+ apiMethod = 'GET',
+ apiUrl = '',
+ requestBody = '',
+ headers = '{ "Content-Type": "application/json" }',
+ outputVariable = 'apiResponse',
+ errorVariable = 'apiError',
+ continueOnError = false
+ } = nodeConfig;
+
+ // Combine global and process variables
+ const allVariables = {
+ ...processVariables.global || {},
+ ...processVariables.process || {},
+ ...processVariables
+ };
+
+ // Substitute variables in URL
+ const processedUrl = substituteVariables(apiUrl, allVariables);
+
+ // Parse and process headers
+ let processedHeaders = {};
+ try {
+ if (typeof headers === 'string') {
+ processedHeaders = JSON.parse(headers);
+ } else if (typeof headers === 'object' && headers !== null) {
+ processedHeaders = headers;
+ }
+
+ // Substitute variables in headers
+ processedHeaders = substituteVariablesInObject(processedHeaders, allVariables);
+ } catch (error) {
+ console.error('Error processing headers:', error);
+ processedHeaders = { 'Content-Type': 'application/json' };
+ }
+
+ // Prepare request options
+ const options = {
+ method: apiMethod,
+ headers: processedHeaders
+ };
+
+ // Add request body for appropriate methods
+ if (['POST', 'PUT', 'PATCH'].includes(apiMethod) && requestBody) {
+ let processedBody;
+
+ try {
+ // Try to parse as JSON if it's a string
+ if (typeof requestBody === 'string') {
+ const bodyObj = JSON.parse(requestBody);
+ processedBody = substituteVariablesInObject(bodyObj, allVariables);
+ options.body = JSON.stringify(processedBody);
+ } else {
+ // If it's already an object
+ processedBody = substituteVariablesInObject(requestBody, allVariables);
+ options.body = JSON.stringify(processedBody);
+ }
+ } catch (error) {
+ // If not valid JSON, treat as string with variable substitution
+ options.body = substituteVariables(requestBody, allVariables);
+ }
+ }
+
+ // Create result object
+ const result = {
+ outputVariable,
+ errorVariable,
+ continueOnError,
+ outputScope: 'global', // Specify that output should go to global variables
+ success: false,
+ data: null,
+ error: null
+ };
+
+ try {
+ // Make the API call
+ const response = await fetch(processedUrl, options);
+
+ // 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();
+ }
+
+ // Store result data
+ result.success = response.ok;
+ result.data = {
+ status: response.status,
+ statusText: response.statusText,
+ headers: Object.fromEntries([...response.headers.entries()]),
+ data: responseData
+ };
+
+ // If not successful, also store as error
+ if (!response.ok) {
+ result.error = {
+ message: `API call failed with status ${response.status}`,
+ status: response.status,
+ statusText: response.statusText,
+ data: responseData
+ };
+ }
+ } catch (error) {
+ // Handle network or other errors
+ result.success = false;
+ result.error = {
+ message: error.message || 'Unknown error occurred during API call',
+ stack: error.stack,
+ name: error.name
+ };
+ }
+
+ return result;
+}
+
+export default {
+ executeApiCall
+};
\ No newline at end of file
diff --git a/stores/variableStore.js b/stores/variableStore.js
index a13efa2..0d49096 100644
--- a/stores/variableStore.js
+++ b/stores/variableStore.js
@@ -37,23 +37,40 @@ export const useVariableStore = defineStore('variables', {
actions: {
// Add a new variable
addVariable(variable) {
- const { scope = 'process' } = variable;
+ // Set scope to global by default
+ const updatedVariable = {
+ ...variable,
+ scope: 'global'
+ };
- if (scope === 'global') {
- this.globalVariables.push({
- ...variable,
- scope: 'global'
- });
+ // For backward compatibility, still respect the scope if it's explicitly set
+ if (variable.scope === 'process') {
+ this.processVariables.push(updatedVariable);
} else {
- this.processVariables.push({
- ...variable,
- scope: 'process'
- });
+ this.globalVariables.push(updatedVariable);
+ }
+ },
+
+ // Add a variable only if it doesn't already exist
+ addVariableIfNotExists(variable) {
+ const name = variable.name;
+ // Default to global scope
+ const scope = variable.scope || 'global';
+ const variables = scope === 'global'
+ ? this.globalVariables
+ : this.processVariables;
+
+ // Check if variable already exists
+ const exists = variables.some(v => v.name === name);
+
+ // If it doesn't exist, add it
+ if (!exists) {
+ this.addVariable(variable);
}
},
// Update an existing variable
- updateVariable(name, updates, scope = 'process') {
+ updateVariable(name, updates, scope = 'global') {
const variables = scope === 'global'
? this.globalVariables
: this.processVariables;
@@ -62,13 +79,14 @@ export const useVariableStore = defineStore('variables', {
if (index !== -1) {
variables[index] = {
...variables[index],
- ...updates
+ ...updates,
+ scope // Ensure the scope is preserved
};
}
},
// Delete a variable
- deleteVariable(name, scope = 'process') {
+ deleteVariable(name, scope = 'global') {
const variables = scope === 'global'
? this.globalVariables
: this.processVariables;