diff --git a/CHANGELOG.md b/CHANGELOG.md index de45ef46..a440944b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ - The format is based on [Keep a Changelog](https://keepachangelog.com/). - This project adheres to [Semantic Versioning](https://semver.org/). +## Version 0.2.2 - tbd + +### Added + +- `updateInstanceStatus` function on `ProcessService` and imported process services to update a workflow instance by its ID to a given status (`RUNNING`, `SUSPENDED`, `CANCELED`, `ERRONEOUS`, `COMPLETED`), with optional `cascade` support + ## Version 0.2.1 - 2026-04-20 ### Fixed diff --git a/lib/handlers/index.ts b/lib/handlers/index.ts index bcad0e21..7097f957 100644 --- a/lib/handlers/index.ts +++ b/lib/handlers/index.ts @@ -6,5 +6,10 @@ export { createProcessActionHandler } from './processActionHandler'; export { registerProcessServiceHandlers } from './processService'; export { buildAnnotationCache } from './annotationCache'; export { registerAnnotationHandlers } from './annotationHandlers'; -export type { EntityRow, ProcessStartPayload, ProcessLifecyclePayload } from './utils'; +export type { + EntityRow, + ProcessStartPayload, + ProcessLifecyclePayload, + ProcessUpdateStatusPayload, +} from './utils'; export type { ProcessDeleteRequest } from './onDeleteUtils'; diff --git a/lib/handlers/processService.ts b/lib/handlers/processService.ts index 597659e4..70e9c492 100644 --- a/lib/handlers/processService.ts +++ b/lib/handlers/processService.ts @@ -1,6 +1,11 @@ import cds from '@sap/cds'; import { PROCESS_LOGGER_PREFIX, PROCESS_PREFIX, PROCESS_SERVICE } from '../constants'; -import { emitProcessEvent, ProcessLifecyclePayload, ProcessStartPayload } from './utils'; +import { + emitProcessEvent, + ProcessLifecyclePayload, + ProcessStartPayload, + ProcessUpdateStatusPayload, +} from './utils'; import { WorkflowStatus } from '../api'; const LOG = cds.log(PROCESS_LOGGER_PREFIX); @@ -25,6 +30,7 @@ export function registerProcessServiceHandlers(service: cds.Service): void { registerGetInstancesByBusinessKeyHandler(service, definitionId); registerGetAttributesHandler(service, definitionId); registerGetOutputsHandler(service, definitionId); + registerUpdateInstanceStatusHandler(service, definitionId); } function registerStartHandler(service: cds.Service, definitionId: string): void { @@ -171,3 +177,34 @@ function registerGetOutputsHandler(service: cds.Service, definitionId: string): return result; }); } + +function registerUpdateInstanceStatusHandler(service: cds.Service, definitionId: string): void { + service.on('updateInstanceStatus', async (req) => { + LOG.debug(`Updating instance status for process: ${definitionId}`); + + const { instanceId, status, cascade } = req.data; + if (!instanceId) { + return req.reject({ status: 400, message: 'Missing required parameter: instanceId' }); + } + if (!status) { + return req.reject({ status: 400, message: 'Missing required parameter: status' }); + } + const validStatuses = Object.values(WorkflowStatus); + if (!validStatuses.includes(status)) { + return req.reject({ + status: 400, + message: `Invalid status: ${status}. Valid values are: ${validStatuses.join(', ')}`, + }); + } + + const payload: ProcessUpdateStatusPayload = { instanceId, status, cascade: cascade ?? false }; + await emitProcessEvent( + 'updateInstanceStatus', + req, + payload, + `Failed to update instance status for instanceId: ${instanceId}`, + ); + + LOG.debug(`Instance status update queued: instanceId=${instanceId}, status=${status}`); + }); +} diff --git a/lib/handlers/utils.ts b/lib/handlers/utils.ts index 52ad4dc1..a0f88002 100644 --- a/lib/handlers/utils.ts +++ b/lib/handlers/utils.ts @@ -6,7 +6,7 @@ const LOG = cds.log(PROCESS_LOGGER_PREFIX); /** * Process event types supported by the system */ -type ProcessEventType = 'start' | 'cancel' | 'suspend' | 'resume'; +type ProcessEventType = 'start' | 'cancel' | 'suspend' | 'resume' | 'updateInstanceStatus'; /** * A row of entity data with string-keyed fields @@ -31,6 +31,15 @@ export interface ProcessLifecyclePayload { cascade: boolean; } +/** + * Payload for updateInstanceStatus events + */ +export interface ProcessUpdateStatusPayload { + instanceId: string; + status: string; + cascade: boolean; +} + async function fetchEntity( results: EntityRow, request: cds.Request, @@ -164,7 +173,7 @@ export async function resolveEntityRowOrReject( export async function emitProcessEvent( event: ProcessEventType, req: cds.Request, - payload: ProcessStartPayload | ProcessLifecyclePayload, + payload: ProcessStartPayload | ProcessLifecyclePayload | ProcessUpdateStatusPayload, processEventFailedMsg: string, businessKeyValue?: string, ): Promise { diff --git a/lib/processImport/csnBuilder.ts b/lib/processImport/csnBuilder.ts index a700aabd..4f92b979 100644 --- a/lib/processImport/csnBuilder.ts +++ b/lib/processImport/csnBuilder.ts @@ -201,6 +201,16 @@ function addProcessActions( returns: { type: instancesType }, }; + definitions[fqn(serviceName, 'updateInstanceStatus')] = { + kind: 'action', + name: fqn(serviceName, 'updateInstanceStatus'), + params: { + instanceId: { type: csn.CdsBuiltinType.String, notNull: true }, + status: { type: csn.CdsBuiltinType.String, notNull: true }, + cascade: { type: csn.CdsBuiltinType.Boolean }, + }, + }; + // Lifecycle actions for (const action of ['suspend', 'resume', 'cancel']) { definitions[fqn(serviceName, action)] = { diff --git a/lib/types/csn-extensions.ts b/lib/types/csn-extensions.ts index 9de4c4ff..3c9083fc 100644 --- a/lib/types/csn-extensions.ts +++ b/lib/types/csn-extensions.ts @@ -58,11 +58,12 @@ export type CsnDefinition = | CsnService | CsnAction | CsnFunction + | CsnEvent | CsnAnnotation; export interface CsnBaseDefinition extends CsnAnnotations { name?: string; doc?: string; - kind: 'entity' | 'type' | 'service' | 'action' | 'function' | 'annotation'; + kind: 'entity' | 'type' | 'service' | 'action' | 'function' | 'event' | 'annotation'; } // // ────────────────────────────────────────────────────────────── @@ -132,6 +133,10 @@ export interface CsnFunction extends CsnBaseDefinition { params?: Record; returns?: CsnType | CsnElement; } +export interface CsnEvent extends CsnBaseDefinition { + kind: 'event'; + elements?: Record; +} // // ────────────────────────────────────────────────────────────── // ANNOTATION diff --git a/srv/BTPProcessService.cds b/srv/BTPProcessService.cds index 6e066e46..04a99d52 100644 --- a/srv/BTPProcessService.cds +++ b/srv/BTPProcessService.cds @@ -27,6 +27,12 @@ service ProcessService { cascade : Boolean } + event updateInstanceStatus { + @mandatory instanceId : String(256); + @mandatory status : String(256); + cascade : Boolean + } + function getAttributes( @mandatory processInstanceId : String(256) )returns AttributesReturn; @@ -40,3 +46,4 @@ service ProcessService { status : many String(256) )returns InstancesReturn; } + diff --git a/srv/BTPProcessService.ts b/srv/BTPProcessService.ts index e3f05af1..88f195f0 100644 --- a/srv/BTPProcessService.ts +++ b/srv/BTPProcessService.ts @@ -142,6 +142,16 @@ class ProcessService extends cds.ApplicationService { return outputs; }); + this.on('updateInstanceStatus', async (request: cds.Request) => { + const { instanceId, status, cascade } = request.data; + LOG.info('Updating instance status', instanceId, '->', status); + await this.workflowInstanceClient.updateWorkflowStatus( + instanceId, + status as WorkflowStatus, + cascade ?? false, + ); + }); + return super.init(); } diff --git a/srv/localProcessService.ts b/srv/localProcessService.ts index 7148dabe..73586d9d 100644 --- a/srv/localProcessService.ts +++ b/srv/localProcessService.ts @@ -213,6 +213,26 @@ class ProcessService extends cds.ApplicationService { return outputs; }); + this.on('updateInstanceStatus', async (req: cds.Request) => { + const { instanceId, status } = req.data; + LOG.info('Updating instance status', instanceId, '->', status); + + LOG.debug( + `==============================================================\n` + + `Update instance status for ${instanceId} to ${status}\n` + + `==============================================================`, + ); + + const result = localWorkflowStore.updateStatus(instanceId, status as WorkflowStatus); + + if (!result.success) { + LOG.warn(`Workflow instance not found: ${instanceId}`); + return; + } + + LOG.debug(`Updated status for instance: ${instanceId} to ${status}`); + }); + return super.init(); } } diff --git a/tests/bookshop/srv/external/eu12.cdsmunich.capprocesspluginhybridtest.annotation_Lifecycle_Process.cds b/tests/bookshop/srv/external/eu12.cdsmunich.capprocesspluginhybridtest.annotation_Lifecycle_Process.cds index 02198360..891bb0b1 100644 --- a/tests/bookshop/srv/external/eu12.cdsmunich.capprocesspluginhybridtest.annotation_Lifecycle_Process.cds +++ b/tests/bookshop/srv/external/eu12.cdsmunich.capprocesspluginhybridtest.annotation_Lifecycle_Process.cds @@ -1,4 +1,4 @@ -/* checksum : 2d9b04f7d100bb10cefeaa255ec0b188 */ +/* checksum : 3f6145d8ec63b84aa16c320a4868fcd9 */ namespace eu12.cdsmunich.capprocesspluginhybridtest; /** DO NOT EDIT. THIS IS A GENERATED SERVICE THAT WILL BE OVERRIDDEN ON NEXT IMPORT. */ @@ -48,6 +48,12 @@ service Annotation_Lifecycle_ProcessService { status : many String ) returns ProcessInstances; + action updateInstanceStatus( + instanceId : String not null, + status : String not null, + cascade : Boolean + ); + action suspend( businessKey : String not null, cascade : Boolean diff --git a/tests/bookshop/srv/external/eu12.cdsmunich.capprocesspluginhybridtest.annotation_Lifecycle_Process_Two.cds b/tests/bookshop/srv/external/eu12.cdsmunich.capprocesspluginhybridtest.annotation_Lifecycle_Process_Two.cds index 5329f538..38f5c322 100644 --- a/tests/bookshop/srv/external/eu12.cdsmunich.capprocesspluginhybridtest.annotation_Lifecycle_Process_Two.cds +++ b/tests/bookshop/srv/external/eu12.cdsmunich.capprocesspluginhybridtest.annotation_Lifecycle_Process_Two.cds @@ -1,4 +1,4 @@ -/* checksum : 15602da859ed8169e46688286553aafe */ +/* checksum : 85ab0177d589131840ae18bca7300e41 */ namespace eu12.cdsmunich.capprocesspluginhybridtest; /** DO NOT EDIT. THIS IS A GENERATED SERVICE THAT WILL BE OVERRIDDEN ON NEXT IMPORT. */ @@ -48,6 +48,12 @@ service Annotation_Lifecycle_Process_TwoService { status : many String ) returns ProcessInstances; + action updateInstanceStatus( + instanceId : String not null, + status : String not null, + cascade : Boolean + ); + action suspend( businessKey : String not null, cascade : Boolean diff --git a/tests/bookshop/srv/external/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Attributes_And_Outputs.cds b/tests/bookshop/srv/external/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Attributes_And_Outputs.cds index c11851f6..23a2fb6d 100644 --- a/tests/bookshop/srv/external/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Attributes_And_Outputs.cds +++ b/tests/bookshop/srv/external/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Attributes_And_Outputs.cds @@ -1,4 +1,4 @@ -/* checksum : b2be28c9da2617d511526b2f68e5e6b0 */ +/* checksum : de8a712854c579e0f68840ddc6c7e1da */ namespace eu12.cdsmunich.capprocesspluginhybridtest; /** DO NOT EDIT. THIS IS A GENERATED SERVICE THAT WILL BE OVERRIDDEN ON NEXT IMPORT. */ @@ -84,6 +84,12 @@ service ImportProcess_Attributes_And_OutputsService { status : many String ) returns ProcessInstances; + action updateInstanceStatus( + instanceId : String not null, + status : String not null, + cascade : Boolean + ); + action suspend( businessKey : String not null, cascade : Boolean diff --git a/tests/bookshop/srv/external/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Complex_Inputs.cds b/tests/bookshop/srv/external/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Complex_Inputs.cds index 93483826..bb3a5ac2 100644 --- a/tests/bookshop/srv/external/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Complex_Inputs.cds +++ b/tests/bookshop/srv/external/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Complex_Inputs.cds @@ -1,4 +1,4 @@ -/* checksum : b0ced28bb4d1bef714f6714bff14642e */ +/* checksum : c7adfadbf57db190b8e967f4eabf8094 */ namespace eu12.cdsmunich.capprocesspluginhybridtest; /** DO NOT EDIT. THIS IS A GENERATED SERVICE THAT WILL BE OVERRIDDEN ON NEXT IMPORT. */ @@ -76,6 +76,12 @@ service ImportProcess_Complex_InputsService { status : many String ) returns ProcessInstances; + action updateInstanceStatus( + instanceId : String not null, + status : String not null, + cascade : Boolean + ); + action suspend( businessKey : String not null, cascade : Boolean diff --git a/tests/bookshop/srv/external/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Simple_Inputs.cds b/tests/bookshop/srv/external/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Simple_Inputs.cds index d26fccaf..8679f701 100644 --- a/tests/bookshop/srv/external/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Simple_Inputs.cds +++ b/tests/bookshop/srv/external/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Simple_Inputs.cds @@ -1,4 +1,4 @@ -/* checksum : 7e054e53e107a7f5c8375eb51a454e7f */ +/* checksum : 980ec47ccf335af79644904cc3144504 */ namespace eu12.cdsmunich.capprocesspluginhybridtest; /** DO NOT EDIT. THIS IS A GENERATED SERVICE THAT WILL BE OVERRIDDEN ON NEXT IMPORT. */ @@ -53,6 +53,12 @@ service ImportProcess_Simple_InputsService { status : many String ) returns ProcessInstances; + action updateInstanceStatus( + instanceId : String not null, + status : String not null, + cascade : Boolean + ); + action suspend( businessKey : String not null, cascade : Boolean diff --git a/tests/bookshop/srv/external/eu12.cdsmunich.capprocesspluginhybridtest.programmatic_Lifecycle_Process.cds b/tests/bookshop/srv/external/eu12.cdsmunich.capprocesspluginhybridtest.programmatic_Lifecycle_Process.cds index 177eecb0..a123baa3 100644 --- a/tests/bookshop/srv/external/eu12.cdsmunich.capprocesspluginhybridtest.programmatic_Lifecycle_Process.cds +++ b/tests/bookshop/srv/external/eu12.cdsmunich.capprocesspluginhybridtest.programmatic_Lifecycle_Process.cds @@ -1,4 +1,4 @@ -/* checksum : 721729e92c5456fb13b5e792ac205215 */ +/* checksum : a2b3149ef0e5da23a6b84e0c53043379 */ namespace eu12.cdsmunich.capprocesspluginhybridtest; /** DO NOT EDIT. THIS IS A GENERATED SERVICE THAT WILL BE OVERRIDDEN ON NEXT IMPORT. */ @@ -50,6 +50,12 @@ service Programmatic_Lifecycle_ProcessService { status : many String ) returns ProcessInstances; + action updateInstanceStatus( + instanceId : String not null, + status : String not null, + cascade : Boolean + ); + action suspend( businessKey : String not null, cascade : Boolean diff --git a/tests/bookshop/srv/external/eu12.cdsmunich.capprocesspluginhybridtest.programmatic_Output_Process.cds b/tests/bookshop/srv/external/eu12.cdsmunich.capprocesspluginhybridtest.programmatic_Output_Process.cds index 8a937cd8..8eb277ac 100644 --- a/tests/bookshop/srv/external/eu12.cdsmunich.capprocesspluginhybridtest.programmatic_Output_Process.cds +++ b/tests/bookshop/srv/external/eu12.cdsmunich.capprocesspluginhybridtest.programmatic_Output_Process.cds @@ -1,4 +1,4 @@ -/* checksum : 5d4deaa24aac52f7afcf274a034ff450 */ +/* checksum : 6fb3b83b7c518c1c26dd8b55ce090cc4 */ namespace eu12.cdsmunich.capprocesspluginhybridtest; /** DO NOT EDIT. THIS IS A GENERATED SERVICE THAT WILL BE OVERRIDDEN ON NEXT IMPORT. */ @@ -57,6 +57,12 @@ service Programmatic_Output_ProcessService { status : many String ) returns ProcessInstances; + action updateInstanceStatus( + instanceId : String not null, + status : String not null, + cascade : Boolean + ); + action suspend( businessKey : String not null, cascade : Boolean diff --git a/tests/bookshop/srv/programmatic-service.cds b/tests/bookshop/srv/programmatic-service.cds index 90685fe4..1c605096 100644 --- a/tests/bookshop/srv/programmatic-service.cds +++ b/tests/bookshop/srv/programmatic-service.cds @@ -50,4 +50,8 @@ service ProgrammaticService { status: many String) returns many ProcessInstance; action genericGetAttributes(processInstanceId: String) returns many ProcessAttribute; action genericGetOutputs(processInstanceId: String) returns ProcessOutputs; + + action genericUpdateInstanceStatus(instanceId: String, status: String, cascade: Boolean); + + action updateInstanceStatusViaProcess(instanceId: String, status: String); } diff --git a/tests/bookshop/srv/programmatic-service.ts b/tests/bookshop/srv/programmatic-service.ts index d038f650..7ce1791d 100644 --- a/tests/bookshop/srv/programmatic-service.ts +++ b/tests/bookshop/srv/programmatic-service.ts @@ -1,6 +1,7 @@ import cds from '@sap/cds'; import Programmatic_Lifecycle_ProcessService from '#cds-models/eu12/cdsmunich/capprocesspluginhybridtest/Programmatic_Lifecycle_ProcessService'; import Programmatic_Outputs_ProcessService from '#cds-models/eu12/cdsmunich/capprocesspluginhybridtest/Programmatic_Output_ProcessService'; +import { WorkflowStatus } from '@cap-js/process/lib/api'; class ProgrammaticService extends cds.ApplicationService { async init() { @@ -148,6 +149,33 @@ class ProgrammaticService extends cds.ApplicationService { return result; }); + this.on('genericUpdateInstanceStatus', async (req: cds.Request) => { + const { instanceId, status, cascade } = req.data; + const validStatuses = Object.values(WorkflowStatus); + if (!validStatuses.includes(status as WorkflowStatus)) { + return req.reject( + 400, + `Invalid status: ${status}. Valid values are: ${validStatuses.join(', ')}`, + ); + } + const queuedProcessService = cds.queued(processService); + await queuedProcessService.emit('updateInstanceStatus', { + instanceId, + status, + cascade: cascade ?? false, + }); + }); + + this.on('updateInstanceStatusViaProcess', async (req: cds.Request) => { + const { instanceId, status } = req.data; + const queuedProgrammaticLifecycle = cds.queued(programmaticLifecycleProcess); + await queuedProgrammaticLifecycle.emit('updateInstanceStatus', { + instanceId, + status, + cascade: false, + }); + }); + await super.init(); } } diff --git a/tests/bookshop/srv/workflows/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Attributes_And_Outputs.json b/tests/bookshop/srv/workflows/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Attributes_And_Outputs.json index 72264a98..339d47f3 100644 --- a/tests/bookshop/srv/workflows/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Attributes_And_Outputs.json +++ b/tests/bookshop/srv/workflows/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Attributes_And_Outputs.json @@ -1,9 +1,10 @@ { "uid": "5edc02ff-ce9a-4aaf-94bb-440b0c981a20", "name": "ImportProcess_Attributes_And_Outputs", - "identifier": "importProcess_Attributes_And_Outputs", + "description": "", "type": "bpi.process", - "projectId": "eu12.cdsmunich.capprocesspluginhybridtest", + "createdAt": "2026-03-18T10:23:31.648909Z", + "updatedAt": "2026-03-18T12:48:22.484258Z", "header": { "inputs": { "title": "inputs", @@ -12,28 +13,23 @@ "definitions": { "date": { "type": "string", - "format": "date", - "title": "date" + "format": "date" }, "dateTime": { "type": "string", - "format": "date-time", - "title": "dateTime" + "format": "date-time" }, "password": { "type": "string", - "password": true, - "title": "password" + "password": true }, "time": { "type": "string", - "format": "time", - "title": "time" + "format": "time" }, "documentFolder": { "type": "string", - "format": "document-folder", - "title": "documentFolder" + "format": "document-folder" } }, "properties": { @@ -54,8 +50,7 @@ "refName": "ImportProcess_Complex_DataType" }, "title": "Complexe", - "description": "", - "refName": "ImportProcess_Complex_DataType" + "description": "" }, "optionalcomplexe": { "$ref": "$.11cbfe86-2e73-4198-868a-d7a2115d82c1", @@ -77,28 +72,23 @@ "definitions": { "date": { "type": "string", - "format": "date", - "title": "date" + "format": "date" }, "dateTime": { "type": "string", - "format": "date-time", - "title": "dateTime" + "format": "date-time" }, "password": { "type": "string", - "password": true, - "title": "password" + "password": true }, "time": { "type": "string", - "format": "time", - "title": "time" + "format": "time" }, "documentFolder": { "type": "string", - "format": "document-folder", - "title": "documentFolder" + "format": "document-folder" } }, "properties": { @@ -125,8 +115,7 @@ "refName": "ImportProcess_Complex_DataType" }, "title": "Complexe", - "description": "", - "refName": "ImportProcess_Complex_DataType" + "description": "" } }, "required": [ @@ -135,6 +124,8 @@ ] }, "processAttributes": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "processAttributes", "type": "object", "properties": {}, "required": [] @@ -146,12 +137,17 @@ "type": "both" } ], + "identifier": "importProcess_Attributes_And_Outputs", + "valid": true, + "projectId": "eu12.cdsmunich.capprocesspluginhybridtest", "dataTypes": [ { "uid": "11cbfe86-2e73-4198-868a-d7a2115d82c1", "name": "ImportProcess_Complex_DataType", - "identifier": "ImportProcess_Complex_DataType", + "description": "", "type": "datatype", + "createdAt": "2026-03-18T10:28:54.794097Z", + "updatedAt": "2026-03-18T10:31:37.975015Z", "header": { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", @@ -164,13 +160,11 @@ "properties": { "SubString1": { "type": "string", - "uid": "567555b3-ffee-41d4-a650-e9006ca9c5f6", - "title": "SubString1" + "uid": "567555b3-ffee-41d4-a650-e9006ca9c5f6" }, "Substring2": { "type": "string", - "uid": "794440a0-a06e-4fa6-a703-1f8fe4bc9d94", - "title": "Substring2" + "uid": "794440a0-a06e-4fa6-a703-1f8fe4bc9d94" } }, "required": [ @@ -178,8 +172,7 @@ "Substring2" ] }, - "type": "array", - "title": "StringList" + "type": "array" }, "StringType": { "type": "object", @@ -194,27 +187,21 @@ "uid": "f3dd8f45-fcd2-4fe2-9d2f-868e51f52b64", "properties": { "SubSubSubDate": { - "type": "string", - "format": "date", - "title": "SubSubSubDate" + "uid": "361a2e90-c32d-421d-8077-6a412436f031", + "$ref": "#/definitions/date" }, "SubSubSubPassword": { - "type": "string", - "password": true, - "title": "SubSubSubPassword" + "uid": "9901ea3d-4e0f-4421-981e-22fbc0d35cc6", + "$ref": "#/definitions/password" }, "SubSubSubAny": { - "uid": "99fd32c0-7ce9-4c22-bb85-890160827365", - "title": "SubSubSubAny" + "uid": "99fd32c0-7ce9-4c22-bb85-890160827365" } - }, - "title": "SubSubStringType" + } } - }, - "title": "SubStringType" + } } - }, - "title": "StringType" + } } }, "required": [ @@ -223,18 +210,17 @@ "definitions": { "password": { "type": "string", - "password": true, - "title": "password" + "password": true }, "date": { "type": "string", - "format": "date", - "title": "date" + "format": "date" } }, - "version": 1, - "description": "" - } + "version": 1 + }, + "identifier": "importProcess_Complex_DataType", + "valid": true } ] } \ No newline at end of file diff --git a/tests/bookshop/srv/workflows/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Complex_Inputs.json b/tests/bookshop/srv/workflows/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Complex_Inputs.json index 1d011fae..155ebcc8 100644 --- a/tests/bookshop/srv/workflows/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Complex_Inputs.json +++ b/tests/bookshop/srv/workflows/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Complex_Inputs.json @@ -1,9 +1,10 @@ { "uid": "edb7b7fd-8a1c-4c51-a19f-b357aece8428", "name": "ImportProcess_Complex_Inputs", - "identifier": "importProcess_Complex_Inputs", + "description": "", "type": "bpi.process", - "projectId": "eu12.cdsmunich.capprocesspluginhybridtest", + "createdAt": "2026-03-18T10:23:06.672697Z", + "updatedAt": "2026-03-19T11:43:58.132360Z", "header": { "inputs": { "title": "inputs", @@ -12,28 +13,23 @@ "definitions": { "date": { "type": "string", - "format": "date", - "title": "date" + "format": "date" }, "dateTime": { "type": "string", - "format": "date-time", - "title": "dateTime" + "format": "date-time" }, "password": { "type": "string", - "password": true, - "title": "password" + "password": true }, "time": { "type": "string", - "format": "time", - "title": "time" + "format": "time" }, "documentFolder": { "type": "string", - "format": "document-folder", - "title": "documentFolder" + "format": "document-folder" } }, "properties": { @@ -70,6 +66,8 @@ "properties": {} }, "processAttributes": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "processAttributes", "type": "object", "properties": {}, "required": [] @@ -81,12 +79,17 @@ "type": "both" } ], + "identifier": "importProcess_Complex_Inputs", + "valid": true, + "projectId": "eu12.cdsmunich.capprocesspluginhybridtest", "dataTypes": [ { "uid": "11cbfe86-2e73-4198-868a-d7a2115d82c1", "name": "ImportProcess_Complex_DataType", - "identifier": "ImportProcess_Complex_DataType", + "description": "", "type": "datatype", + "createdAt": "2026-03-18T10:28:54.794097Z", + "updatedAt": "2026-03-18T10:31:37.975015Z", "header": { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", @@ -99,13 +102,11 @@ "properties": { "SubString1": { "type": "string", - "uid": "567555b3-ffee-41d4-a650-e9006ca9c5f6", - "title": "SubString1" + "uid": "567555b3-ffee-41d4-a650-e9006ca9c5f6" }, "Substring2": { "type": "string", - "uid": "794440a0-a06e-4fa6-a703-1f8fe4bc9d94", - "title": "Substring2" + "uid": "794440a0-a06e-4fa6-a703-1f8fe4bc9d94" } }, "required": [ @@ -113,8 +114,7 @@ "Substring2" ] }, - "type": "array", - "title": "StringList" + "type": "array" }, "StringType": { "type": "object", @@ -129,27 +129,21 @@ "uid": "f3dd8f45-fcd2-4fe2-9d2f-868e51f52b64", "properties": { "SubSubSubDate": { - "type": "string", - "format": "date", - "title": "SubSubSubDate" + "uid": "361a2e90-c32d-421d-8077-6a412436f031", + "$ref": "#/definitions/date" }, "SubSubSubPassword": { - "type": "string", - "password": true, - "title": "SubSubSubPassword" + "uid": "9901ea3d-4e0f-4421-981e-22fbc0d35cc6", + "$ref": "#/definitions/password" }, "SubSubSubAny": { - "uid": "99fd32c0-7ce9-4c22-bb85-890160827365", - "title": "SubSubSubAny" + "uid": "99fd32c0-7ce9-4c22-bb85-890160827365" } - }, - "title": "SubSubStringType" + } } - }, - "title": "SubStringType" + } } - }, - "title": "StringType" + } } }, "required": [ @@ -158,18 +152,17 @@ "definitions": { "password": { "type": "string", - "password": true, - "title": "password" + "password": true }, "date": { "type": "string", - "format": "date", - "title": "date" + "format": "date" } }, - "version": 1, - "description": "" - } + "version": 1 + }, + "identifier": "importProcess_Complex_DataType", + "valid": true } ] } \ No newline at end of file diff --git a/tests/bookshop/srv/workflows/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Simple_Inputs.json b/tests/bookshop/srv/workflows/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Simple_Inputs.json index 13169e36..19d87987 100644 --- a/tests/bookshop/srv/workflows/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Simple_Inputs.json +++ b/tests/bookshop/srv/workflows/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Simple_Inputs.json @@ -1,9 +1,10 @@ { "uid": "0fc541c5-5266-458e-801a-9fe7fbfc32e5", "name": "ImportProcess_Simple_Inputs", - "identifier": "importProcess_Simple_Inputs", + "description": "", "type": "bpi.process", - "projectId": "eu12.cdsmunich.capprocesspluginhybridtest", + "createdAt": "2026-03-18T10:22:42.550371Z", + "updatedAt": "2026-03-18T10:25:09.164955Z", "header": { "inputs": { "title": "inputs", @@ -12,28 +13,23 @@ "definitions": { "date": { "type": "string", - "format": "date", - "title": "date" + "format": "date" }, "dateTime": { "type": "string", - "format": "date-time", - "title": "dateTime" + "format": "date-time" }, "password": { "type": "string", - "password": true, - "title": "password" + "password": true }, "time": { "type": "string", - "format": "time", - "title": "time" + "format": "time" }, "documentFolder": { "type": "string", - "format": "document-folder", - "title": "documentFolder" + "format": "document-folder" } }, "properties": { @@ -53,20 +49,17 @@ "description": "" }, "date": { - "type": "string", - "format": "date", + "$ref": "#/definitions/date", "title": "Date", "description": "" }, "datetime": { - "type": "string", - "format": "date-time", + "$ref": "#/definitions/dateTime", "title": "DateTime", "description": "" }, "documentfolder": { - "type": "string", - "format": "document-folder", + "$ref": "#/definitions/documentFolder", "title": "DocumentFolder", "description": "" } @@ -87,9 +80,14 @@ "properties": {} }, "processAttributes": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "processAttributes", "type": "object", "properties": {}, "required": [] } - } + }, + "identifier": "importProcess_Simple_Inputs", + "valid": true, + "projectId": "eu12.cdsmunich.capprocesspluginhybridtest" } \ No newline at end of file diff --git a/tests/hybrid/importedCDS/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Attributes_And_Outputs.cds b/tests/hybrid/importedCDS/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Attributes_And_Outputs.cds index c11851f6..23a2fb6d 100644 --- a/tests/hybrid/importedCDS/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Attributes_And_Outputs.cds +++ b/tests/hybrid/importedCDS/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Attributes_And_Outputs.cds @@ -1,4 +1,4 @@ -/* checksum : b2be28c9da2617d511526b2f68e5e6b0 */ +/* checksum : de8a712854c579e0f68840ddc6c7e1da */ namespace eu12.cdsmunich.capprocesspluginhybridtest; /** DO NOT EDIT. THIS IS A GENERATED SERVICE THAT WILL BE OVERRIDDEN ON NEXT IMPORT. */ @@ -84,6 +84,12 @@ service ImportProcess_Attributes_And_OutputsService { status : many String ) returns ProcessInstances; + action updateInstanceStatus( + instanceId : String not null, + status : String not null, + cascade : Boolean + ); + action suspend( businessKey : String not null, cascade : Boolean diff --git a/tests/hybrid/importedCDS/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Complex_Inputs.cds b/tests/hybrid/importedCDS/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Complex_Inputs.cds index 93483826..bb3a5ac2 100644 --- a/tests/hybrid/importedCDS/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Complex_Inputs.cds +++ b/tests/hybrid/importedCDS/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Complex_Inputs.cds @@ -1,4 +1,4 @@ -/* checksum : b0ced28bb4d1bef714f6714bff14642e */ +/* checksum : c7adfadbf57db190b8e967f4eabf8094 */ namespace eu12.cdsmunich.capprocesspluginhybridtest; /** DO NOT EDIT. THIS IS A GENERATED SERVICE THAT WILL BE OVERRIDDEN ON NEXT IMPORT. */ @@ -76,6 +76,12 @@ service ImportProcess_Complex_InputsService { status : many String ) returns ProcessInstances; + action updateInstanceStatus( + instanceId : String not null, + status : String not null, + cascade : Boolean + ); + action suspend( businessKey : String not null, cascade : Boolean diff --git a/tests/hybrid/importedCDS/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Simple_Inputs.cds b/tests/hybrid/importedCDS/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Simple_Inputs.cds index d26fccaf..8679f701 100644 --- a/tests/hybrid/importedCDS/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Simple_Inputs.cds +++ b/tests/hybrid/importedCDS/eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Simple_Inputs.cds @@ -1,4 +1,4 @@ -/* checksum : 7e054e53e107a7f5c8375eb51a454e7f */ +/* checksum : 980ec47ccf335af79644904cc3144504 */ namespace eu12.cdsmunich.capprocesspluginhybridtest; /** DO NOT EDIT. THIS IS A GENERATED SERVICE THAT WILL BE OVERRIDDEN ON NEXT IMPORT. */ @@ -53,6 +53,12 @@ service ImportProcess_Simple_InputsService { status : many String ) returns ProcessInstances; + action updateInstanceStatus( + instanceId : String not null, + status : String not null, + cascade : Boolean + ); + action suspend( businessKey : String not null, cascade : Boolean diff --git a/tests/integration/genericProgrammaticApproach.test.ts b/tests/integration/genericProgrammaticApproach.test.ts index eb939e3f..5dc9e7f6 100644 --- a/tests/integration/genericProgrammaticApproach.test.ts +++ b/tests/integration/genericProgrammaticApproach.test.ts @@ -221,4 +221,65 @@ describe('Generic ProcessService Integration Tests', () => { expect(foundMessages[0].data.context.number).toEqual(42); }); }); + + describe('Update Instance Status', () => { + async function getInstanceId(businessKey: string, maxRetries = 10): Promise { + await (cds as any).flush(); + const res = await POST('/odata/v4/programmatic/genericGetInstancesByBusinessKey', { + businessKey, + }); + if (res.data.value?.length > 0) return res.data.value[0].id; + if (maxRetries <= 0) throw new Error(`No instance found for businessKey: ${businessKey}`); + await new Promise((r) => setTimeout(r, 1000)); + return getInstanceId(businessKey, maxRetries - 1); + } + + async function updateInstanceStatus(instanceId: string, status: string, cascade?: boolean) { + return POST('/odata/v4/programmatic/genericUpdateInstanceStatus', { + instanceId, + status, + cascade, + }); + } + + it('should emit an updateInstanceStatus event to the outbox', async () => { + const businessKey = generateID(); + await genericStart(businessKey); + + const instanceId = await getInstanceId(businessKey); + const response = await updateInstanceStatus(instanceId, 'SUSPENDED'); + + expect(response.status).toBe(204); + expect(foundMessages.some((m: any) => m.event === 'updateInstanceStatus')).toBe(true); + }); + + it('should reflect the new status when queried after update', async () => { + const businessKey = generateID(); + await genericStart(businessKey); + + const instanceId = await getInstanceId(businessKey); + await updateInstanceStatus(instanceId, 'SUSPENDED'); + await (cds as any).flush(); + + const res = await POST('/odata/v4/programmatic/genericGetInstancesByBusinessKey', { + businessKey, + status: ['SUSPENDED'], + }); + expect(res.data.value.some((i: any) => i.id === instanceId)).toBe(true); + }); + + it('should return 400 for an invalid status value', async () => { + const businessKey = generateID(); + await genericStart(businessKey); + + const instanceId = await getInstanceId(businessKey); + + try { + await updateInstanceStatus(instanceId, 'INVALID'); + fail('Expected request to be rejected'); + } catch (error: any) { + expect(error.response.status).toBe(400); + } + }); + }); }); diff --git a/tests/integration/programmaticApproach.test.ts b/tests/integration/programmaticApproach.test.ts index 29d6bb3c..552c2d28 100644 --- a/tests/integration/programmaticApproach.test.ts +++ b/tests/integration/programmaticApproach.test.ts @@ -238,4 +238,58 @@ describe('Programmatic Approach Integration Tests', () => { expect(foundMessages[0].data.context.optional_datetime).toEqual(optional_datetime); }); }); + + describe('Update Instance Status via imported process service', () => { + async function waitForInstance( + businessKey: string, + status: string[], + maxRetries = 10, + ): Promise { + await (cds as any).flush(); + const res = await POST('/odata/v4/programmatic/genericGetInstancesByBusinessKey', { + businessKey, + status, + }); + if (res.data.value?.length > 0) return res.data.value[0].id; + if (maxRetries <= 0) + throw new Error(`No instance found for businessKey: ${businessKey} with status: ${status}`); + await new Promise((r) => setTimeout(r, 1000)); + return waitForInstance(businessKey, status, maxRetries - 1); + } + + it('should emit an updateInstanceStatus event via imported process service', async () => { + const ID = generateID(); + await POST('/odata/v4/programmatic/genericStart', { + definitionId: 'eu12.cdsmunich.capprocesspluginhybridtest.programmatic_Lifecycle_Process', + businessKey: ID, + context: JSON.stringify({ ID }), + }); + + const instanceId = await waitForInstance(ID, ['RUNNING']); + + const response = await POST('/odata/v4/programmatic/updateInstanceStatusViaProcess', { + instanceId, + status: 'SUSPENDED', + }); + + expect(response.status).toBe(204); + + // Flush up to 3 times and poll: imported service outbox → ProcessService outbox → handler + async function waitForSuspended( + id: string, + businessKey: string, + maxRetries = 3, + ): Promise { + await (cds as any).flush(); + const res = await POST('/odata/v4/programmatic/genericGetInstancesByBusinessKey', { + businessKey, + status: ['SUSPENDED'], + }); + if (res.data.value.some((j: any) => j.id === id)) return true; + if (maxRetries <= 0) return false; + return waitForSuspended(id, businessKey, maxRetries - 1); + } + expect(await waitForSuspended(instanceId, ID)).toBe(true); + }); + }); });