Skip to content
Merged
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
2 changes: 1 addition & 1 deletion lib/handlers/processActionHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
}

Expand Down
44 changes: 37 additions & 7 deletions lib/handlers/processService.ts
Original file line number Diff line number Diff line change
@@ -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);

Expand Down Expand Up @@ -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}`);
});
}

Expand All @@ -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}`);
});
Expand All @@ -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}`);
});
Expand All @@ -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}`);
});
Expand All @@ -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;
});
Expand Down
1 change: 0 additions & 1 deletion lib/handlers/processStart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!,
);
}

Expand Down
5 changes: 2 additions & 3 deletions lib/handlers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,14 +212,13 @@ export async function emitProcessEvent(
req: cds.Request,
payload: ProcessStartPayload | ProcessLifecyclePayload,
processEventFailedMsg: string,
msgArgs: string,
): Promise<void> {
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 });
}
}
9 changes: 9 additions & 0 deletions lib/processImport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -269,6 +270,12 @@ function addProcessTypes(
name: instancesName,
items: { type: instanceName },
};

definitions[statusName] = {
kind: 'type',
name: statusName,
items: { type: csn.CdsBuiltinType.String },
};
}

// ============================================================================
Expand All @@ -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')] = {
Expand Down Expand Up @@ -317,6 +325,7 @@ function addProcessActions(
name: fqn(serviceName, 'getInstancesByBusinessKey'),
params: {
businessKey: { type: csn.CdsBuiltinType.String, notNull: true },
status: { type: statusType },
},
returns: { type: instancesType },
};
Expand Down
3 changes: 2 additions & 1 deletion srv/BTPProcessService.cds
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ service ProcessService {
)returns AnyType;

function getInstancesByBusinessKey(
@mandatory businessKey : String(256)
@mandatory businessKey : String(256),
status : many String(256)
)returns InstancesReturn;
}
19 changes: 13 additions & 6 deletions srv/BTPProcessService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});

Expand Down
12 changes: 11 additions & 1 deletion srv/localProcessService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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` +
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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. */
Expand Down Expand Up @@ -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;
};
Expand All @@ -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(
Expand Down
3 changes: 3 additions & 0 deletions tests/bookshop/srv/shipment-service.cds
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
15 changes: 14 additions & 1 deletion tests/bookshop/srv/shipment-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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();
}
}
Expand Down
60 changes: 60 additions & 0 deletions tests/integration/programmaticApproach.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down