diff --git a/lib/handlers/processActionHandler.ts b/lib/handlers/processActionHandler.ts index ca7a7671..2aa57dfe 100644 --- a/lib/handlers/processActionHandler.ts +++ b/lib/handlers/processActionHandler.ts @@ -93,7 +93,7 @@ export function createProcessActionHandler(config: ProcessActionConfig) { // Emit process event const payload: ProcessLifecyclePayload = { businessKey, cascade: specs.cascade }; - await emitProcessEvent(config.action, req, payload, config.logMessages.FAILED, businessKey); + await emitProcessEvent(config.action, req, payload, config.logMessages.FAILED); }; } diff --git a/lib/handlers/processService.ts b/lib/handlers/processService.ts index 968d9182..3c0570c9 100644 --- a/lib/handlers/processService.ts +++ b/lib/handlers/processService.ts @@ -1,6 +1,7 @@ import cds from '@sap/cds'; import { PROCESS_LOGGER_PREFIX, PROCESS_PREFIX, PROCESS_SERVICE } from '../constants'; import { emitProcessEvent, ProcessLifecyclePayload, ProcessStartPayload } from './utils'; +import { WorkflowStatus } from '../api'; const LOG = cds.log(PROCESS_LOGGER_PREFIX); @@ -36,7 +37,7 @@ function registerStartHandler(service: cds.Service, definitionId: string): void } const payload: ProcessStartPayload = { definitionId, context: inputs }; - await emitProcessEvent('start', req, payload, 'PROCESS_START_FAILED', definitionId); + await emitProcessEvent('start', req, payload, `Failed to start workflow: ${definitionId}`); }); } @@ -51,7 +52,12 @@ function registerSuspendHandler(service: cds.Service, definitionId: string): voi const payload: ProcessLifecyclePayload = { businessKey, cascade: cascade ?? false }; - await emitProcessEvent('suspend', req, payload, 'PROCESS_SUSPEND_FAILED', businessKey); + await emitProcessEvent( + 'suspend', + req, + payload, + `Failed to suspend process with business key: ${businessKey}`, + ); LOG.debug(`Process suspended: businessKey=${businessKey}`); }); @@ -68,7 +74,12 @@ function registerResumeHandler(service: cds.Service, definitionId: string): void const payload: ProcessLifecyclePayload = { businessKey, cascade: cascade ?? false }; - await emitProcessEvent('resume', req, payload, 'PROCESS_RESUME_FAILED', businessKey); + await emitProcessEvent( + 'resume', + req, + payload, + `Failed to resume process with business key: ${businessKey}`, + ); LOG.debug(`Process resumed: businessKey=${businessKey}`); }); @@ -84,7 +95,12 @@ function registerCancelHandler(service: cds.Service, definitionId: string): void } const payload: ProcessLifecyclePayload = { businessKey, cascade: cascade ?? false }; - await emitProcessEvent('cancel', req, payload, 'PROCESS_CANCEL_FAILED', businessKey); + await emitProcessEvent( + 'cancel', + req, + payload, + `Failed to cancel process with business key: ${businessKey}`, + ); LOG.debug(`Process cancelled: businessKey=${businessKey}`); }); @@ -97,13 +113,27 @@ function registerGetInstancesByBusinessKeyHandler( service.on('getInstancesByBusinessKey', async (req) => { LOG.debug(`Getting instances by businessKey for process: ${definitionId}`); - const { businessKey } = req.data; + const { businessKey, status } = req.data; if (!businessKey) { - return req.reject({ status: 400, message: 'MISSING_REQUIRED_PARAM_BUSINESS_KEY' }); + return req.reject({ status: 400, message: 'Missing required parameter: businessKey' }); + } + if (status) { + const validStatuses = Object.values(WorkflowStatus); + const statuses = Array.isArray(status) ? status : [status]; + const invalidStatuses = statuses.filter((s) => !validStatuses.includes(s)); + if (invalidStatuses.length > 0) { + return req.reject({ + status: 400, + message: `Invalid status value(s): ${invalidStatuses.join(', ')}. Valid values are: ${validStatuses.join(', ')}`, + }); + } } const processService = await cds.connect.to(PROCESS_SERVICE); - const result = await processService.send('getInstancesByBusinessKey', { businessKey }); + const result = await processService.send('getInstancesByBusinessKey', { + businessKey, + status, + }); return result; }); diff --git a/lib/handlers/processStart.ts b/lib/handlers/processStart.ts index c7149e94..b169fe56 100644 --- a/lib/handlers/processStart.ts +++ b/lib/handlers/processStart.ts @@ -103,7 +103,6 @@ export async function handleProcessStart(req: cds.Request, data: EntityRow): Pro req, payload, `Failed to start process with definition ID ${startSpecs.id!}.`, - startSpecs.id!, ); } diff --git a/lib/handlers/utils.ts b/lib/handlers/utils.ts index f67a8519..7aaed31d 100644 --- a/lib/handlers/utils.ts +++ b/lib/handlers/utils.ts @@ -212,14 +212,13 @@ export async function emitProcessEvent( req: cds.Request, payload: ProcessStartPayload | ProcessLifecyclePayload, processEventFailedMsg: string, - msgArgs: string, ): Promise { try { const processService = await cds.connect.to(PROCESS_SERVICE); const queuedProcessService = cds.queued(processService); await queuedProcessService.emit(event, payload); } catch (error) { - LOG.error(processEventFailedMsg, msgArgs, error); - req.reject({ status: 500, message: processEventFailedMsg, args: [msgArgs] }); + LOG.error(processEventFailedMsg, error); + req.reject({ status: 500, message: processEventFailedMsg }); } } diff --git a/lib/processImport.ts b/lib/processImport.ts index cf33d1cb..76e7e3d6 100644 --- a/lib/processImport.ts +++ b/lib/processImport.ts @@ -218,6 +218,7 @@ function addProcessTypes( const attributesName = fqn(serviceName, 'ProcessAttributes'); const instanceName = fqn(serviceName, 'ProcessInstance'); const instancesName = fqn(serviceName, 'ProcessInstances'); + const statusName = fqn(serviceName, 'ProcessInstanceStatus'); definitions[inputsName] = buildTypeFromSchema( inputsName, @@ -269,6 +270,12 @@ function addProcessTypes( name: instancesName, items: { type: instanceName }, }; + + definitions[statusName] = { + kind: 'type', + name: statusName, + items: { type: csn.CdsBuiltinType.String }, + }; } // ============================================================================ @@ -283,6 +290,7 @@ function addProcessActions( const outputsType = fqn(serviceName, 'ProcessOutputs'); const attributesType = fqn(serviceName, 'ProcessAttributes'); const instancesType = fqn(serviceName, 'ProcessInstances'); + const statusType = fqn(serviceName, 'ProcessInstanceStatus'); // Start action definitions[fqn(serviceName, 'start')] = { @@ -317,6 +325,7 @@ function addProcessActions( name: fqn(serviceName, 'getInstancesByBusinessKey'), params: { businessKey: { type: csn.CdsBuiltinType.String, notNull: true }, + status: { type: statusType }, }, returns: { type: instancesType }, }; diff --git a/srv/BTPProcessService.cds b/srv/BTPProcessService.cds index cbfbd74e..cf4b2857 100644 --- a/srv/BTPProcessService.cds +++ b/srv/BTPProcessService.cds @@ -35,6 +35,7 @@ service ProcessService { )returns AnyType; function getInstancesByBusinessKey( - @mandatory businessKey : String(256) + @mandatory businessKey : String(256), + status : many String(256) )returns InstancesReturn; } diff --git a/srv/BTPProcessService.ts b/srv/BTPProcessService.ts index a0d3fc3b..80a6d8be 100644 --- a/srv/BTPProcessService.ts +++ b/srv/BTPProcessService.ts @@ -87,18 +87,25 @@ class ProcessService extends cds.ApplicationService { this.on('getInstancesByBusinessKey', async (request: cds.Request) => { const { businessKey } = request.data; + let { status } = request.data; if (!businessKey) { return request.reject({ status: 400, message: 'Missing required parameter: businessKey' }); } - const instances = await this.workflowInstanceClient.getWorkflowsByBusinessKey(businessKey, [ - WorkflowStatus.RUNNING, - WorkflowStatus.SUSPENDED, - WorkflowStatus.COMPLETED, - WorkflowStatus.ERRONEOUS, - ]); + if (!status) { + status = [ + WorkflowStatus.RUNNING, + WorkflowStatus.SUSPENDED, + WorkflowStatus.COMPLETED, + WorkflowStatus.ERRONEOUS, + ]; + } + const instances = await this.workflowInstanceClient.getWorkflowsByBusinessKey( + businessKey, + status, + ); return instances; }); diff --git a/srv/localProcessService.ts b/srv/localProcessService.ts index 49622214..5827a4db 100644 --- a/srv/localProcessService.ts +++ b/srv/localProcessService.ts @@ -127,6 +127,7 @@ class ProcessService extends cds.ApplicationService { this.on('getInstancesByBusinessKey', async (req: cds.Request) => { const { businessKey } = req.data; + let { status } = req.data; LOG.debug( `==============================================================\n` + @@ -138,7 +139,16 @@ class ProcessService extends cds.ApplicationService { return req.reject({ status: 400, message: 'Missing required parameter: businessKey' }); } - const instances = localWorkflowStore.getInstancesByBusinessKey(businessKey); + if (!status) { + status = [ + WorkflowStatus.RUNNING, + WorkflowStatus.SUSPENDED, + WorkflowStatus.COMPLETED, + WorkflowStatus.ERRONEOUS, + ]; + } + + const instances = localWorkflowStore.getInstancesByBusinessKey(businessKey, status); LOG.debug(`Found ${instances.length} workflow instance(s) for businessKey: ${businessKey}`); return instances; diff --git a/tests/bookshop/srv/external/eu12.bpm-horizon-walkme.sdshipmentprocessor.shipmentHandler.cds b/tests/bookshop/srv/external/eu12.bpm-horizon-walkme.sdshipmentprocessor.shipmentHandler.cds index bd5c5a40..6c08660d 100644 --- a/tests/bookshop/srv/external/eu12.bpm-horizon-walkme.sdshipmentprocessor.shipmentHandler.cds +++ b/tests/bookshop/srv/external/eu12.bpm-horizon-walkme.sdshipmentprocessor.shipmentHandler.cds @@ -1,4 +1,4 @@ -/* checksum : 599270b792f9fe49fe94c62583c09a3e */ +/* checksum : ba13b7a95c8d1c3567a2bdb109bd687d */ namespace eu12.![bpm-horizon-walkme].sdshipmentprocessor; /** DO NOT EDIT. THIS IS A GENERATED SERVICE THAT WILL BE OVERRIDDEN ON NEXT IMPORT. */ @@ -39,16 +39,23 @@ service ShipmentHandlerService { }; type ProcessOutputs { - shipmentProcessResultOutput : ShipmentProcessResult; + shipmentProcessResultOutput : ShipmentProcessResult not null; }; - type ProcessAttributes { }; + type ProcessAttribute { + id : String not null; + label : String not null; + value : String; + type : String not null; + }; + + type ProcessAttributes : many ProcessAttribute; type ProcessInstance { definitionId : String; definitionVersion : String; id : String; - status: String; + status : String; startedAt : String; startedBy : String; }; @@ -68,7 +75,8 @@ service ShipmentHandlerService { ) returns ProcessOutputs; function getInstancesByBusinessKey( - businessKey : String not null + businessKey : String not null, + status : many String ) returns ProcessInstances; action suspend( diff --git a/tests/bookshop/srv/shipment-service.cds b/tests/bookshop/srv/shipment-service.cds index fe3e9c3c..7dd5790d 100644 --- a/tests/bookshop/srv/shipment-service.cds +++ b/tests/bookshop/srv/shipment-service.cds @@ -28,4 +28,7 @@ service ShipmentService { action getShipmentAttributes(shipmentID: UUID) returns String; action getShipmentOutputs(shipmentID: UUID) returns String; + + action getInstancesByShipmentID(shipmentID: UUID, + status: many String) returns String; } diff --git a/tests/bookshop/srv/shipment-service.ts b/tests/bookshop/srv/shipment-service.ts index 3d226302..1f70f6c4 100644 --- a/tests/bookshop/srv/shipment-service.ts +++ b/tests/bookshop/srv/shipment-service.ts @@ -104,13 +104,14 @@ class ShipmentService extends cds.ApplicationService { // Get all workflow instances by business key const instances = await processService.getInstancesByBusinessKey({ businessKey: shipmentID, + status: ['COMPLETED'], }); const allOutputs = []; // Get outputs for each workflow instance for (const instance of instances) { - if (instance.id && instance.status == 'COMPLETED') { + if (instance.id) { const outputs = await processService.getOutputs(instance.id); allOutputs.push({ workflowId: instance.id, @@ -122,6 +123,18 @@ class ShipmentService extends cds.ApplicationService { return JSON.stringify(allOutputs, null, 2); }); + this.on('getInstancesByShipmentID', async (req: cds.Request) => { + const { shipmentID, status } = req.data; + + const processService = await cds.connect.to(ShipmentHandlerService); + const instances = await processService.getInstancesByBusinessKey({ + businessKey: shipmentID, + status: status, + }); + + return JSON.stringify(instances, null, 2); + }); + await super.init(); } } diff --git a/tests/integration/programmaticApproach.test.ts b/tests/integration/programmaticApproach.test.ts index f7855dbb..8bc4ed0e 100644 --- a/tests/integration/programmaticApproach.test.ts +++ b/tests/integration/programmaticApproach.test.ts @@ -222,6 +222,66 @@ describe('Programatic Approach Integration Tests', () => { }); }); + describe('Get Instances by Shipment ID', () => { + it('should return instances with expected properties', async () => { + const shipmentID = await createShipment(); + await startShipment(shipmentID); + + const response = await POST('/odata/v4/shipment/getInstancesByShipmentID', { shipmentID }); + + const parsed = JSON.parse(response.data.value); + expect(Array.isArray(parsed)).toBe(true); + expect(parsed.length).toBeGreaterThan(0); + expect(parsed[0]).toHaveProperty('id'); + expect(parsed[0]).toHaveProperty('status'); + expect(parsed[0]).toHaveProperty('definitionId'); + }); + + it('should return an empty array when no workflow has been started', async () => { + const shipmentID = await createShipment(); + + const response = await POST('/odata/v4/shipment/getInstancesByShipmentID', { shipmentID }); + + expect(response.status).toBe(200); + const parsed = JSON.parse(response.data.value); + expect(Array.isArray(parsed)).toBe(true); + expect(parsed.length).toBe(0); + }); + + it('should filter instances by status', async () => { + const shipmentID = await createShipment(); + await startShipment(shipmentID); + + const response = await POST('/odata/v4/shipment/getInstancesByShipmentID', { + shipmentID, + status: ['COMPLETED'], + }); + + expect(response.status).toBe(200); + const parsed = JSON.parse(response.data.value); + expect(Array.isArray(parsed)).toBe(true); + // All returned instances should have the requested status + for (const instance of parsed) { + expect(instance.status).toBe('COMPLETED'); + } + }); + + it('should return no instances for a non-matching status filter', async () => { + const shipmentID = await createShipment(); + await startShipment(shipmentID); + + const response = await POST('/odata/v4/shipment/getInstancesByShipmentID', { + shipmentID, + status: ['CANCELED'], + }); + + expect(response.status).toBe(200); + const parsed = JSON.parse(response.data.value); + expect(Array.isArray(parsed)).toBe(true); + expect(parsed.length).toBe(0); + }); + }); + describe('Get Shipment Outputs', () => { it('should return outputs for a started (COMPLETED) shipment workflow', async () => { const shipmentID = await createShipment();