Skip to content

feat: update workflow instances based on instance id #108

Draft
tilwbr wants to merge 10 commits into
mainfrom
107-update-workflow-instances-based-on-instance-id-not-business-key
Draft

feat: update workflow instances based on instance id #108
tilwbr wants to merge 10 commits into
mainfrom
107-update-workflow-instances-based-on-instance-id-not-business-key

Conversation

@tilwbr
Copy link
Copy Markdown
Contributor

@tilwbr tilwbr commented May 12, 2026

Add updateInstanceStatus Function to Workflow Process Services

New Feature

✨ Introduces an updateInstanceStatus function that allows callers to update the status of a workflow instance directly by its instance ID. The feature supports an optional cascade parameter and validates the provided status against known WorkflowStatus values, returning a structured UpdateStatusResult ({ id, success }).

Changes

  • lib/handlers/processService.ts: Registered the new updateInstanceStatusHandler handler, which validates instanceId and status parameters, checks against valid WorkflowStatus values, and delegates to PROCESS_SERVICE.
  • lib/processImport/csnBuilder.ts: Added UpdateStatusResult type definition (with id and success fields) and the updateInstanceStatus function signature to the generated CSN definitions for every imported process service.
  • srv/BTPProcessService.cds: Added the UpdateStatusResult type and updateInstanceStatus function declaration to the BTP process service definition.
  • srv/BTPProcessService.ts: Implemented the updateInstanceStatus handler in the BTP process service, delegating to workflowInstanceClient.updateWorkflowStatus.
  • srv/localProcessService.ts: Implemented the updateInstanceStatus handler in the local/mock process service, updating the local workflow store and returning appropriate 404 or success results.
  • tests/bookshop/srv/programmatic-service.cds: Added UpdateStatusResult type, genericUpdateInstanceStatus, and updateInstanceStatusViaProcess test actions to the bookshop programmatic service.
  • tests/bookshop/srv/programmatic-service.ts: Wired up genericUpdateInstanceStatus and updateInstanceStatusViaProcess handlers for integration testing.
  • tests/bookshop/srv/external/*.cds & tests/hybrid/importedCDS/*.cds: Updated all generated external CDS files with the new UpdateStatusResult type and updateInstanceStatus function, along with refreshed checksums.
  • tests/bookshop/srv/workflows/*.json: Updated test workflow JSON fixtures to align with the latest schema (field reordering, removed redundant title fields in type definitions).
  • tests/integration/genericProgrammaticApproach.test.ts: Added integration tests covering success, status reflection, invalid status (400), and non-existent instance (404) scenarios for updateInstanceStatus.
  • tests/integration/programmaticApproach.test.ts: Added an integration test verifying updateInstanceStatus invoked via an imported process service returns { id, success: true }.
  • 🔄 Regenerate and Update Summary
PR Bot Information

Version: 1.20.47

  • Output Template: Default Template
  • Summary Prompt: Default Prompt
  • Event Trigger: pull_request.opened
  • Correlation ID: 8da34664-1273-4437-86f2-e4f4ff1ee8b3
  • File Content Strategy: Full file content
  • LLM: anthropic--claude-4.6-sonnet

@tilwbr tilwbr linked an issue May 12, 2026 that may be closed by this pull request
Copy link
Copy Markdown
Contributor

@hyperspace-insights hyperspace-insights Bot left a comment

Choose a reason for hiding this comment

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

The PR introduces updateInstanceStatus across all service layers (BTP, local, process service handler) and adds corresponding CDS type/function definitions and tests. The most critical issue is the misuse of function instead of action for a state-mutating operation, which appears consistently in both srv/BTPProcessService.cds and lib/processImport/csnBuilder.ts (and consequently in all generated .cds fixture files). Additionally, the local service logs unvalidated input before performing validation, and the test suite has reliability gaps with unguarded array accesses and fail()-based assertions that can silently pass.

PR Bot Information

Version: 1.20.47

  • File Content Strategy: Full file content
  • Event Trigger: pull_request.opened
  • LLM: anthropic--claude-4.6-sonnet
  • Correlation ID: 8da34664-1273-4437-86f2-e4f4ff1ee8b3

Comment on lines +216 to +224
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` +
`==============================================================`,
);
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

Comment on lines +284 to +291
it('should return 404 for a non-existent instance ID', async () => {
try {
await updateInstanceStatus('non-existent-id', 'SUSPENDED');
fail('Expected request to be rejected');
} catch (error: any) {
expect(error.response.status).toBe(404);
}
});
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: Test for 404 does not start a process first, but updateInstanceStatus routes through genericUpdateInstanceStatusprocessService.send('updateInstanceStatus', ...) → the local service. Since the local service uses the in-memory localWorkflowStore, any prior test run that left state could affect this test; conversely, the store is never reset between tests. However, the larger issue is that fail(...) at line 288 (and line 278) is not reliable in all Jest configurations — if the POST unexpectedly resolves instead of throwing, fail may itself throw an unrecognised error rather than failing the assertion.

Should use Jest's expect.assertions(1) at the start of the test block to guarantee the catch branch is reached, rather than relying on a bare fail() call.

Suggested change
it('should return 404 for a non-existent instance ID', async () => {
try {
await updateInstanceStatus('non-existent-id', 'SUSPENDED');
fail('Expected request to be rejected');
} catch (error: any) {
expect(error.response.status).toBe(404);
}
});
it('should return 404 for a non-existent instance ID', async () => {
expect.assertions(1);
try {
await updateInstanceStatus('non-existent-id', 'SUSPENDED');
} catch (error: any) {
expect(error.response.status).toBe(404);
}
});

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

Comment on lines +276 to +282
try {
await updateInstanceStatus(instanceId, 'INVALID');
fail('Expected request to be rejected');
} catch (error: any) {
expect(error.response.status).toBe(400);
}
});
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: Same fail(...) reliability issue — if the POST call does not throw, the test silently passes without any assertion being evaluated.

Should use expect.assertions(1) to ensure the catch block is actually executed.

Suggested change
try {
await updateInstanceStatus(instanceId, 'INVALID');
fail('Expected request to be rejected');
} catch (error: any) {
expect(error.response.status).toBe(400);
}
});
expect.assertions(1);
try {
await updateInstanceStatus(instanceId, 'INVALID');
} catch (error: any) {
expect(error.response.status).toBe(400);
}

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

Comment on lines +226 to +230
async function getInstanceId(businessKey: string): Promise<string> {
const res = await POST('/odata/v4/programmatic/genericGetInstancesByBusinessKey', {
businessKey,
});
return res.data.value[0].id;
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.

Bug: getInstanceId accesses res.data.value[0].id without checking whether the array is non-empty. If the instance hasn't been flushed/stored yet (e.g., timing issues), res.data.value[0] will be undefined, causing a runtime error that masks the actual test failure.

Should add a guard or assert that the array is non-empty before accessing the first element.

Suggested change
async function getInstanceId(businessKey: string): Promise<string> {
const res = await POST('/odata/v4/programmatic/genericGetInstancesByBusinessKey', {
businessKey,
});
return res.data.value[0].id;
async function getInstanceId(businessKey: string): Promise<string> {
const res = await POST('/odata/v4/programmatic/genericGetInstancesByBusinessKey', {
businessKey,
});
expect(res.data.value.length).toBeGreaterThan(0);
return res.data.value[0].id;
}

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

Comment on lines +215 to +224
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 },
};
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

Comment thread srv/BTPProcessService.cds Outdated
Comment on lines +47 to +51
function updateInstanceStatus(
@mandatory instanceId : String(256),
@mandatory status : String(256),
cascade : Boolean
) returns UpdateStatusResult;
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: updateInstanceStatus modifies workflow state (it is not idempotent and not safe) but is declared as a CDS function. OData maps function to HTTP GET (safe/read-only) and action to HTTP POST (state-changing). Using function here is semantically incorrect and can cause incorrect caching behaviour.

Should be declared as action instead of function.

Suggested change
function updateInstanceStatus(
@mandatory instanceId : String(256),
@mandatory status : String(256),
cascade : Boolean
) returns UpdateStatusResult;
action updateInstanceStatus(
@mandatory instanceId : String(256),
@mandatory status : String(256),
cascade : Boolean
) returns UpdateStatusResult;

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

update workflow instances based on instance id, not business Key

1 participant