Enhance Workflow API Call Handling and Authorization Logic

- 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.
This commit is contained in:
Afiq 2025-08-04 12:50:54 +08:00
parent dfea8e7f47
commit bae98c2b17
4 changed files with 545 additions and 348 deletions

View File

@ -655,11 +655,18 @@ function buildApiRequest(nodeData, variables) {
// 4. Authorization // 4. Authorization
if (nodeData.authorization && nodeData.authorization.type && nodeData.authorization.type !== 'none') { if (nodeData.authorization && nodeData.authorization.type && nodeData.authorization.type !== 'none') {
const auth = nodeData.authorization; const auth = nodeData.authorization;
if (auth.type === 'bearer' && auth.token) { if (auth.type === 'bearer' && auth.token) {
headers['Authorization'] = `Bearer ${substituteVariables(auth.token, variables)}`; headers['Authorization'] = `Bearer ${substituteVariables(auth.token, variables)}`;
} else if (auth.type === 'basic' && auth.username && auth.password) { } else if (auth.type === 'basic') {
const token = btoa(`${substituteVariables(auth.username, variables)}:${substituteVariables(auth.password, variables)}`); if (auth.token) {
headers['Authorization'] = `Basic ${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 = btoa(`${substituteVariables(auth.username, variables)}:${substituteVariables(auth.password, variables)}`);
headers['Authorization'] = `Basic ${token}`;
}
} else if (auth.type === 'apiKey' && auth.key && auth.value) { } else if (auth.type === 'apiKey' && auth.key && auth.value) {
if (auth.in === 'header') { if (auth.in === 'header') {
headers[substituteVariables(auth.key, variables)] = substituteVariables(auth.value, variables); headers[substituteVariables(auth.key, variables)] = substituteVariables(auth.value, variables);
@ -712,26 +719,45 @@ const executeCurrentStep = async () => {
if (currentNode.value?.type === 'api') { if (currentNode.value?.type === 'api') {
console.log(`[Workflow] Executing API node: ${currentNode.value.data?.label || currentNode.value.label}`); console.log(`[Workflow] Executing API node: ${currentNode.value.data?.label || currentNode.value.label}`);
const nodeData = currentNode.value.data || {}; const nodeData = currentNode.value.data || {};
// Use new structure if present
if (nodeData.body || nodeData.headers || nodeData.params || nodeData.authorization) { // Use new structure if present (check for any new structure properties)
const { url, headers, body } = buildApiRequest(nodeData, processVariables.value); if (nodeData.body !== undefined || nodeData.headers !== undefined || nodeData.params !== undefined || nodeData.authorization !== undefined) {
const apiMethod = nodeData.apiMethod || 'GET';
const outputVariable = nodeData.outputVariable || 'apiResponse'; const outputVariable = nodeData.outputVariable || 'apiResponse';
const errorVariable = nodeData.errorVariable || 'apiError'; const errorVariable = nodeData.errorVariable || 'apiError';
const continueOnError = nodeData.continueOnError || false; const continueOnError = nodeData.continueOnError || false;
try { try {
const response = await $fetch(url, { // Use proxy endpoint to avoid CORS issues
method: apiMethod, const response = await $fetch('/api/process/workflow-api-call', {
headers, method: 'POST',
body: ['GET', 'HEAD'].includes(apiMethod) ? undefined : body, body: {
nodeData,
processVariables: processVariables.value
}
}); });
processVariables.value[outputVariable] = response;
processVariables.value[errorVariable] = null; if (response.success) {
console.log('[Workflow] API call success. Output variable set:', outputVariable, response); processVariables.value[outputVariable] = response.data;
if (canAutoProgress(currentNode.value)) { processVariables.value[errorVariable] = null;
moveToNextStep(); console.log('[Workflow] API call success. Output variable set:', outputVariable, response.data);
if (canAutoProgress(currentNode.value)) {
moveToNextStep();
} else {
console.log('[Workflow] API completed, multiple paths available - waiting for user choice');
}
} else { } else {
console.log('[Workflow] API completed, multiple paths available - waiting for user choice'); processVariables.value[errorVariable] = response.error;
console.error('[Workflow] API call failed:', response.error);
if (continueOnError) {
if (canAutoProgress(currentNode.value)) {
moveToNextStep();
} else {
console.log('[Workflow] API failed but continuing, multiple paths available - waiting for user choice');
}
} else {
error.value = 'API call failed: ' + (response.error.message || response.error);
notifyParentOfError(error.value);
}
} }
} catch (err) { } catch (err) {
processVariables.value[errorVariable] = err; processVariables.value[errorVariable] = err;
@ -749,6 +775,7 @@ const executeCurrentStep = async () => {
} }
} else { } else {
// Fallback: old structure // Fallback: old structure
const { const {
apiUrl, apiUrl,
apiMethod = 'GET', apiMethod = 'GET',

View File

@ -1,73 +1,82 @@
{ {
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"definitions": { "definitions": {
"user": { "caseInstance": {
"type": "object", "type": "object",
"properties": { "properties": {
"userID": { "caseID": {
"type": "integer" "type": "integer"
}, },
"userSecretKey": { "caseUUID": {
"type": "string"
},
"caseName": {
"type": "string"
},
"caseStatus": {
"type": "string",
"default": "active"
},
"caseVariables": {
"type": [ "type": [
"number",
"string", "string",
"boolean",
"object",
"array",
"null" "null"
] ]
}, },
"userUsername": { "caseSettings": {
"type": [ "type": [
"number",
"string", "string",
"boolean",
"object",
"array",
"null" "null"
] ]
}, },
"userPassword": { "caseDefinition": {
"type": [ "type": [
"number",
"string", "string",
"boolean",
"object",
"array",
"null" "null"
] ]
}, },
"userFullName": { "caseCreatedDate": {
"type": [ "type": "string",
"string", "format": "date-time"
"null"
]
}, },
"userEmail": { "caseModifiedDate": {
"type": [
"string",
"null"
]
},
"userPhone": {
"type": [
"string",
"null"
]
},
"userStatus": {
"type": [
"string",
"null"
]
},
"userCreatedDate": {
"type": [ "type": [
"string", "string",
"null" "null"
], ],
"format": "date-time" "format": "date-time"
}, },
"userModifiedDate": { "caseCompletedDate": {
"type": [ "type": [
"string", "string",
"null" "null"
], ],
"format": "date-time" "format": "date-time"
}, },
"caseInstance": { "startedBy": {
"type": "array", "anyOf": [
"items": { {
"$ref": "#/definitions/caseInstance" "$ref": "#/definitions/user"
} },
{
"type": "null"
}
]
},
"process": {
"$ref": "#/definitions/process"
}, },
"caseTimeline": { "caseTimeline": {
"type": "array", "type": "array",
@ -75,123 +84,45 @@
"$ref": "#/definitions/caseTimeline" "$ref": "#/definitions/caseTimeline"
} }
}, },
"forms": {
"type": "array",
"items": {
"$ref": "#/definitions/form"
}
},
"formHistoryEntries": {
"type": "array",
"items": {
"$ref": "#/definitions/formHistory"
}
},
"processes": {
"type": "array",
"items": {
"$ref": "#/definitions/process"
}
},
"processHistoryEntries": {
"type": "array",
"items": {
"$ref": "#/definitions/processHistory"
}
},
"task": { "task": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/task" "$ref": "#/definitions/task"
} }
},
"userrole": {
"type": "array",
"items": {
"$ref": "#/definitions/userrole"
}
},
"startedCases": {
"type": "array",
"items": {
"$ref": "#/definitions/caseInstance"
}
},
"assignedTasks": {
"type": "array",
"items": {
"$ref": "#/definitions/task"
}
},
"caseTimelineEntries": {
"type": "array",
"items": {
"$ref": "#/definitions/caseTimeline"
}
} }
} }
}, },
"role": { "caseTimeline": {
"type": "object", "type": "object",
"properties": { "properties": {
"roleID": { "timelineID": {
"type": "integer" "type": "integer"
}, },
"roleName": { "timelineType": {
"type": "string"
},
"timelineDescription": {
"type": [ "type": [
"string", "string",
"null" "null"
] ]
}, },
"roleDescription": { "timelineDate": {
"type": [
"string",
"null"
]
},
"roleStatus": {
"type": [
"string",
"null"
]
},
"roleCreatedDate": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"roleModifiedDate": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"userrole": {
"type": "array",
"items": {
"$ref": "#/definitions/userrole"
}
}
}
},
"userrole": {
"type": "object",
"properties": {
"userRoleID": {
"type": "integer"
},
"userRoleCreatedDate": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
}, },
"role": { "caseInstance": {
"$ref": "#/definitions/role" "$ref": "#/definitions/caseInstance"
}, },
"user": { "user": {
"$ref": "#/definitions/user" "anyOf": [
{
"$ref": "#/definitions/user"
},
{
"type": "null"
}
]
} }
} }
}, },
@ -277,13 +208,13 @@
} }
] ]
}, },
"history": { "formHistory": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/formHistory" "$ref": "#/definitions/formHistory"
} }
}, },
"tasks": { "task": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/task" "$ref": "#/definitions/task"
@ -366,7 +297,7 @@
"form": { "form": {
"$ref": "#/definitions/form" "$ref": "#/definitions/form"
}, },
"savedByUser": { "user": {
"anyOf": [ "anyOf": [
{ {
"$ref": "#/definitions/user" "$ref": "#/definitions/user"
@ -507,17 +438,11 @@
} }
] ]
}, },
"history": { "processHistory": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/processHistory" "$ref": "#/definitions/processHistory"
} }
},
"cases": {
"type": "array",
"items": {
"$ref": "#/definitions/caseInstance"
}
} }
} }
}, },
@ -625,7 +550,7 @@
"process": { "process": {
"$ref": "#/definitions/process" "$ref": "#/definitions/process"
}, },
"savedByUser": { "user": {
"anyOf": [ "anyOf": [
{ {
"$ref": "#/definitions/user" "$ref": "#/definitions/user"
@ -637,93 +562,48 @@
} }
} }
}, },
"caseInstance": { "role": {
"type": "object", "type": "object",
"properties": { "properties": {
"caseID": { "roleID": {
"type": "integer" "type": "integer"
}, },
"caseUUID": { "roleName": {
"type": "string"
},
"caseName": {
"type": "string"
},
"caseStatus": {
"type": "string",
"default": "active"
},
"caseVariables": {
"type": [ "type": [
"number",
"string", "string",
"boolean",
"object",
"array",
"null" "null"
] ]
}, },
"caseSettings": { "roleDescription": {
"type": [ "type": [
"number",
"string", "string",
"boolean",
"object",
"array",
"null" "null"
] ]
}, },
"caseDefinition": { "roleStatus": {
"type": [ "type": [
"number",
"string", "string",
"boolean",
"object",
"array",
"null" "null"
] ]
}, },
"caseCreatedDate": { "roleCreatedDate": {
"type": "string",
"format": "date-time"
},
"caseModifiedDate": {
"type": [ "type": [
"string", "string",
"null" "null"
], ],
"format": "date-time" "format": "date-time"
}, },
"caseCompletedDate": { "roleModifiedDate": {
"type": [ "type": [
"string", "string",
"null" "null"
], ],
"format": "date-time" "format": "date-time"
}, },
"process": { "userrole": {
"$ref": "#/definitions/process"
},
"startedBy": {
"anyOf": [
{
"$ref": "#/definitions/user"
},
{
"type": "null"
}
]
},
"tasks": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/task" "$ref": "#/definitions/userrole"
}
},
"timeline": {
"type": "array",
"items": {
"$ref": "#/definitions/caseTimeline"
} }
} }
} }
@ -775,10 +655,10 @@
], ],
"format": "date-time" "format": "date-time"
}, },
"case": { "caseInstance": {
"$ref": "#/definitions/caseInstance" "$ref": "#/definitions/caseInstance"
}, },
"assignedTo": { "user": {
"anyOf": [ "anyOf": [
{ {
"$ref": "#/definitions/user" "$ref": "#/definitions/user"
@ -800,51 +680,144 @@
} }
} }
}, },
"caseTimeline": { "user": {
"type": "object", "type": "object",
"properties": { "properties": {
"timelineID": { "userID": {
"type": "integer" "type": "integer"
}, },
"timelineType": { "userSecretKey": {
"type": "string"
},
"timelineDescription": {
"type": [ "type": [
"string", "string",
"null" "null"
] ]
}, },
"timelineDate": { "userUsername": {
"type": [
"string",
"null"
]
},
"userPassword": {
"type": [
"string",
"null"
]
},
"userFullName": {
"type": [
"string",
"null"
]
},
"userEmail": {
"type": [
"string",
"null"
]
},
"userPhone": {
"type": [
"string",
"null"
]
},
"userStatus": {
"type": [
"string",
"null"
]
},
"userCreatedDate": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"userModifiedDate": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"caseInstance": {
"type": "array",
"items": {
"$ref": "#/definitions/caseInstance"
}
},
"caseTimeline": {
"type": "array",
"items": {
"$ref": "#/definitions/caseTimeline"
}
},
"form": {
"type": "array",
"items": {
"$ref": "#/definitions/form"
}
},
"formHistory": {
"type": "array",
"items": {
"$ref": "#/definitions/formHistory"
}
},
"process": {
"type": "array",
"items": {
"$ref": "#/definitions/process"
}
},
"processHistory": {
"type": "array",
"items": {
"$ref": "#/definitions/processHistory"
}
},
"task": {
"type": "array",
"items": {
"$ref": "#/definitions/task"
}
},
"userrole": {
"type": "array",
"items": {
"$ref": "#/definitions/userrole"
}
}
}
},
"userrole": {
"type": "object",
"properties": {
"userRoleID": {
"type": "integer"
},
"userRoleCreatedDate": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
}, },
"case": { "role": {
"$ref": "#/definitions/caseInstance" "$ref": "#/definitions/role"
}, },
"createdBy": { "user": {
"anyOf": [ "$ref": "#/definitions/user"
{
"$ref": "#/definitions/user"
},
{
"type": "null"
}
]
} }
} }
} }
}, },
"type": "object", "type": "object",
"properties": { "properties": {
"user": { "caseInstance": {
"$ref": "#/definitions/user" "$ref": "#/definitions/caseInstance"
}, },
"role": { "caseTimeline": {
"$ref": "#/definitions/role" "$ref": "#/definitions/caseTimeline"
},
"userrole": {
"$ref": "#/definitions/userrole"
}, },
"form": { "form": {
"$ref": "#/definitions/form" "$ref": "#/definitions/form"
@ -858,14 +831,17 @@
"processHistory": { "processHistory": {
"$ref": "#/definitions/processHistory" "$ref": "#/definitions/processHistory"
}, },
"caseInstance": { "role": {
"$ref": "#/definitions/caseInstance" "$ref": "#/definitions/role"
}, },
"task": { "task": {
"$ref": "#/definitions/task" "$ref": "#/definitions/task"
}, },
"caseTimeline": { "user": {
"$ref": "#/definitions/caseTimeline" "$ref": "#/definitions/user"
},
"userrole": {
"$ref": "#/definitions/userrole"
} }
} }
} }

View File

@ -12,50 +12,42 @@ datasource db {
url = env("DATABASE_URL") url = env("DATABASE_URL")
} }
model user { model caseInstance {
userID Int @id @default(autoincrement()) caseID Int @id @default(autoincrement())
userSecretKey String? @db.VarChar(255) caseUUID String @unique @db.VarChar(36)
userUsername String? @db.VarChar(255) processID Int
userPassword String? @db.VarChar(255) caseName String @db.VarChar(255)
userFullName String? @db.VarChar(255) caseStatus String @default("active") @db.VarChar(50)
userEmail String? @db.VarChar(255) caseStartedBy Int?
userPhone String? @db.VarChar(255) caseVariables Json?
userStatus String? @db.VarChar(255) caseSettings Json?
userCreatedDate DateTime? @db.DateTime(0) caseDefinition Json?
userModifiedDate DateTime? @db.DateTime(0) caseCreatedDate DateTime @default(now()) @db.DateTime(0)
caseInstance caseInstance[] caseModifiedDate DateTime? @db.DateTime(0)
caseTimeline caseTimeline[] caseCompletedDate DateTime? @db.DateTime(0)
forms form[] @relation("FormCreator") startedBy user? @relation(fields: [caseStartedBy], references: [userID])
formHistoryEntries formHistory[] process process @relation(fields: [processID], references: [processID])
processes process[] @relation("ProcessCreator") caseTimeline caseTimeline[]
processHistoryEntries processHistory[] task task[]
task task[]
userrole userrole[] @@index([processID], map: "FK_case_process")
startedCases caseInstance[] @relation("CaseStartedBy") @@index([caseStartedBy], map: "FK_case_startedBy")
assignedTasks task[] @relation("TaskAssignedTo") @@index([caseStatus], map: "IDX_case_status")
caseTimelineEntries caseTimeline[]
} }
model role { model caseTimeline {
roleID Int @id @default(autoincrement()) timelineID Int @id @default(autoincrement())
roleName String? @db.VarChar(255) caseID Int
roleDescription String? @db.VarChar(255) timelineType String @db.VarChar(50)
roleStatus String? @db.VarChar(255) timelineDescription String? @db.Text
roleCreatedDate DateTime? @db.DateTime(0) timelineDate DateTime @default(now()) @db.DateTime(0)
roleModifiedDate DateTime? @db.DateTime(0) timelineCreatedBy Int?
userrole userrole[] caseInstance caseInstance @relation(fields: [caseID], references: [caseID])
} user user? @relation(fields: [timelineCreatedBy], references: [userID])
model userrole { @@index([caseID], map: "FK_caseTimeline_case")
userRoleID Int @id @default(autoincrement()) @@index([timelineCreatedBy], map: "FK_caseTimeline_createdBy")
userRoleUserID Int @default(0) @@index([timelineDate], map: "IDX_caseTimeline_date")
userRoleRoleID Int @default(0)
userRoleCreatedDate DateTime @db.DateTime(0)
role role @relation(fields: [userRoleRoleID], references: [roleID], onDelete: NoAction, onUpdate: NoAction, map: "FK_userrole_role")
user user @relation(fields: [userRoleUserID], references: [userID], onDelete: NoAction, onUpdate: NoAction, map: "FK_userrole_user")
@@index([userRoleRoleID], map: "FK_userrole_role")
@@index([userRoleUserID], map: "FK_userrole_user")
} }
model form { model form {
@ -67,14 +59,14 @@ model form {
formStatus String @default("active") @db.VarChar(50) formStatus String @default("active") @db.VarChar(50)
formCreatedBy Int? formCreatedBy Int?
formCreatedDate DateTime @default(now()) @db.DateTime(0) formCreatedDate DateTime @default(now()) @db.DateTime(0)
formModifiedDate DateTime? @updatedAt @db.DateTime(0) formModifiedDate DateTime? @db.DateTime(0)
customCSS String? @db.Text customCSS String? @db.Text
customScript String? @db.LongText customScript String? @db.LongText
formEvents Json? formEvents Json?
scriptMode String? @default("safe") @db.VarChar(20) scriptMode String? @default("safe") @db.VarChar(20)
creator user? @relation("FormCreator", fields: [formCreatedBy], references: [userID]) creator user? @relation(fields: [formCreatedBy], references: [userID])
history formHistory[] @relation("FormHistoryEntries") formHistory formHistory[]
tasks task[] task task[]
@@index([formCreatedBy], map: "FK_form_creator") @@index([formCreatedBy], map: "FK_form_creator")
} }
@ -95,13 +87,13 @@ model formHistory {
changeDescription String? @db.Text changeDescription String? @db.Text
savedBy Int? savedBy Int?
savedDate DateTime @default(now()) @db.DateTime(0) savedDate DateTime @default(now()) @db.DateTime(0)
form form @relation("FormHistoryEntries", fields: [formID], references: [formID], onDelete: Cascade) form form @relation(fields: [formID], references: [formID], onDelete: Cascade)
savedByUser user? @relation(fields: [savedBy], references: [userID]) user user? @relation(fields: [savedBy], references: [userID])
@@index([formID], map: "FK_formHistory_form") @@index([formID], map: "FK_formHistory_form")
@@index([savedBy], map: "FK_formHistory_savedBy") @@index([savedBy], map: "FK_formHistory_savedBy")
@@index([formUUID], map: "IDX_formHistory_uuid")
@@index([savedDate], map: "IDX_formHistory_date") @@index([savedDate], map: "IDX_formHistory_date")
@@index([formUUID], map: "IDX_formHistory_uuid")
} }
model process { model process {
@ -114,7 +106,7 @@ model process {
processStatus String @default("draft") @db.VarChar(50) processStatus String @default("draft") @db.VarChar(50)
processCreatedBy Int? processCreatedBy Int?
processCreatedDate DateTime @default(now()) @db.DateTime(0) processCreatedDate DateTime @default(now()) @db.DateTime(0)
processModifiedDate DateTime? @updatedAt @db.DateTime(0) processModifiedDate DateTime? @db.DateTime(0)
isTemplate Boolean @default(false) isTemplate Boolean @default(false)
processCategory String? @db.VarChar(100) processCategory String? @db.VarChar(100)
processOwner String? @db.VarChar(255) processOwner String? @db.VarChar(255)
@ -125,13 +117,12 @@ model process {
templateCategory String? @db.VarChar(100) templateCategory String? @db.VarChar(100)
processDeletedDate DateTime? @db.DateTime(0) processDeletedDate DateTime? @db.DateTime(0)
caseInstance caseInstance[] caseInstance caseInstance[]
creator user? @relation("ProcessCreator", fields: [processCreatedBy], references: [userID]) creator user? @relation(fields: [processCreatedBy], references: [userID])
history processHistory[] @relation("ProcessHistoryEntries") processHistory processHistory[]
cases caseInstance[]
@@index([processCreatedBy], map: "FK_process_creator") @@index([processCreatedBy], map: "FK_process_creator")
@@index([processStatus], map: "IDX_process_status")
@@index([processCategory], map: "IDX_process_category") @@index([processCategory], map: "IDX_process_category")
@@index([processStatus], map: "IDX_process_status")
@@index([isTemplate], map: "IDX_process_template") @@index([isTemplate], map: "IDX_process_template")
} }
@ -155,72 +146,77 @@ model processHistory {
changeDescription String? @db.Text changeDescription String? @db.Text
savedBy Int? savedBy Int?
savedDate DateTime @default(now()) @db.DateTime(0) savedDate DateTime @default(now()) @db.DateTime(0)
process process @relation("ProcessHistoryEntries", fields: [processID], references: [processID], onDelete: Cascade) process process @relation(fields: [processID], references: [processID], onDelete: Cascade)
savedByUser user? @relation(fields: [savedBy], references: [userID]) user user? @relation(fields: [savedBy], references: [userID])
@@index([processID], map: "FK_processHistory_process") @@index([processID], map: "FK_processHistory_process")
@@index([savedBy], map: "FK_processHistory_savedBy") @@index([savedBy], map: "FK_processHistory_savedBy")
@@index([processUUID], map: "IDX_processHistory_uuid")
@@index([savedDate], map: "IDX_processHistory_date") @@index([savedDate], map: "IDX_processHistory_date")
@@index([processUUID], map: "IDX_processHistory_uuid")
} }
model caseInstance { model role {
caseID Int @id @default(autoincrement()) roleID Int @id @default(autoincrement())
caseUUID String @unique @db.VarChar(36) roleName String? @db.VarChar(255)
processID Int roleDescription String? @db.VarChar(255)
caseName String @db.VarChar(255) roleStatus String? @db.VarChar(255)
caseStatus String @default("active") @db.VarChar(50) roleCreatedDate DateTime? @db.DateTime(0)
caseStartedBy Int? roleModifiedDate DateTime? @db.DateTime(0)
caseVariables Json? userrole userrole[]
caseSettings Json?
caseDefinition Json?
caseCreatedDate DateTime @default(now()) @db.DateTime(0)
caseModifiedDate DateTime? @updatedAt @db.DateTime(0)
caseCompletedDate DateTime? @db.DateTime(0)
process process @relation(fields: [processID], references: [processID])
startedBy user? @relation("CaseStartedBy", fields: [caseStartedBy], references: [userID])
tasks task[]
timeline caseTimeline[]
@@index([processID], map: "FK_case_process")
@@index([caseStartedBy], map: "FK_case_startedBy")
@@index([caseStatus], map: "IDX_case_status")
} }
model task { model task {
taskID Int @id @default(autoincrement()) taskID Int @id @default(autoincrement())
taskUUID String @unique @db.VarChar(36) taskUUID String @unique @db.VarChar(36)
caseID Int caseID Int
taskName String @db.VarChar(255) taskName String @db.VarChar(255)
taskType String @db.VarChar(50) taskType String @db.VarChar(50)
taskStatus String @default("pending") @db.VarChar(50) taskStatus String @default("pending") @db.VarChar(50)
taskAssignedTo Int? taskAssignedTo Int?
taskFormID Int? taskFormID Int?
taskData Json? taskData Json?
taskCreatedDate DateTime @default(now()) @db.DateTime(0) taskCreatedDate DateTime @default(now()) @db.DateTime(0)
taskModifiedDate DateTime? @updatedAt @db.DateTime(0) taskModifiedDate DateTime? @db.DateTime(0)
taskCompletedDate DateTime? @db.DateTime(0) taskCompletedDate DateTime? @db.DateTime(0)
case caseInstance @relation(fields: [caseID], references: [caseID]) caseInstance caseInstance @relation(fields: [caseID], references: [caseID])
assignedTo user? @relation("TaskAssignedTo", fields: [taskAssignedTo], references: [userID]) user user? @relation(fields: [taskAssignedTo], references: [userID])
form form? @relation(fields: [taskFormID], references: [formID]) form form? @relation(fields: [taskFormID], references: [formID])
@@index([caseID], map: "FK_task_case")
@@index([taskAssignedTo], map: "FK_task_assignedTo") @@index([taskAssignedTo], map: "FK_task_assignedTo")
@@index([caseID], map: "FK_task_case")
@@index([taskFormID], map: "FK_task_form") @@index([taskFormID], map: "FK_task_form")
@@index([taskStatus], map: "IDX_task_status") @@index([taskStatus], map: "IDX_task_status")
} }
model caseTimeline { model user {
timelineID Int @id @default(autoincrement()) userID Int @id @default(autoincrement())
caseID Int userSecretKey String? @db.VarChar(255)
timelineType String @db.VarChar(50) userUsername String? @db.VarChar(255)
timelineDescription String? @db.Text userPassword String? @db.VarChar(255)
timelineDate DateTime @default(now()) @db.DateTime(0) userFullName String? @db.VarChar(255)
timelineCreatedBy Int? userEmail String? @db.VarChar(255)
case caseInstance @relation(fields: [caseID], references: [caseID]) userPhone String? @db.VarChar(255)
createdBy user? @relation(fields: [timelineCreatedBy], references: [userID]) userStatus String? @db.VarChar(255)
userCreatedDate DateTime? @db.DateTime(0)
@@index([caseID], map: "FK_caseTimeline_case") userModifiedDate DateTime? @db.DateTime(0)
@@index([timelineCreatedBy], map: "FK_caseTimeline_createdBy") caseInstance caseInstance[]
@@index([timelineDate], map: "IDX_caseTimeline_date") caseTimeline caseTimeline[]
form form[]
formHistory formHistory[]
process process[]
processHistory processHistory[]
task task[]
userrole userrole[]
}
model userrole {
userRoleID Int @id @default(autoincrement())
userRoleUserID Int @default(0)
userRoleRoleID Int @default(0)
userRoleCreatedDate DateTime @db.DateTime(0)
role role @relation(fields: [userRoleRoleID], references: [roleID], onDelete: NoAction, onUpdate: NoAction, map: "FK_userrole_role")
user user @relation(fields: [userRoleUserID], references: [userID], onDelete: NoAction, onUpdate: NoAction, map: "FK_userrole_user")
@@index([userRoleRoleID], map: "FK_userrole_role")
@@index([userRoleUserID], map: "FK_userrole_user")
} }

View File

@ -0,0 +1,198 @@
/**
* 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
}
};
}
});