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

View File

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

View File

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