Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion lib/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
39 changes: 38 additions & 1 deletion lib/handlers/processService.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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 {
Expand Down Expand Up @@ -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}`);
});
}
13 changes: 11 additions & 2 deletions lib/handlers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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<void> {
Expand Down
10 changes: 10 additions & 0 deletions lib/processImport/csnBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
},
};
Comment on lines +204 to +212
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Best Practices: The updateInstanceStatus operation modifies server-side state (it changes the status of a workflow instance) but is declared as a CDS function. In CDS/OData semantics, function maps to an HTTP GET (safe, read-only), while action maps to an HTTP POST (non-safe, state-changing). Declaring a state-mutating operation as a function violates OData protocol semantics and may cause caching or proxy issues.

Should use kind: 'action' instead of kind: 'function'.

Suggested change
definitions[fqn(serviceName, 'updateInstanceStatus')] = {
kind: 'function',
name: fqn(serviceName, 'updateInstanceStatus'),
params: {
instanceId: { type: csn.CdsBuiltinType.String, notNull: true },
status: { type: csn.CdsBuiltinType.String, notNull: true },
cascade: { type: csn.CdsBuiltinType.Boolean },
},
returns: { type: updateStatusResultType },
};
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 },
},
returns: { type: updateStatusResultType },
};

Double-check suggestion before committing. Edit this comment for amendments.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful


// Lifecycle actions
for (const action of ['suspend', 'resume', 'cancel']) {
definitions[fqn(serviceName, action)] = {
Expand Down
7 changes: 6 additions & 1 deletion lib/types/csn-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
//
// ──────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -132,6 +133,10 @@ export interface CsnFunction extends CsnBaseDefinition {
params?: Record<string, CsnElement>;
returns?: CsnType | CsnElement;
}
export interface CsnEvent extends CsnBaseDefinition {
kind: 'event';
elements?: Record<string, CsnElement>;
}
//
// ──────────────────────────────────────────────────────────────
// ANNOTATION
Expand Down
7 changes: 7 additions & 0 deletions srv/BTPProcessService.cds
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -40,3 +46,4 @@ service ProcessService {
status : many String(256)
)returns InstancesReturn;
}

10 changes: 10 additions & 0 deletions srv/BTPProcessService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
20 changes: 20 additions & 0 deletions srv/localProcessService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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` +
`==============================================================`,
);
Comment on lines +216 to +224
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic Error: Validation occurs after data is logged, allowing unsanitized/invalid data to be logged before input checks

LOG.info and LOG.debug at lines 218–224 log instanceId and status before the null-checks at lines 226–238 are performed. If instanceId or status are missing or contain malicious content, they will be emitted to logs before being rejected.

Consider moving the validation block above the log statements.

Suggested change
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` +
`==============================================================`,
);
this.on('updateInstanceStatus', async (req: cds.Request) => {
const { instanceId, status } = 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 as WorkflowStatus)) {
return req.reject({
status: 400,
message: `Invalid status: ${status}. Valid values are: ${validStatuses.join(', ')}`,
});
}
LOG.info('Updating instance status', instanceId, '->', status);
LOG.debug(
`==============================================================\n` +
`Update instance status for ${instanceId} to ${status}\n` +
`==============================================================`,
);

Double-check suggestion before committing. Edit this comment for amendments.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful


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();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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. */
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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. */
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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. */
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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. */
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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. */
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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. */
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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. */
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions tests/bookshop/srv/programmatic-service.cds
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Loading
Loading