From 0a5f1af72520b87036173c04e498395431bf2ab8 Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Wed, 4 Mar 2026 13:09:02 +0100 Subject: [PATCH 01/11] Added handling of global variables to the task list of the management system --- .../data/organization/[dataPath]/route.ts | 21 +---- .../data/user/[userId]/[dataPath]/route.ts | 92 ++++++------------- .../app/api/spaces/[spaceId]/data/util.ts | 13 --- .../lib/data/db/machine-config.ts | 36 +++++++- .../lib/engines/deployment.ts | 3 + .../lib/engines/server-actions.ts | 46 +++++++++- 6 files changed, 114 insertions(+), 97 deletions(-) diff --git a/src/management-system-v2/app/api/spaces/[spaceId]/data/organization/[dataPath]/route.ts b/src/management-system-v2/app/api/spaces/[spaceId]/data/organization/[dataPath]/route.ts index 87745ad36..618cae73f 100644 --- a/src/management-system-v2/app/api/spaces/[spaceId]/data/organization/[dataPath]/route.ts +++ b/src/management-system-v2/app/api/spaces/[spaceId]/data/organization/[dataPath]/route.ts @@ -1,6 +1,6 @@ -import { getDeepConfigurationById, updateParameter } from '@/lib/data/db/machine-config'; +import { getNestedOrgParameter, updateParameter } from '@/lib/data/db/machine-config'; import { NextRequest, NextResponse } from 'next/server'; -import { filterParameter, getParameterFromPath } from '../../util'; +import { filterParameter } from '../../util'; export async function GET( request: NextRequest, @@ -11,14 +11,7 @@ export async function GET( try { const { spaceId, dataPath } = await params; - const conf = await getDeepConfigurationById(spaceId); - - let org = conf.content.find((entry) => entry.name === 'organization'); - - if (!org) return new NextResponse(null, { status: 404 }); - - const parameter = getParameterFromPath(org.subParameters, dataPath); - + const parameter = await getNestedOrgParameter(spaceId, dataPath); if (!parameter) return new NextResponse(null, { status: 404 }); let value = searchParams.has('full', 'true') ? filterParameter(parameter) : parameter.value; @@ -42,13 +35,7 @@ export async function PUT( return new NextResponse('Expected an object with a value ({ value: ... })', { status: 400 }); } - const conf = await getDeepConfigurationById(spaceId); - - let org = conf.content.find((entry) => entry.name === 'organization'); - - if (!org) return new NextResponse(null, { status: 404 }); - - const parameter = getParameterFromPath(org.subParameters, dataPath); + const parameter = await getNestedOrgParameter(spaceId, dataPath); if (!parameter) return new NextResponse(null, { status: 404 }); try { diff --git a/src/management-system-v2/app/api/spaces/[spaceId]/data/user/[userId]/[dataPath]/route.ts b/src/management-system-v2/app/api/spaces/[spaceId]/data/user/[userId]/[dataPath]/route.ts index 492509554..59328c337 100644 --- a/src/management-system-v2/app/api/spaces/[spaceId]/data/user/[userId]/[dataPath]/route.ts +++ b/src/management-system-v2/app/api/spaces/[spaceId]/data/user/[userId]/[dataPath]/route.ts @@ -1,34 +1,8 @@ -import { getUserConfig, updateParameter } from '@/lib/data/db/machine-config'; +import { getNestedUserParameter, updateParameter } from '@/lib/data/db/machine-config'; import { NextRequest, NextResponse } from 'next/server'; import { isUserErrorResponse } from '@/lib/user-error'; -import { filterParameter, getParameterFromPath } from '../../../util'; +import { filterParameter } from '../../../util'; import { getUserById } from '@/lib/data/db/iam/users'; -import { Parameter } from '@/lib/data/machine-config-schema'; -import { truthyFilter } from '@/lib/typescript-utils'; - -function toDummyParameter( - key: string, - value: any, - children: { key: string; value: any }[] = [], -): Parameter { - return { - id: key, - name: key, - displayName: [ - { - language: 'en', - text: key, - }, - ], - value, - parameterType: 'none', - hasChanges: false, - structureVisible: true, - usedAsInputParameterIn: [], - changeableByUser: false, - subParameters: children.map(({ key, value }) => toDummyParameter(key, value)), - }; -} export async function GET( request: NextRequest, @@ -36,42 +10,33 @@ export async function GET( ) { const searchParams = request.nextUrl.searchParams; + let value: any; + try { - const { spaceId, userId, dataPath } = await params; - const userData = await getUserConfig(userId, spaceId); + let { spaceId, userId, dataPath } = await params; - if (isUserErrorResponse(userData)) { - return new NextResponse('Cannot get user data', { status: 400 }); - } + if (dataPath.startsWith('user-info')) { + const info = await getUserById(userId); + if (info.isGuest) return new NextResponse(null, { status: 404 }); + + const userInfo = { ...info, name: `${info.firstName} ${info.lastName}` }; + + const [varName] = dataPath.split('.').slice(1); - const meta = await getUserById(userId); - if (meta.isGuest) return new NextResponse(null, { status: 404 }); - - const userInfo = toDummyParameter( - 'user-info', - 'empty', - Object.entries({ - ...meta, - id: undefined, - isGuest: undefined, - favourites: undefined, - name: `${meta.firstName} ${meta.lastName}`, - }) - .filter(([_, value]) => !!value) - .map(([key, value]) => ({ - key, - value, - })), - ); - - const parameter = getParameterFromPath( - [...userData.content[0].subParameters, userInfo], - dataPath, - ); - - if (!parameter) return new NextResponse(null, { status: 404 }); - - let value = searchParams.has('full', 'true') ? filterParameter(parameter) : parameter.value; + if (!(varName in userInfo)) return new NextResponse(null, { status: 404 }); + + value = userInfo[varName as keyof typeof userInfo]; + } else { + const parameter = await getNestedUserParameter(userId, spaceId, dataPath); + + if (isUserErrorResponse(parameter)) { + return new NextResponse('Cannot get user data', { status: 400 }); + } + + if (!parameter) return new NextResponse(null, { status: 404 }); + + value = searchParams.has('full', 'true') ? filterParameter(parameter) : parameter.value; + } return NextResponse.json(value, { status: 200, @@ -92,13 +57,12 @@ export async function PUT( return new NextResponse('Expected an object with a value ({ value: ... })', { status: 400 }); } - const userData = await getUserConfig(userId, spaceId); + const parameter = await getNestedUserParameter(userId, spaceId, dataPath); - if (isUserErrorResponse(userData)) { + if (isUserErrorResponse(parameter)) { return new NextResponse('Cannot get user data', { status: 400 }); } - const parameter = getParameterFromPath(userData.content[0].subParameters, dataPath); if (!parameter) return new NextResponse(null, { status: 404 }); try { diff --git a/src/management-system-v2/app/api/spaces/[spaceId]/data/util.ts b/src/management-system-v2/app/api/spaces/[spaceId]/data/util.ts index 1a11202c5..f303cde6e 100644 --- a/src/management-system-v2/app/api/spaces/[spaceId]/data/util.ts +++ b/src/management-system-v2/app/api/spaces/[spaceId]/data/util.ts @@ -17,16 +17,3 @@ export function filterParameter(parameter: Parameter): NestedFilteredParameter { subParameters: parameter.subParameters.map(filterParameter), }; } - -export function getParameterFromPath(data: (Parameter | VirtualParameter)[], dataPath: string) { - const segments = dataPath.split('.'); - - let parameter: Parameter | undefined = undefined; - for (const segment of segments) { - parameter = data.find((entry) => entry.name === segment); - if (!parameter) return; - data = parameter.subParameters; - } - - return parameter; -} diff --git a/src/management-system-v2/lib/data/db/machine-config.ts b/src/management-system-v2/lib/data/db/machine-config.ts index 6427fa4c6..6c3bbb394 100644 --- a/src/management-system-v2/lib/data/db/machine-config.ts +++ b/src/management-system-v2/lib/data/db/machine-config.ts @@ -22,7 +22,7 @@ import { } from '../machine-config-schema'; import { getFolderById, getRootFolder } from './folders'; import db from '.'; -import { UserError, userError } from '@/lib/user-error'; +import { UserError, isUserErrorResponse, userError } from '@/lib/user-error'; import { getCurrentUser } from '@/components/auth'; import Ability, { UnauthorizedError } from '@/lib/ability/abilityHelper'; import { asyncFilter, asyncForEach, asyncMap } from '@/lib/helpers/javascriptHelpers'; @@ -4554,6 +4554,40 @@ export async function getUserConfig( } } +function getParameterFromPath(data: (Parameter | VirtualParameter)[], dataPath: string) { + const segments = dataPath.split('.'); + + let parameter: Parameter | undefined = undefined; + for (const segment of segments) { + parameter = data.find((entry) => entry.name === segment); + if (!parameter) return; + data = parameter.subParameters; + } + + return parameter; +} + +export async function getNestedUserParameter( + userId: string, + spaceId: string, + parameterPath: string, +) { + const config = await getUserConfig(userId, spaceId); + + if (isUserErrorResponse(config)) return config; + + return getParameterFromPath(config.content[0].subParameters, parameterPath); +} + +export async function getNestedOrgParameter(spaceId: string, parameterPath: string) { + const conf = await getDeepConfigurationById(spaceId); + + let org = conf.content.find((entry) => entry.name === 'organization'); + if (!org) return; + + return getParameterFromPath(org.subParameters, parameterPath); +} + /** * Synchronizes the userParameters stored in the organizational config for the given spaceId. * Creates a new userParameter for any organization member who does not yet have one. Removes diff --git a/src/management-system-v2/lib/engines/deployment.ts b/src/management-system-v2/lib/engines/deployment.ts index 963fd11a5..822d33f6f 100644 --- a/src/management-system-v2/lib/engines/deployment.ts +++ b/src/management-system-v2/lib/engines/deployment.ts @@ -295,6 +295,9 @@ export type InstanceInfo = { adaptationLog: any[]; processVersion: string; userTasks: any[]; + managementSystemLocation?: string; + processInitiator?: string; + spaceIdOfProcessInitiator?: string; }; export type DeployedProcessInfo = { definitionId: string; diff --git a/src/management-system-v2/lib/engines/server-actions.ts b/src/management-system-v2/lib/engines/server-actions.ts index df8e479e1..b8f6f997c 100644 --- a/src/management-system-v2/lib/engines/server-actions.ts +++ b/src/management-system-v2/lib/engines/server-actions.ts @@ -1,6 +1,6 @@ 'use server'; -import { UserFacingError, getErrorMessage, userError } from '../user-error'; +import { UserFacingError, getErrorMessage, isUserErrorResponse, userError } from '../user-error'; import { DeployedProcessInfo, deployProcess as _deployProcess, @@ -33,6 +33,7 @@ import { getCorrectVariableState, getCorrectMilestoneState, inlineScript, + getGlobalVariables, } from '@proceed/user-task-helper'; import { ExtendedTaskListEntry, UserTask } from '../user-task-schema'; @@ -49,6 +50,7 @@ import { Variable } from '@proceed/bpmn-helper/src/getters'; import { getUsersInSpace } from '../data/db/iam/memberships'; import Ability from '../ability/abilityHelper'; import { getUserById } from '../data/db/iam/users'; +import { getNestedUserParameter } from '../data/db/machine-config'; export async function getCorrectTargetEngines( spaceId: string, @@ -299,10 +301,50 @@ export async function getTasklistEntryHTML( initialVariables = getCorrectVariableState(userTask, instance); milestones = await getCorrectMilestoneState(version.bpmn, userTask, instance); - variableChanges = initialVariables; html = await getUserTaskFileFromMachine(engine, definitionId, filename); + const globalVars = await getGlobalVariables(html, async (varPath) => { + let segments = varPath.split('.'); + if (!instance.processInitiator || !instance.spaceIdOfProcessInitiator) { + console.error( + 'Trying to get global data for a user task but the instance is missing initiator information.', + ); + return; + } + if (segments[0] === '@process-initiator') { + if (segments[1] === 'user-info') { + const info = await getUserById(instance.processInitiator); + if (!info || info.isGuest) return; + + const userInfo = { ...info, name: `${info.firstName} ${info.lastName}` }; + + return userInfo[segments[2] as keyof typeof userInfo]; + } else { + const parameter = await getNestedUserParameter( + instance.processInitiator, + instance.spaceIdOfProcessInitiator, + segments.slice(1).join('.'), + ); + + if (isUserErrorResponse(parameter)) { + console.error(parameter.error.message); + return; + } + if (!parameter) { + console.error('Could not get user data for a user task'); + return; + } + + return parameter.value; + } + } + }); + + initialVariables = { ...initialVariables, ...globalVars }; + + variableChanges = initialVariables; + html = html.replace(/\/resources\/process[^"]*/g, (match) => { const path = match.split('/'); return `/api/private/${spaceId}/engine/resources/process/${definitionId}/images/${path.pop()}`; From 0473424adf1717d376ee2587fa8ffeb36bd36fd2 Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Wed, 4 Mar 2026 13:44:08 +0100 Subject: [PATCH 02/11] Added support for @organization and @worker in user task data access --- .../tasklist/TaskList-DisplayItem.js | 2 +- .../lib/engines/server-actions.ts | 24 +++++++++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/engine/universal/ui/src/display-items/tasklist/TaskList-DisplayItem.js b/src/engine/universal/ui/src/display-items/tasklist/TaskList-DisplayItem.js index 28bed065b..eac6a13a4 100644 --- a/src/engine/universal/ui/src/display-items/tasklist/TaskList-DisplayItem.js +++ b/src/engine/universal/ui/src/display-items/tasklist/TaskList-DisplayItem.js @@ -175,7 +175,7 @@ class TaskListTab extends DisplayItem { path += '/organization'; varPath = varPath.split('.').slice(1).join('.'); } else { - throw new Error(`Unable to get data for global variable (@global.${varPath}).`); + return; } path += `/${varPath}`; diff --git a/src/management-system-v2/lib/engines/server-actions.ts b/src/management-system-v2/lib/engines/server-actions.ts index b8f6f997c..c11e10939 100644 --- a/src/management-system-v2/lib/engines/server-actions.ts +++ b/src/management-system-v2/lib/engines/server-actions.ts @@ -50,7 +50,7 @@ import { Variable } from '@proceed/bpmn-helper/src/getters'; import { getUsersInSpace } from '../data/db/iam/memberships'; import Ability from '../ability/abilityHelper'; import { getUserById } from '../data/db/iam/users'; -import { getNestedUserParameter } from '../data/db/machine-config'; +import { getNestedOrgParameter, getNestedUserParameter } from '../data/db/machine-config'; export async function getCorrectTargetEngines( spaceId: string, @@ -312,19 +312,29 @@ export async function getTasklistEntryHTML( ); return; } - if (segments[0] === '@process-initiator') { - if (segments[1] === 'user-info') { - const info = await getUserById(instance.processInitiator); + if (segments[0] === '@organization') { + const parameter = await getNestedOrgParameter(spaceId, segments.slice(1).join('.')); + return parameter?.value; + } else { + let { userId } = await getCurrentUser(); + if (segments[0] === '@process-initiator' || segments[0] === '@worker') { + if (segments[0] === '@process-initiator') { + userId = instance.processInitiator; + } + segments = segments.slice(1); + } + if (segments[0] === 'user-info') { + const info = await getUserById(userId); if (!info || info.isGuest) return; const userInfo = { ...info, name: `${info.firstName} ${info.lastName}` }; - return userInfo[segments[2] as keyof typeof userInfo]; + return userInfo[segments[1] as keyof typeof userInfo]; } else { const parameter = await getNestedUserParameter( - instance.processInitiator, + userId, instance.spaceIdOfProcessInitiator, - segments.slice(1).join('.'), + segments.join('.'), ); if (isUserErrorResponse(parameter)) { From 34f514a4a4ebaca04acc3a69b1410aef173e538f Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Wed, 4 Mar 2026 16:15:12 +0100 Subject: [PATCH 03/11] Added a new table for instance information; Global data is fetched every time a user task is opened to ensure that the the data of the current worker is always correctly inserted --- .../executions/deployment-hook.ts | 29 ++++- .../[environmentId]/tasks/form-list.tsx | 2 +- .../lib/data/db/instances.ts | 52 +++++++++ .../lib/data/instances.ts | 63 ++++++++++ .../lib/engines/server-actions.ts | 109 +++++++++--------- .../lib/user-task-schema.tsx | 2 +- .../migration.sql | 21 ++++ src/management-system-v2/prisma/schema.prisma | 25 +++- 8 files changed, 238 insertions(+), 65 deletions(-) create mode 100644 src/management-system-v2/lib/data/db/instances.ts create mode 100644 src/management-system-v2/lib/data/instances.ts create mode 100644 src/management-system-v2/prisma/migrations/20260304144124_instance_information/migration.sql diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployment-hook.ts b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployment-hook.ts index 0465c3446..0996bcf84 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployment-hook.ts +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployment-hook.ts @@ -1,4 +1,5 @@ import { useEnvironment, useSession } from '@/components/auth-can'; +import { addInstance } from '@/lib/data/instances'; import { DeployedProcessInfo, InstanceInfo, @@ -15,7 +16,7 @@ import { Engine } from '@/lib/engines/machines'; import { getStartFormFromMachine } from '@/lib/engines/tasklist'; import useEngines from '@/lib/engines/use-engines'; import { asyncFilter, asyncForEach, deepEquals } from '@/lib/helpers/javascriptHelpers'; -import { getErrorMessage, userError } from '@/lib/user-error'; +import { getErrorMessage, isUserErrorResponse, userError } from '@/lib/user-error'; import { useQuery } from '@tanstack/react-query'; import { useCallback } from 'react'; @@ -100,13 +101,31 @@ function useDeployment(definitionId: string, initialData?: DeployedProcessInfo) }); const startInstance = async (versionId: string, variables: { [key: string]: any } = {}) => { - if (engines?.length) + if (engines?.length) { // TODO: in case of static deployment or different versions on different engines we will have // to check if the engine can actually be used to start an instance - return await startInstanceOnMachine(definitionId, versionId, engines[0], variables, { - processInitiator: session?.user.id, - spaceIdOfProcessInitiator: space.spaceId, + const instanceId = await startInstanceOnMachine( + definitionId, + versionId, + engines[0], + variables, + { + processInitiator: session?.user.id, + spaceIdOfProcessInitiator: space.spaceId, + }, + ); + + if (isUserErrorResponse(instanceId)) return instanceId; + + await addInstance({ + id: instanceId, + definitionId, + initiatorId: session?.user.id || '', + initiatorSpaceId: space.spaceId, }); + + return instanceId; + } }; const activeStates = ['PAUSED', 'RUNNING', 'READY', 'DEPLOYMENT-WAITING', 'WAITING']; diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/tasks/form-list.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/tasks/form-list.tsx index 72be29b90..d62109cff 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/tasks/form-list.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/tasks/form-list.tsx @@ -195,7 +195,7 @@ const FormList: React.FC = ({ data }) => { id: v4(), name: task.name, taskId: '', - instanceID: '', + instanceID: undefined, fileName: '', state: 'READY', machineId: 'ms-local', diff --git a/src/management-system-v2/lib/data/db/instances.ts b/src/management-system-v2/lib/data/db/instances.ts new file mode 100644 index 000000000..902e5df9b --- /dev/null +++ b/src/management-system-v2/lib/data/db/instances.ts @@ -0,0 +1,52 @@ +import db from '@/lib/data/db'; +import { z } from 'zod'; + +export async function getInstances(spaceId: string) { + const instances = await db.processInstance.findMany({ + where: { initiatorSpaceId: spaceId }, + }); + + return instances; +} + +export async function getInstanceById(instanceId: string) { + const instance = await db.processInstance.findUnique({ + where: { + id: instanceId, + }, + }); + + return instance; +} + +const InstanceSchema = z.object({ + id: z.string(), + definitionId: z.string(), + initiatorId: z.string(), + initiatorSpaceId: z.string(), +}); +export type Instance = z.infer; + +export async function addInstance(instanceInput: Instance) { + const newInstance = InstanceSchema.parse(instanceInput); + + return await db.processInstance.createMany({ + data: [newInstance], + }); +} + +export async function deleteInstance(instanceId: string) { + return await db.processInstance.delete({ + where: { + id: instanceId, + }, + }); +} + +export async function deleteInstances(definitionId: string) { + return await db.processInstance.deleteMany({ + where: { + definitionId, + }, + }); +} diff --git a/src/management-system-v2/lib/data/instances.ts b/src/management-system-v2/lib/data/instances.ts new file mode 100644 index 000000000..e8b75b165 --- /dev/null +++ b/src/management-system-v2/lib/data/instances.ts @@ -0,0 +1,63 @@ +'use server'; + +import { z } from 'zod'; +import { enableUseDB } from 'FeatureFlags'; +import { + getInstances as _getInstances, + getInstanceById as _getInstanceById, + addInstance as _addInstance, + deleteInstances as _deleteInstances, + Instance, +} from './db/instances'; +import { UnauthorizedError } from '../ability/abilityHelper'; +import { UserErrorType, userError } from '../user-error'; + +export async function getInstances(spaceId: string) { + if (!enableUseDB) throw new Error('Not implemented for enableUseDB=false'); + + try { + return await _getInstances(spaceId); + } catch (err) { + if (err instanceof UnauthorizedError) + return userError('Permission denied', UserErrorType.PermissionError); + else return userError('Error getting instances'); + } +} + +export async function getInstanceById(instanceId: string) { + if (!enableUseDB) throw new Error('Not implemented for enableUseDB=false'); + + try { + return await _getInstanceById(instanceId); + } catch (err) { + if (err instanceof UnauthorizedError) + return userError('Permission denied', UserErrorType.PermissionError); + else return userError(`Error getting instance ${instanceId}`); + } +} + +export async function addInstance(instance: Instance) { + if (!enableUseDB) throw new Error('Not implemented for enableUseDB=false'); + + try { + return await _addInstance(instance); + } catch (e) { + if (e instanceof UnauthorizedError) + return userError('Permission denied', UserErrorType.PermissionError); + else if (e instanceof z.ZodError) + return userError('Schema validation failed', UserErrorType.SchemaValidationError); + else return userError('Error adding an instance'); + } +} + +export async function deleteInstances(definitionId: string) { + if (!enableUseDB) throw new Error('Not implemented for enableUseDB=false'); + + try { + return await _deleteInstances(definitionId); + } catch (e) { + if (e instanceof UnauthorizedError) + return userError('Permission denied', UserErrorType.PermissionError); + else return userError('Error deleting instances'); + } +} diff --git a/src/management-system-v2/lib/engines/server-actions.ts b/src/management-system-v2/lib/engines/server-actions.ts index c11e10939..72496438e 100644 --- a/src/management-system-v2/lib/engines/server-actions.ts +++ b/src/management-system-v2/lib/engines/server-actions.ts @@ -2,7 +2,6 @@ import { UserFacingError, getErrorMessage, isUserErrorResponse, userError } from '../user-error'; import { - DeployedProcessInfo, deployProcess as _deployProcess, getDeployments as fetchDeployments, getDeployment as fetchDeployment, @@ -51,6 +50,7 @@ import { getUsersInSpace } from '../data/db/iam/memberships'; import Ability from '../ability/abilityHelper'; import { getUserById } from '../data/db/iam/users'; import { getNestedOrgParameter, getNestedUserParameter } from '../data/db/machine-config'; +import { deleteInstances, getInstanceById } from '../data/instances'; export async function getCorrectTargetEngines( spaceId: string, @@ -126,6 +126,8 @@ export async function removeDeployment(definitionId: string, spaceId: string) { }); await removeDeploymentFromMachines(engines, definitionId); + + await deleteInstances(definitionId); } catch (e) { const message = getErrorMessage(e); return userError(message); @@ -229,9 +231,11 @@ export async function getAvailableTaskListEntries(spaceId: string, engines: Engi username?: string | null; firstName?: string | null; lastName?: string | null; - }[] = await asyncMap(task.actualOwner, async (owner) => { - return getUserById(owner, undefined, tx) || owner; - }); + }[] = ( + await asyncMap(task.actualOwner, async (owner) => { + return getUserById(owner, undefined, tx) || owner; + }) + ).filter(truthyFilter); return users.map((user) => typeof user === 'string' @@ -278,6 +282,7 @@ export async function getTasklistEntryHTML( if (engine && (!html || !milestones || !initialVariables)) { const [taskId, instanceId, startTimeString] = userTaskId.split('|'); + const [definitionId] = instanceId.split('-_'); const startTime = parseInt(startTimeString); @@ -304,55 +309,6 @@ export async function getTasklistEntryHTML( html = await getUserTaskFileFromMachine(engine, definitionId, filename); - const globalVars = await getGlobalVariables(html, async (varPath) => { - let segments = varPath.split('.'); - if (!instance.processInitiator || !instance.spaceIdOfProcessInitiator) { - console.error( - 'Trying to get global data for a user task but the instance is missing initiator information.', - ); - return; - } - if (segments[0] === '@organization') { - const parameter = await getNestedOrgParameter(spaceId, segments.slice(1).join('.')); - return parameter?.value; - } else { - let { userId } = await getCurrentUser(); - if (segments[0] === '@process-initiator' || segments[0] === '@worker') { - if (segments[0] === '@process-initiator') { - userId = instance.processInitiator; - } - segments = segments.slice(1); - } - if (segments[0] === 'user-info') { - const info = await getUserById(userId); - if (!info || info.isGuest) return; - - const userInfo = { ...info, name: `${info.firstName} ${info.lastName}` }; - - return userInfo[segments[1] as keyof typeof userInfo]; - } else { - const parameter = await getNestedUserParameter( - userId, - instance.spaceIdOfProcessInitiator, - segments.join('.'), - ); - - if (isUserErrorResponse(parameter)) { - console.error(parameter.error.message); - return; - } - if (!parameter) { - console.error('Could not get user data for a user task'); - return; - } - - return parameter.value; - } - } - }); - - initialVariables = { ...initialVariables, ...globalVars }; - variableChanges = initialVariables; html = html.replace(/\/resources\/process[^"]*/g, (match) => { @@ -410,6 +366,53 @@ export async function getTasklistEntryHTML( if (!html) throw new Error('Failed to get the html for the user task'); if (!milestones) throw new Error('Failed to get the milestones for the user task'); + const globalVars = await getGlobalVariables(html, async (varPath) => { + let segments = varPath.split('.'); + + if (segments[0] === '@organization') { + const parameter = await getNestedOrgParameter(spaceId, segments.slice(1).join('.')); + return parameter?.value; + } else { + let { userId } = await getCurrentUser(); + if (segments[0] === '@process-initiator' || segments[0] === '@worker') { + if (segments[0] === '@process-initiator') { + if (!storedUserTask.instanceID) { + throw new Error('Trying to get the instance initiator but not in an instance'); + } + const instance = await getInstanceById(storedUserTask.instanceID); + if (!instance) throw new Error('Unknown instance'); + if (isUserErrorResponse(instance)) throw instance; + + userId = instance.initiatorId; + } + segments = segments.slice(1); + } + if (segments[0] === 'user-info') { + const info = await getUserById(userId); + if (!info || info.isGuest) return; + + const userInfo = { ...info, name: `${info.firstName} ${info.lastName}` }; + + return userInfo[segments[1] as keyof typeof userInfo]; + } else { + const parameter = await getNestedUserParameter(userId, spaceId, segments.join('.')); + + if (isUserErrorResponse(parameter)) { + console.error(parameter.error.message); + return; + } + if (!parameter) { + console.error('Could not get user data for a user task'); + return; + } + + return parameter.value; + } + } + }); + + variableChanges = { ...variableChanges, ...globalVars }; + return inlineUserTaskData(html, mapResourceUrls(variableChanges), milestones); } catch (e) { const message = getErrorMessage(e); diff --git a/src/management-system-v2/lib/user-task-schema.tsx b/src/management-system-v2/lib/user-task-schema.tsx index 0a403d19a..4f3e5628f 100644 --- a/src/management-system-v2/lib/user-task-schema.tsx +++ b/src/management-system-v2/lib/user-task-schema.tsx @@ -4,7 +4,7 @@ export const UserTaskInputSchema = z.object({ id: z.string(), taskId: z.string(), name: z.string().nullish(), - instanceID: z.string(), + instanceID: z.string().optional(), fileName: z.string(), html: z.string().nullish(), state: z.string(), diff --git a/src/management-system-v2/prisma/migrations/20260304144124_instance_information/migration.sql b/src/management-system-v2/prisma/migrations/20260304144124_instance_information/migration.sql new file mode 100644 index 000000000..253e90854 --- /dev/null +++ b/src/management-system-v2/prisma/migrations/20260304144124_instance_information/migration.sql @@ -0,0 +1,21 @@ +-- AlterTable +ALTER TABLE "userTask" ALTER COLUMN "instanceID" DROP NOT NULL; + +-- CreateTable +CREATE TABLE "process-instance" ( + "id" TEXT NOT NULL, + "definitionId" TEXT NOT NULL, + "initiatorId" TEXT NOT NULL, + "initiatorSpaceId" TEXT NOT NULL, + + CONSTRAINT "process-instance_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "process-instance" ADD CONSTRAINT "process-instance_initiatorId_fkey" FOREIGN KEY ("initiatorId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "process-instance" ADD CONSTRAINT "process-instance_initiatorSpaceId_fkey" FOREIGN KEY ("initiatorSpaceId") REFERENCES "space"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "userTask" ADD CONSTRAINT "userTask_instanceID_fkey" FOREIGN KEY ("instanceID") REFERENCES "process-instance"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/management-system-v2/prisma/schema.prisma b/src/management-system-v2/prisma/schema.prisma index 15dacb09b..89056917f 100644 --- a/src/management-system-v2/prisma/schema.prisma +++ b/src/management-system-v2/prisma/schema.prisma @@ -49,6 +49,7 @@ model User { emailVerificationTokens EmailVerificationToken[] @relation("verificationToken") mcpPairingCodes McpPairingCode[] @relation("pairingCode") Config Config[] + startedInstances ProcessInstance[] @relation("instanceInitiator") @@map("user") } @@ -144,7 +145,7 @@ model ArtifactVersionReference { } model Space { - id String @id @default(uuid()) + id String @id @default(uuid()) name String? isOrganization Boolean isActive Boolean? @@ -152,7 +153,7 @@ model Space { contactPhoneNumber String? contactEmail String? spaceLogo String? - owner User? @relation("spaceOwner", fields: [ownerId], references: [id], onDelete: Cascade) + owner User? @relation("spaceOwner", fields: [ownerId], references: [id], onDelete: Cascade) ownerId String? members Membership[] folders Folder[] @@ -160,9 +161,10 @@ model Space { htmlForms HtmlForm[] roles Role[] engines Engine[] - settings SpaceSettings? @relation("spaceSettings") + settings SpaceSettings? @relation("spaceSettings") Config Config[] mcpPairingCodes McpPairingCode[] + instances ProcessInstance[] @relation("instanceInitiatorSpace") @@map("space") } @@ -318,11 +320,24 @@ model HtmlForm { @@map("html-form") } +model ProcessInstance { + id String @id + definitionId String + initiator User @relation("instanceInitiator", fields: [initiatorId], references: [id], onDelete: Cascade) + initiatorId String + initiatorSpace Space @relation("instanceInitiatorSpace", fields: [initiatorSpaceId], references: [id], onDelete: Cascade) + initiatorSpaceId String + userTasks UserTask[] @relation("instance") + + @@map("process-instance") +} + model UserTask { - id String @id + id String @id taskId String name String? - instanceID String + instance ProcessInstance? @relation("instance", fields: [instanceID], references: [id], onDelete: Cascade) + instanceID String? fileName String html String? state String From 0e7807cecacf4b6b304495fc4811e5121331e08e Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Mon, 9 Mar 2026 11:32:11 +0100 Subject: [PATCH 04/11] Inserting global variables into start form --- .../[processId]/process-deployment-view.tsx | 22 ++++- .../lib/engines/server-actions.ts | 99 +++++++++++-------- 2 files changed, 77 insertions(+), 44 deletions(-) diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/process-deployment-view.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/process-deployment-view.tsx index 86fea36dd..18ed6f770 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/process-deployment-view.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/process-deployment-view.tsx @@ -33,6 +33,10 @@ import { DeployedProcessInfo } from '@/lib/engines/deployment'; import StartFormModal from './start-form-modal'; import useInstanceVariables from './use-instance-variables'; import { inlineScript, inlineUserTaskData } from '@proceed/user-task-helper'; +import { getGlobalVariablesForHTML } from '@/lib/engines/server-actions'; +import { useEnvironment } from '@/components/auth-can'; +import { useSession } from 'next-auth/react'; +import { isUserErrorResponse } from '@/lib/user-error'; export default function ProcessDeploymentView({ processId, @@ -56,6 +60,9 @@ export default function ProcessDeploymentView({ const canvasRef = useRef(null); const [infoPanelOpen, setInfoPanelOpen] = useState(false); + const { spaceId } = useEnvironment(); + const { data: session } = useSession(); + const { data: deploymentInfo, refetch, @@ -199,8 +206,21 @@ export default function ProcessDeploymentView({ .filter((variable) => variable.value !== undefined) .map((variable) => [variable.name, variable.value]), ); + + if (!session) throw new Error('Unknown user tries to start an instance!'); + + const globalVars = await getGlobalVariablesForHTML( + spaceId, + session.user.id, + startForm, + ); + startForm = inlineScript(startForm, '', '', variableDefinitions); - startForm = inlineUserTaskData(startForm, mappedVariables, []); + startForm = inlineUserTaskData( + startForm, + { ...mappedVariables, ...globalVars }, + [], + ); setStartForm(startForm); } else { diff --git a/src/management-system-v2/lib/engines/server-actions.ts b/src/management-system-v2/lib/engines/server-actions.ts index 72496438e..52c018074 100644 --- a/src/management-system-v2/lib/engines/server-actions.ts +++ b/src/management-system-v2/lib/engines/server-actions.ts @@ -1,6 +1,12 @@ 'use server'; -import { UserFacingError, getErrorMessage, isUserErrorResponse, userError } from '../user-error'; +import { + UserErrorType, + UserFacingError, + getErrorMessage, + isUserErrorResponse, + userError, +} from '../user-error'; import { deployProcess as _deployProcess, getDeployments as fetchDeployments, @@ -255,6 +261,48 @@ export async function getAvailableTaskListEntries(spaceId: string, engines: Engi } } +export async function getGlobalVariablesForHTML( + spaceId: string, + initiatorId: string, + html: string, +) { + return await getGlobalVariables(html, async (varPath) => { + let segments = varPath.split('.'); + + if (segments[0] === '@organization') { + const parameter = await getNestedOrgParameter(spaceId, segments.slice(1).join('.')); + return parameter?.value; + } else { + let { userId } = await getCurrentUser(); + if (segments[0] === '@process-initiator' || segments[0] === '@worker') { + if (segments[0] === '@process-initiator') userId = initiatorId; + segments = segments.slice(1); + } + if (segments[0] === 'user-info') { + const info = await getUserById(userId); + if (!info || info.isGuest) return; + + const userInfo = { ...info, name: `${info.firstName} ${info.lastName}` }; + + return userInfo[segments[1] as keyof typeof userInfo]; + } else { + const parameter = await getNestedUserParameter(userId, spaceId, segments.join('.')); + + if (isUserErrorResponse(parameter)) { + console.error(parameter.error.message); + return; + } + if (!parameter) { + console.error('Could not get user data for a user task'); + return; + } + + return parameter.value; + } + } + }); +} + export async function getTasklistEntryHTML( spaceId: string, userTaskId: string, @@ -366,50 +414,15 @@ export async function getTasklistEntryHTML( if (!html) throw new Error('Failed to get the html for the user task'); if (!milestones) throw new Error('Failed to get the milestones for the user task'); - const globalVars = await getGlobalVariables(html, async (varPath) => { - let segments = varPath.split('.'); - - if (segments[0] === '@organization') { - const parameter = await getNestedOrgParameter(spaceId, segments.slice(1).join('.')); - return parameter?.value; - } else { - let { userId } = await getCurrentUser(); - if (segments[0] === '@process-initiator' || segments[0] === '@worker') { - if (segments[0] === '@process-initiator') { - if (!storedUserTask.instanceID) { - throw new Error('Trying to get the instance initiator but not in an instance'); - } - const instance = await getInstanceById(storedUserTask.instanceID); - if (!instance) throw new Error('Unknown instance'); - if (isUserErrorResponse(instance)) throw instance; - - userId = instance.initiatorId; - } - segments = segments.slice(1); - } - if (segments[0] === 'user-info') { - const info = await getUserById(userId); - if (!info || info.isGuest) return; - - const userInfo = { ...info, name: `${info.firstName} ${info.lastName}` }; + let globalVars: Record = {}; - return userInfo[segments[1] as keyof typeof userInfo]; - } else { - const parameter = await getNestedUserParameter(userId, spaceId, segments.join('.')); + if (storedUserTask.instanceID) { + const instance = await getInstanceById(storedUserTask.instanceID); + if (!instance) throw new Error('Unknown instance'); + if (isUserErrorResponse(instance)) throw instance; - if (isUserErrorResponse(parameter)) { - console.error(parameter.error.message); - return; - } - if (!parameter) { - console.error('Could not get user data for a user task'); - return; - } - - return parameter.value; - } - } - }); + globalVars = await getGlobalVariablesForHTML(spaceId, instance.initiatorId, html); + } variableChanges = { ...variableChanges, ...globalVars }; From 27dd595ef299030b44464b5aa57623e7817034df Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Tue, 24 Mar 2026 16:56:42 +0100 Subject: [PATCH 05/11] Removed unused functions --- .../lib/data/db/machine-config.ts | 34 ------------------- 1 file changed, 34 deletions(-) diff --git a/src/management-system-v2/lib/data/db/machine-config.ts b/src/management-system-v2/lib/data/db/machine-config.ts index 6a0c2de15..3bce9f685 100644 --- a/src/management-system-v2/lib/data/db/machine-config.ts +++ b/src/management-system-v2/lib/data/db/machine-config.ts @@ -4654,40 +4654,6 @@ export async function getUserConfig( } } -function getParameterFromPath(data: (Parameter | VirtualParameter)[], dataPath: string) { - const segments = dataPath.split('.'); - - let parameter: Parameter | undefined = undefined; - for (const segment of segments) { - parameter = data.find((entry) => entry.name === segment); - if (!parameter) return; - data = parameter.subParameters; - } - - return parameter; -} - -export async function getNestedUserParameter( - userId: string, - spaceId: string, - parameterPath: string, -) { - const config = await getUserConfig(userId, spaceId); - - if (isUserErrorResponse(config)) return config; - - return getParameterFromPath(config.content[0].subParameters, parameterPath); -} - -export async function getNestedOrgParameter(spaceId: string, parameterPath: string) { - const conf = await getDeepConfigurationById(spaceId); - - let org = conf.content.find((entry) => entry.name === 'organization'); - if (!org) return; - - return getParameterFromPath(org.subParameters, parameterPath); -} - /** * Retrieves the user-specific data for a given personal space. The returned userdata is * wrapped in a dummy config to allow convenient display in the config editor. From 17d065b90cbef992d701286a3449dfe6170d3151 Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Tue, 24 Mar 2026 17:35:46 +0100 Subject: [PATCH 06/11] Adjusted the html completion logic to work with the new access functions for organization and user data --- .../lib/engines/server-actions.ts | 46 ++++++++----------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/src/management-system-v2/lib/engines/server-actions.ts b/src/management-system-v2/lib/engines/server-actions.ts index cb44b6d02..6780a4406 100644 --- a/src/management-system-v2/lib/engines/server-actions.ts +++ b/src/management-system-v2/lib/engines/server-actions.ts @@ -57,6 +57,7 @@ import Ability from '../ability/abilityHelper'; import { getUserById } from '../data/db/iam/users'; import { getNestedOrgParameter, getNestedUserParameter } from '../data/db/machine-config'; import { deleteInstances, getInstanceById } from '../data/instances'; +import { getDataObject, isErrorResponse } from '@/app/api/spaces/[spaceId]/data/helper'; export async function getCorrectTargetEngines( spaceId: string, @@ -270,37 +271,30 @@ export async function getGlobalVariablesForHTML( return await getGlobalVariables(html, async (varPath) => { let segments = varPath.split('.'); - if (segments[0] === '@organization') { - const parameter = await getNestedOrgParameter(spaceId, segments.slice(1).join('.')); - return parameter?.value; - } else { - let { userId } = await getCurrentUser(); - if (segments[0] === '@process-initiator' || segments[0] === '@worker') { - if (segments[0] === '@process-initiator') userId = initiatorId; - segments = segments.slice(1); - } - if (segments[0] === 'user-info') { - const info = await getUserById(userId); - if (!info || info.isGuest) return; + let userId: string | undefined; - const userInfo = { ...info, name: `${info.firstName} ${info.lastName}` }; + if (segments[0] === '@process-initiator') { + userId = initiatorId; + } else if (segments[0] === '@worker' || !segments[0].startsWith('@')) { + ({ userId } = await getCurrentUser()); + } else if (segments[0] !== '@organization') { + console.error(`Invalid selector for global data access in user task html. (${segments[0]})`); + return; + } - return userInfo[segments[1] as keyof typeof userInfo]; - } else { - const parameter = await getNestedUserParameter(userId, spaceId, segments.join('.')); + if (segments[0].startsWith('@')) segments = segments.slice(1); - if (isUserErrorResponse(parameter)) { - console.error(parameter.error.message); - return; - } - if (!parameter) { - console.error('Could not get user data for a user task'); - return; - } + const result = await getDataObject(spaceId, segments.join('.'), userId); - return parameter.value; - } + if (isErrorResponse(result)) { + console.error( + 'Ecountered error while trying to get global variable for user task rendering:', + await result.data.text(), + ); + return; } + + return result.data.value; }); } From de1ec640f5af156a56ebaa3a76d0be7568f4650e Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Tue, 31 Mar 2026 10:16:09 +0200 Subject: [PATCH 07/11] Removed instance storing code that will be added in a different branch --- .../executions/deployment-hook.ts | 26 ++------ .../[environmentId]/tasks/form-list.tsx | 2 +- .../lib/data/db/instances.ts | 52 --------------- .../lib/data/db/machine-config.ts | 16 ++--- .../lib/data/instances.ts | 63 ------------------- .../lib/engines/server-actions.ts | 24 +++---- .../lib/user-task-schema.tsx | 2 +- .../migration.sql | 21 ------- src/management-system-v2/prisma/schema.prisma | 25 ++------ 9 files changed, 28 insertions(+), 203 deletions(-) delete mode 100644 src/management-system-v2/lib/data/db/instances.ts delete mode 100644 src/management-system-v2/lib/data/instances.ts delete mode 100644 src/management-system-v2/prisma/migrations/20260304144124_instance_information/migration.sql diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployment-hook.ts b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployment-hook.ts index 0996bcf84..d4099db1c 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployment-hook.ts +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployment-hook.ts @@ -1,5 +1,4 @@ import { useEnvironment, useSession } from '@/components/auth-can'; -import { addInstance } from '@/lib/data/instances'; import { DeployedProcessInfo, InstanceInfo, @@ -16,7 +15,7 @@ import { Engine } from '@/lib/engines/machines'; import { getStartFormFromMachine } from '@/lib/engines/tasklist'; import useEngines from '@/lib/engines/use-engines'; import { asyncFilter, asyncForEach, deepEquals } from '@/lib/helpers/javascriptHelpers'; -import { getErrorMessage, isUserErrorResponse, userError } from '@/lib/user-error'; +import { getErrorMessage, userError } from '@/lib/user-error'; import { useQuery } from '@tanstack/react-query'; import { useCallback } from 'react'; @@ -104,27 +103,10 @@ function useDeployment(definitionId: string, initialData?: DeployedProcessInfo) if (engines?.length) { // TODO: in case of static deployment or different versions on different engines we will have // to check if the engine can actually be used to start an instance - const instanceId = await startInstanceOnMachine( - definitionId, - versionId, - engines[0], - variables, - { - processInitiator: session?.user.id, - spaceIdOfProcessInitiator: space.spaceId, - }, - ); - - if (isUserErrorResponse(instanceId)) return instanceId; - - await addInstance({ - id: instanceId, - definitionId, - initiatorId: session?.user.id || '', - initiatorSpaceId: space.spaceId, + return await startInstanceOnMachine(definitionId, versionId, engines[0], variables, { + processInitiator: session?.user.id, + spaceIdOfProcessInitiator: space.spaceId, }); - - return instanceId; } }; diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/tasks/form-list.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/tasks/form-list.tsx index de76f6ee9..29dd6693a 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/tasks/form-list.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/tasks/form-list.tsx @@ -195,7 +195,7 @@ const FormList: React.FC = ({ data }) => { id: v4(), name: task.name, taskId: '', - instanceID: undefined, + instanceID: '', fileName: '', state: 'READY', machineId: 'ms-local', diff --git a/src/management-system-v2/lib/data/db/instances.ts b/src/management-system-v2/lib/data/db/instances.ts deleted file mode 100644 index 902e5df9b..000000000 --- a/src/management-system-v2/lib/data/db/instances.ts +++ /dev/null @@ -1,52 +0,0 @@ -import db from '@/lib/data/db'; -import { z } from 'zod'; - -export async function getInstances(spaceId: string) { - const instances = await db.processInstance.findMany({ - where: { initiatorSpaceId: spaceId }, - }); - - return instances; -} - -export async function getInstanceById(instanceId: string) { - const instance = await db.processInstance.findUnique({ - where: { - id: instanceId, - }, - }); - - return instance; -} - -const InstanceSchema = z.object({ - id: z.string(), - definitionId: z.string(), - initiatorId: z.string(), - initiatorSpaceId: z.string(), -}); -export type Instance = z.infer; - -export async function addInstance(instanceInput: Instance) { - const newInstance = InstanceSchema.parse(instanceInput); - - return await db.processInstance.createMany({ - data: [newInstance], - }); -} - -export async function deleteInstance(instanceId: string) { - return await db.processInstance.delete({ - where: { - id: instanceId, - }, - }); -} - -export async function deleteInstances(definitionId: string) { - return await db.processInstance.deleteMany({ - where: { - definitionId, - }, - }); -} diff --git a/src/management-system-v2/lib/data/db/machine-config.ts b/src/management-system-v2/lib/data/db/machine-config.ts index 3bce9f685..81e62c888 100644 --- a/src/management-system-v2/lib/data/db/machine-config.ts +++ b/src/management-system-v2/lib/data/db/machine-config.ts @@ -25,7 +25,7 @@ import { } from '@/lib/data/machine-config-aas-schema'; import { getFolderById, getRootFolder } from './folders'; import db from '.'; -import { UserError, isUserErrorResponse, userError } from '@/lib/user-error'; +import { UserError, userError } from '@/lib/user-error'; import { getCurrentUser } from '@/components/auth'; import Ability, { UnauthorizedError } from '@/lib/ability/abilityHelper'; import { asyncFilter, asyncForEach, asyncMap } from '@/lib/helpers/javascriptHelpers'; @@ -526,7 +526,7 @@ export async function addMachineConfigVersion( previousMachine: Parameter, currentMachine: Parameter, versionNo: Int, -) {} +) { } // TODO rework: versioning /** @@ -2025,8 +2025,8 @@ export async function updateParameter( // make sure to remove backlinks from unlinked parameters let linkIds = parameter.transformation ? Object.values(parameter.transformation.linkedInputParameters).map( - ({ id }: { id: any }) => id, - ) + ({ id }: { id: any }) => id, + ) : []; const removedIds = linkIds.filter( (id) => @@ -2181,8 +2181,8 @@ export async function convertParameterType( inputParam.id === parameterId ? { data: { value: (newParameter as Parameter).value ?? updatedParameter.value } } : await db.configParameter.findUnique({ - where: { id: inputParam.id }, - }); + where: { id: inputParam.id }, + }); // convert to number if possible inputValues[key.substring(1)] = possiblyNumber( @@ -2310,7 +2310,7 @@ export async function removeParameter(parameterId: string) { const parameterPath = findPathToParameter(parameter.id, fullConfig, [], 'config'); if ( parameterPath.slice(0, 2).toString() == - ['identity-and-access-management', 'common-user-data'].toString() && + ['identity-and-access-management', 'common-user-data'].toString() && fullConfig.configType == 'organization' ) { await removeCommonUserDataPropagation(parameterPath.slice(2), fullConfig); @@ -2404,7 +2404,7 @@ export async function removeConfigVersion(configId: string, versionNo: number) { const error = e as Error; throw userError( error.message ?? - `There was an error removing the config versions: ${configId}-${versionNo}`, + `There was an error removing the config versions: ${configId}-${versionNo}`, ); } } else { diff --git a/src/management-system-v2/lib/data/instances.ts b/src/management-system-v2/lib/data/instances.ts deleted file mode 100644 index e8b75b165..000000000 --- a/src/management-system-v2/lib/data/instances.ts +++ /dev/null @@ -1,63 +0,0 @@ -'use server'; - -import { z } from 'zod'; -import { enableUseDB } from 'FeatureFlags'; -import { - getInstances as _getInstances, - getInstanceById as _getInstanceById, - addInstance as _addInstance, - deleteInstances as _deleteInstances, - Instance, -} from './db/instances'; -import { UnauthorizedError } from '../ability/abilityHelper'; -import { UserErrorType, userError } from '../user-error'; - -export async function getInstances(spaceId: string) { - if (!enableUseDB) throw new Error('Not implemented for enableUseDB=false'); - - try { - return await _getInstances(spaceId); - } catch (err) { - if (err instanceof UnauthorizedError) - return userError('Permission denied', UserErrorType.PermissionError); - else return userError('Error getting instances'); - } -} - -export async function getInstanceById(instanceId: string) { - if (!enableUseDB) throw new Error('Not implemented for enableUseDB=false'); - - try { - return await _getInstanceById(instanceId); - } catch (err) { - if (err instanceof UnauthorizedError) - return userError('Permission denied', UserErrorType.PermissionError); - else return userError(`Error getting instance ${instanceId}`); - } -} - -export async function addInstance(instance: Instance) { - if (!enableUseDB) throw new Error('Not implemented for enableUseDB=false'); - - try { - return await _addInstance(instance); - } catch (e) { - if (e instanceof UnauthorizedError) - return userError('Permission denied', UserErrorType.PermissionError); - else if (e instanceof z.ZodError) - return userError('Schema validation failed', UserErrorType.SchemaValidationError); - else return userError('Error adding an instance'); - } -} - -export async function deleteInstances(definitionId: string) { - if (!enableUseDB) throw new Error('Not implemented for enableUseDB=false'); - - try { - return await _deleteInstances(definitionId); - } catch (e) { - if (e instanceof UnauthorizedError) - return userError('Permission denied', UserErrorType.PermissionError); - else return userError('Error deleting instances'); - } -} diff --git a/src/management-system-v2/lib/engines/server-actions.ts b/src/management-system-v2/lib/engines/server-actions.ts index 6780a4406..0237beaf4 100644 --- a/src/management-system-v2/lib/engines/server-actions.ts +++ b/src/management-system-v2/lib/engines/server-actions.ts @@ -1,12 +1,6 @@ 'use server'; -import { - UserErrorType, - UserFacingError, - getErrorMessage, - isUserErrorResponse, - userError, -} from '../user-error'; +import { UserFacingError, getErrorMessage, isUserErrorResponse, userError } from '../user-error'; import { deployProcess as _deployProcess, getDeployments as fetchDeployments, @@ -52,11 +46,8 @@ import { import { getFileFromMachine, submitFileToMachine, updateVariablesOnMachine } from './instances'; import { getProcessIds, getVariablesFromElementById } from '@proceed/bpmn-helper'; import { Variable } from '@proceed/bpmn-helper/src/getters'; -import { getUsersInSpace } from '../data/db/iam/memberships'; import Ability from '../ability/abilityHelper'; import { getUserById } from '../data/db/iam/users'; -import { getNestedOrgParameter, getNestedUserParameter } from '../data/db/machine-config'; -import { deleteInstances, getInstanceById } from '../data/instances'; import { getDataObject, isErrorResponse } from '@/app/api/spaces/[spaceId]/data/helper'; export async function getCorrectTargetEngines( @@ -134,8 +125,6 @@ export async function removeDeployment(definitionId: string, spaceId: string) { }); await removeDeploymentFromMachines(engines, definitionId); - - await deleteInstances(definitionId); } catch (e) { const message = getErrorMessage(e); return userError(message); @@ -412,11 +401,16 @@ export async function getTasklistEntryHTML( let globalVars: Record = {}; if (storedUserTask.instanceID) { - const instance = await getInstanceById(storedUserTask.instanceID); + if (!engine) throw new Error('Cannot retrieve the instance initiator information.'); + const [definitionId] = storedUserTask.instanceID.split('-_'); + const deployment = await fetchDeployment(engine, definitionId); + const instance = deployment.instances.find( + (i) => i.processInstanceId === storedUserTask.instanceID, + ); if (!instance) throw new Error('Unknown instance'); - if (isUserErrorResponse(instance)) throw instance; + if (!instance.processInitiator) throw new Error('Missing initiator information'); - globalVars = await getGlobalVariablesForHTML(spaceId, instance.initiatorId, html); + globalVars = await getGlobalVariablesForHTML(spaceId, instance.processInitiator, html); } variableChanges = { ...variableChanges, ...globalVars }; diff --git a/src/management-system-v2/lib/user-task-schema.tsx b/src/management-system-v2/lib/user-task-schema.tsx index 4f3e5628f..0a403d19a 100644 --- a/src/management-system-v2/lib/user-task-schema.tsx +++ b/src/management-system-v2/lib/user-task-schema.tsx @@ -4,7 +4,7 @@ export const UserTaskInputSchema = z.object({ id: z.string(), taskId: z.string(), name: z.string().nullish(), - instanceID: z.string().optional(), + instanceID: z.string(), fileName: z.string(), html: z.string().nullish(), state: z.string(), diff --git a/src/management-system-v2/prisma/migrations/20260304144124_instance_information/migration.sql b/src/management-system-v2/prisma/migrations/20260304144124_instance_information/migration.sql deleted file mode 100644 index 253e90854..000000000 --- a/src/management-system-v2/prisma/migrations/20260304144124_instance_information/migration.sql +++ /dev/null @@ -1,21 +0,0 @@ --- AlterTable -ALTER TABLE "userTask" ALTER COLUMN "instanceID" DROP NOT NULL; - --- CreateTable -CREATE TABLE "process-instance" ( - "id" TEXT NOT NULL, - "definitionId" TEXT NOT NULL, - "initiatorId" TEXT NOT NULL, - "initiatorSpaceId" TEXT NOT NULL, - - CONSTRAINT "process-instance_pkey" PRIMARY KEY ("id") -); - --- AddForeignKey -ALTER TABLE "process-instance" ADD CONSTRAINT "process-instance_initiatorId_fkey" FOREIGN KEY ("initiatorId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "process-instance" ADD CONSTRAINT "process-instance_initiatorSpaceId_fkey" FOREIGN KEY ("initiatorSpaceId") REFERENCES "space"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "userTask" ADD CONSTRAINT "userTask_instanceID_fkey" FOREIGN KEY ("instanceID") REFERENCES "process-instance"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/management-system-v2/prisma/schema.prisma b/src/management-system-v2/prisma/schema.prisma index 6da3932b1..1ef590747 100644 --- a/src/management-system-v2/prisma/schema.prisma +++ b/src/management-system-v2/prisma/schema.prisma @@ -49,7 +49,6 @@ model User { emailVerificationTokens EmailVerificationToken[] @relation("verificationToken") mcpPairingCodes McpPairingCode[] @relation("pairingCode") Config Config[] - startedInstances ProcessInstance[] @relation("instanceInitiator") @@map("user") } @@ -145,7 +144,7 @@ model ArtifactVersionReference { } model Space { - id String @id @default(uuid()) + id String @id @default(uuid()) name String? isOrganization Boolean isActive Boolean? @@ -153,7 +152,7 @@ model Space { contactPhoneNumber String? contactEmail String? spaceLogo String? - owner User? @relation("spaceOwner", fields: [ownerId], references: [id], onDelete: Cascade) + owner User? @relation("spaceOwner", fields: [ownerId], references: [id], onDelete: Cascade) ownerId String? members Membership[] folders Folder[] @@ -161,10 +160,9 @@ model Space { htmlForms HtmlForm[] roles Role[] engines Engine[] - settings SpaceSettings? @relation("spaceSettings") + settings SpaceSettings? @relation("spaceSettings") Config Config[] mcpPairingCodes McpPairingCode[] - instances ProcessInstance[] @relation("instanceInitiatorSpace") @@map("space") } @@ -326,24 +324,11 @@ model HtmlForm { @@map("html-form") } -model ProcessInstance { - id String @id - definitionId String - initiator User @relation("instanceInitiator", fields: [initiatorId], references: [id], onDelete: Cascade) - initiatorId String - initiatorSpace Space @relation("instanceInitiatorSpace", fields: [initiatorSpaceId], references: [id], onDelete: Cascade) - initiatorSpaceId String - userTasks UserTask[] @relation("instance") - - @@map("process-instance") -} - model UserTask { - id String @id + id String @id taskId String name String? - instance ProcessInstance? @relation("instance", fields: [instanceID], references: [id], onDelete: Cascade) - instanceID String? + instanceID String fileName String html String? state String From ca6c51db449cc612b9943006aed77970e39279b6 Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Tue, 31 Mar 2026 10:16:49 +0200 Subject: [PATCH 08/11] Ran prettier --- .../lib/data/db/machine-config.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/management-system-v2/lib/data/db/machine-config.ts b/src/management-system-v2/lib/data/db/machine-config.ts index 81e62c888..dc9747ba0 100644 --- a/src/management-system-v2/lib/data/db/machine-config.ts +++ b/src/management-system-v2/lib/data/db/machine-config.ts @@ -526,7 +526,7 @@ export async function addMachineConfigVersion( previousMachine: Parameter, currentMachine: Parameter, versionNo: Int, -) { } +) {} // TODO rework: versioning /** @@ -2025,8 +2025,8 @@ export async function updateParameter( // make sure to remove backlinks from unlinked parameters let linkIds = parameter.transformation ? Object.values(parameter.transformation.linkedInputParameters).map( - ({ id }: { id: any }) => id, - ) + ({ id }: { id: any }) => id, + ) : []; const removedIds = linkIds.filter( (id) => @@ -2181,8 +2181,8 @@ export async function convertParameterType( inputParam.id === parameterId ? { data: { value: (newParameter as Parameter).value ?? updatedParameter.value } } : await db.configParameter.findUnique({ - where: { id: inputParam.id }, - }); + where: { id: inputParam.id }, + }); // convert to number if possible inputValues[key.substring(1)] = possiblyNumber( @@ -2310,7 +2310,7 @@ export async function removeParameter(parameterId: string) { const parameterPath = findPathToParameter(parameter.id, fullConfig, [], 'config'); if ( parameterPath.slice(0, 2).toString() == - ['identity-and-access-management', 'common-user-data'].toString() && + ['identity-and-access-management', 'common-user-data'].toString() && fullConfig.configType == 'organization' ) { await removeCommonUserDataPropagation(parameterPath.slice(2), fullConfig); @@ -2404,7 +2404,7 @@ export async function removeConfigVersion(configId: string, versionNo: number) { const error = e as Error; throw userError( error.message ?? - `There was an error removing the config versions: ${configId}-${versionNo}`, + `There was an error removing the config versions: ${configId}-${versionNo}`, ); } } else { From 30efb3d39eb268d72a698ffff2639771208868b0 Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Thu, 2 Apr 2026 08:49:13 +0200 Subject: [PATCH 09/11] Fixed: Missing import --- src/management-system-v2/lib/engines/server-actions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/management-system-v2/lib/engines/server-actions.ts b/src/management-system-v2/lib/engines/server-actions.ts index 92f7626b9..00badd958 100644 --- a/src/management-system-v2/lib/engines/server-actions.ts +++ b/src/management-system-v2/lib/engines/server-actions.ts @@ -1,6 +1,6 @@ 'use server'; -import { UserFacingError, getErrorMessage, isUserErrorResponse, userError } from '../user-error'; +import { UserFacingError, getErrorMessage, userError } from '../user-error'; import { deployProcess as _deployProcess, getDeployments as fetchDeployments, @@ -12,7 +12,7 @@ import { } from './deployment'; import { Engine, SpaceEngine } from './machines'; import { savedEnginesToEngines } from './saved-engines-helpers'; -import { getCurrentEnvironment } from '@/components/auth'; +import { getCurrentEnvironment, getCurrentUser } from '@/components/auth'; import { enableUseDB } from 'FeatureFlags'; import { getDbEngines, getDbEngineByAddress } from '@/lib/data/db/engines'; import { asyncFilter, asyncMap, asyncForEach } from '../helpers/javascriptHelpers'; From 18e6fdf48ed80303eb9ce175017ab67c0d747f18 Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Tue, 14 Apr 2026 09:19:12 +0200 Subject: [PATCH 10/11] Throwing error when user task contains invalid global data access --- .../display-items/tasklist/TaskList-DisplayItem.js | 2 +- .../lib/engines/server-actions.ts | 13 +++++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/engine/universal/ui/src/display-items/tasklist/TaskList-DisplayItem.js b/src/engine/universal/ui/src/display-items/tasklist/TaskList-DisplayItem.js index 8e4d5b0ab..24ee0ea2d 100644 --- a/src/engine/universal/ui/src/display-items/tasklist/TaskList-DisplayItem.js +++ b/src/engine/universal/ui/src/display-items/tasklist/TaskList-DisplayItem.js @@ -175,7 +175,7 @@ class TaskListTab extends DisplayItem { path += '/organization'; varPath = varPath.split('.').slice(1).join('.'); } else { - return; + throw new Error(`Unable to get data for global variable (@global.${varPath}).`); } path += `/${varPath}`; diff --git a/src/management-system-v2/lib/engines/server-actions.ts b/src/management-system-v2/lib/engines/server-actions.ts index 00badd958..81452f142 100644 --- a/src/management-system-v2/lib/engines/server-actions.ts +++ b/src/management-system-v2/lib/engines/server-actions.ts @@ -340,8 +340,9 @@ export async function getGlobalVariablesForHTML( } else if (segments[0] === '@worker' || !segments[0].startsWith('@')) { ({ userId } = await getCurrentUser()); } else if (segments[0] !== '@organization') { - console.error(`Invalid selector for global data access in user task html. (${segments[0]})`); - return; + throw new UserFacingError( + `Invalid selector for global data access in user task html. (${segments[0]})`, + ); } if (segments[0].startsWith('@')) segments = segments.slice(1); @@ -349,14 +350,10 @@ export async function getGlobalVariablesForHTML( const result = await getDataObject(spaceId, segments.join('.'), userId); if (isErrorResponse(result)) { - console.error( - 'Ecountered error while trying to get global variable for user task rendering:', - await result.data.text(), - ); - return; + throw new UserFacingError(await result.data.text()); } - return result.data.value; + return result.data?.value; }); } From 365442d7991060f4eb4a56f32d13f0f80d0251cd Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Tue, 14 Apr 2026 09:40:19 +0200 Subject: [PATCH 11/11] An attempt to start an instance when no engine is available will now lead to a user readable error instead of silently failing --- .../executions/deployment-hook.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployment-hook.ts b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployment-hook.ts index d4099db1c..935abc023 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployment-hook.ts +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployment-hook.ts @@ -100,14 +100,14 @@ function useDeployment(definitionId: string, initialData?: DeployedProcessInfo) }); const startInstance = async (versionId: string, variables: { [key: string]: any } = {}) => { - if (engines?.length) { - // TODO: in case of static deployment or different versions on different engines we will have - // to check if the engine can actually be used to start an instance - return await startInstanceOnMachine(definitionId, versionId, engines[0], variables, { - processInitiator: session?.user.id, - spaceIdOfProcessInitiator: space.spaceId, - }); - } + if (!engines?.length) return userError('No fitting engine found'); + + // TODO: in case of static deployment or different versions on different engines we will have + // to check if the engine can actually be used to start an instance + return await startInstanceOnMachine(definitionId, versionId, engines[0], variables, { + processInitiator: session?.user.id, + spaceIdOfProcessInitiator: space.spaceId, + }); }; const activeStates = ['PAUSED', 'RUNNING', 'READY', 'DEPLOYMENT-WAITING', 'WAITING']; @@ -171,7 +171,8 @@ function useDeployment(definitionId: string, initialData?: DeployedProcessInfo) } async function getStartForm(versionId: string) { - if (!engines) return; + if (!engines?.length) return userError('No fitting engine found'); + try { // TODO: in case of static deployment or different versions on different engines we will have // to check if the engine can actually be used to start an instance