diff --git a/CHANGELOG.md b/CHANGELOG.md index de45ef46..13acb949 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ - The format is based on [Keep a Changelog](https://keepachangelog.com/). - This project adheres to [Semantic Versioning](https://semver.org/). +## Version 0.3.0 - 2026-05-11 + +### Added + +- `getInstances` function replacing `getInstancesByBusinessKey` — supports filtering by all SBPA workflow instance query parameters: `id`, `businessKey`, `status`, `definitionId`, `definitionVersion`, `startedAt`, `startedFrom`, `startedUpTo`, `completedAt`, `completedFrom`, `completedUpTo`, `startedBy`, `subject`, `containsText`, `rootInstanceId`, `parentInstanceId`, `orderBy`, `top`, `skip`, `inlinecount` + +### Changed + +- `getInstancesByBusinessKey` is replaced by `getInstances` in both the specific process services and the generic `ProcessService` + ## Version 0.2.1 - 2026-04-20 ### Fixed diff --git a/README.md b/README.md index 0ed362a6..f16e9309 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,7 @@ CAP Plugin to interact with SAP Build Process Automation to manage processes. - [What Gets Generated](#what-gets-generated) - [Starting a Process](#starting-a-process) - [Suspending, Resuming, and Cancelling a Process](#suspending-resuming-and-cancelling-a-process) - - [Querying Process Instances](#querying-process-instances) - - [Limitations](#limitations) + - [Querying Process Instances](#querying-process-instances) - [Limitations](#limitations) - [Generic ProcessService](#generic-processservice) - [Service Definition](#service-definition) - [Usage](#usage) @@ -567,7 +566,7 @@ The import generates: - A CDS service definition in `./srv/external/` (annotated with `@bpm.process` and `@protocol: 'none'`) - Typed `ProcessInputs`, `ProcessOutputs`, `ProcessAttribute`, and `ProcessInstance` types based on the process definition - Typed actions: `start`, `suspend`, `resume`, `cancel` -- Typed functions: `getAttributes`, `getOutputs`, `getInstancesByBusinessKey` +- Typed functions: `getAttributes`, `getOutputs`, `getInstances` - A process definition JSON in `./srv/workflows/` After importing, run `cds-typer` to generate TypeScript types for the imported service. @@ -609,11 +608,20 @@ The `cascade` parameter is optional and defaults to `false`. When set to `true`, ```typescript // Get all instances matching a business key, optionally filtered by status -const instances = await processService.getInstancesByBusinessKey({ +const instances = await processService.getInstances({ businessKey: 'order-12345', status: ['RUNNING', 'SUSPENDED'], }); +// Filter by additional parameters +const instances = await processService.getInstances({ + definitionId: 'eu12.myorg.myproject.myProcess', + startedFrom: '2024-01-01T00:00:00Z', + orderBy: 'startedAt desc', + top: 10, + skip: 0, +}); + // Get attributes for a specific process instance const attributes = await processService.getAttributes({ processInstanceId: 'instance-uuid', @@ -625,8 +633,28 @@ const outputs = await processService.getOutputs({ }); ``` -Valid status values are: `RUNNING`, `SUSPENDED`, `CANCELLED`, `ERRONEOUS`, `COMPLETED`. -If no status filter is provided, all statuses except `CANCELLED` are returned. +All parameters are optional. Supported filter parameters: + +| Parameter | Type | Description | +| ------------------- | --------------- | ------------------------------------------------------------------------------- | +| `id` | `String` | Filter by workflow instance ID | +| `businessKey` | `String` | Filter by business key | +| `status` | `Array` | Filter by status (`RUNNING`, `SUSPENDED`, `CANCELED`, `ERRONEOUS`, `COMPLETED`) | +| `definitionId` | `String` | Filter by process definition ID | +| `definitionVersion` | `String` | Filter by process definition version | +| `startedFrom` | `Timestamp` | Filter instances started on or after this date | +| `startedUpTo` | `Timestamp` | Filter instances started on or before this date | +| `completedFrom` | `Timestamp` | Filter instances completed on or after this date | +| `completedUpTo` | `Timestamp` | Filter instances completed on or before this date | +| `startedBy` | `String` | Filter by the user who started the instance | +| `subject` | `String` | Filter by subject | +| `containsText` | `String` | Full-text search across instance fields | +| `rootInstanceId` | `String` | Filter by root instance ID | +| `parentInstanceId` | `String` | Filter by parent instance ID | +| `orderBy` | `String` | Sort order (e.g. `startedAt desc`, `businessKey asc`) | +| `top` | `Integer` | Maximum number of results to return | +| `skip` | `Integer` | Number of results to skip (for pagination) | +| `inlinecount` | `String` | Set to `allpages` to include total count in response | #### Limitations @@ -642,15 +670,15 @@ The generic `ProcessService` allows setting the business key to mimic the behavi The generic `ProcessService` defines the following events and functions: -| Operation | Type | Description | -| --------------------------- | -------- | ----------------------------------------------------------------- | -| `start` | event | Start a workflow instance with a `definitionId` and `context` | -| `cancel` | event | Cancel all running/suspended instances matching a `businessKey` | -| `suspend` | event | Suspend all running instances matching a `businessKey` | -| `resume` | event | Resume all suspended instances matching a `businessKey` | -| `getAttributes` | function | Retrieve attributes for a specific process instance | -| `getOutputs` | function | Retrieve outputs for a specific process instance | -| `getInstancesByBusinessKey` | function | Find process instances by business key and optional status filter | +| Operation | Type | Description | +| --------------- | -------- | --------------------------------------------------------------- | +| `start` | event | Start a workflow instance with a `definitionId` and `context` | +| `cancel` | event | Cancel all running/suspended instances matching a `businessKey` | +| `suspend` | event | Suspend all running instances matching a `businessKey` | +| `resume` | event | Resume all suspended instances matching a `businessKey` | +| `getAttributes` | function | Retrieve attributes for a specific process instance | +| `getOutputs` | function | Retrieve outputs for a specific process instance | +| `getInstances` | function | Query process instances with flexible filter parameters | #### Usage @@ -688,12 +716,20 @@ await processService.emit('resume', { cascade: false, }); -// Query instances by business key -const instances = await processService.send('getInstancesByBusinessKey', { +// Query instances with flexible filters +const instances = await processService.send('getInstances', { businessKey: 'order-12345', status: ['RUNNING', 'SUSPENDED'], }); +// Query with additional params +const instances = await processService.send('getInstances', { + definitionId: 'eu12.myorg.myproject.myProcess', + startedFrom: '2024-01-01T00:00:00Z', + orderBy: 'startedAt desc', + top: 10, +}); + // Get attributes of a specific instance const attributes = await processService.send('getAttributes', { processInstanceId: 'instance-uuid', @@ -705,7 +741,7 @@ const outputs = await processService.send('getOutputs', { }); ``` -> **Note:** The generic ProcessService uses `emit` for lifecycle events (start, cancel, suspend, resume) which are processed asynchronously through the CDS outbox, and `send` for query functions (getAttributes, getOutputs, getInstancesByBusinessKey) which return data synchronously. +> **Note:** The generic ProcessService uses `emit` for lifecycle events (start, cancel, suspend, resume) which are processed asynchronously through the CDS outbox, and `send` for query functions (getAttributes, getOutputs, getInstances) which return data synchronously. > Make sure to check whether the outbox is correctly used. If not, refer to cds.queued to make sure it is used. ## Build-Time Validation diff --git a/lib/api/index.ts b/lib/api/index.ts index c44043eb..908d7a80 100644 --- a/lib/api/index.ts +++ b/lib/api/index.ts @@ -18,13 +18,19 @@ export { IWorkflowInstanceClient, WorkflowInstance, WorkflowStatus, + GetInstancesParams, StartWorkflowResult, UpdateStatusResult, + INSTANCES_PARAMS_SKIP_KEYS, + INSTANCES_PARAM_KEY_MAP, createWorkflowInstanceClient, startWorkflow, getWorkflowsByBusinessKey, + getInstances, updateWorkflowStatus, updateMultipleWorkflowStatus, + getAttributes, + getOutputs, } from './workflow-client'; // Local Workflow Store - for local development diff --git a/lib/api/local-workflow-store.ts b/lib/api/local-workflow-store.ts index 22e95ff8..abdbf43a 100644 --- a/lib/api/local-workflow-store.ts +++ b/lib/api/local-workflow-store.ts @@ -1,6 +1,8 @@ import { WorkflowStatus, WorkflowInstance, + GetInstancesParams, + INSTANCES_PARAMS_SKIP_KEYS, StartWorkflowResult, UpdateStatusResult, } from './workflow-client'; @@ -75,6 +77,82 @@ export class LocalWorkflowStore { return filtered; } + getInstances(params: GetInstancesParams): LocalWorkflowInstance[] { + const specialKeys = new Set([ + ...INSTANCES_PARAMS_SKIP_KEYS, + 'startedFrom', + 'startedUpTo', + 'completedFrom', + 'completedUpTo', + 'containsText', + 'rootInstanceId', + 'parentInstanceId', + 'skip', + 'top', + 'orderBy', + 'inlinecount', + ]); + + let filteredInstances = [...this.instances]; + + for (const [key, value] of Object.entries(params)) { + if (value == null || specialKeys.has(key)) continue; + filteredInstances = filteredInstances.filter( + (i) => i[key as keyof LocalWorkflowInstance] === value, + ); + } + + if (params.status && params.status.length > 0) { + filteredInstances = filteredInstances.filter((i) => params.status!.includes(i.status)); + } + + if (params.startedFrom != null) { + const from = new Date(params.startedFrom); + filteredInstances = filteredInstances.filter( + (i) => i.startedAt != null && new Date(i.startedAt) >= from, + ); + } + if (params.startedUpTo != null) { + const upTo = new Date(params.startedUpTo); + filteredInstances = filteredInstances.filter( + (i) => i.startedAt != null && new Date(i.startedAt) <= upTo, + ); + } + if (params.completedFrom != null) { + const from = new Date(params.completedFrom); + filteredInstances = filteredInstances.filter( + (i) => i.completedAt != null && new Date(i.completedAt) >= from, + ); + } + if (params.completedUpTo != null) { + const upTo = new Date(params.completedUpTo); + filteredInstances = filteredInstances.filter( + (i) => i.completedAt != null && new Date(i.completedAt) <= upTo, + ); + } + + if (params.containsText != null) { + const text = params.containsText.toLowerCase(); + filteredInstances = filteredInstances.filter( + (i) => + i.id.toLowerCase().includes(text) || + i.subject?.toLowerCase().includes(text) || + i.businessKey?.toLowerCase().includes(text), + ); + } + + if (params.rootInstanceId != null) + filteredInstances = filteredInstances.filter((i) => i.id === params.rootInstanceId); + if (params.parentInstanceId != null) + filteredInstances = filteredInstances.filter((i) => i.id === params.parentInstanceId); + + const skip = params.skip ?? 0; + const top = params.top ?? filteredInstances.length; + filteredInstances = filteredInstances.slice(skip, skip + top); + + return filteredInstances; + } + getInstance(instanceId: string): LocalWorkflowInstance | undefined { return this.instances.find((i) => i.id === instanceId); } diff --git a/lib/api/workflow-client.ts b/lib/api/workflow-client.ts index 50d442ad..84df3f7a 100644 --- a/lib/api/workflow-client.ts +++ b/lib/api/workflow-client.ts @@ -4,6 +4,17 @@ import { PROCESS_LOGGER_PREFIX } from '../constants'; const LOG = cds.log(PROCESS_LOGGER_PREFIX); const BASE_PATH = '/public/workflow/rest'; +// Keys in GetInstancesParams that need special handling and are not direct API query params +export const INSTANCES_PARAMS_SKIP_KEYS = new Set(['status']); + +// Remap camelCase param keys to the API's expected query param names +export const INSTANCES_PARAM_KEY_MAP: Partial> = { + orderBy: '$orderby', + top: '$top', + skip: '$skip', + inlinecount: '$inlinecount', +}; + // ============ Types & Enums ============ export enum WorkflowStatus { @@ -19,6 +30,34 @@ export interface WorkflowInstance { businessKey?: string; status: WorkflowStatus; definitionId?: string; + definitionVersion?: string; + startedAt?: string; + completedAt?: string; + startedBy?: string; + subject?: string; +} + +export interface GetInstancesParams { + id?: string | null; + businessKey?: string | null; + status?: WorkflowStatus[]; + definitionId?: string | null; + definitionVersion?: string | null; + startedAt?: string | null; + startedFrom?: string | null; + startedUpTo?: string | null; + completedAt?: string | null; + completedFrom?: string | null; + completedUpTo?: string | null; + startedBy?: string | null; + subject?: string | null; + containsText?: string | null; + rootInstanceId?: string | null; + parentInstanceId?: string | null; + orderBy?: string | null; + top?: number | null; + skip?: number | null; + inlinecount?: string | null; } export interface StartWorkflowResult { @@ -41,6 +80,8 @@ export interface IWorkflowInstanceClient { status: WorkflowStatus[], ): Promise; + getInstances(params: GetInstancesParams): Promise; + updateWorkflowStatus( instanceId: string, status: WorkflowStatus, @@ -120,6 +161,44 @@ export async function getWorkflowsByBusinessKey( return await res.json(); } +export async function getInstances( + serviceUrl: string, + jwt: string, + params: GetInstancesParams, +): Promise { + const queryParts: string[] = []; + + for (const [key, value] of Object.entries(params)) { + if (value == null || INSTANCES_PARAMS_SKIP_KEYS.has(key as keyof GetInstancesParams)) continue; + const apiKey = INSTANCES_PARAM_KEY_MAP[key as keyof GetInstancesParams] ?? key; + queryParts.push(`${apiKey}=${encodeURIComponent(String(value))}`); + } + + for (const s of params.status ?? []) { + queryParts.push(`status=${encodeURIComponent(s)}`); + } + + const queryString = queryParts.join('&'); + const queryUrl = `${serviceUrl}${BASE_PATH}/v1/workflow-instances${queryString ? '?' + queryString : ''}`; + LOG.debug('Invoking url: ' + queryUrl); + + const res = await fetch(queryUrl, { + method: 'GET', + headers: { + Authorization: `Bearer ${jwt}`, + 'Content-Type': 'application/json', + }, + }); + + if (!res.ok) { + const body = await res.text(); + const errorMessage = `Failed to retrieve workflow instances: ${body || res.statusText || 'Unknown error'}`; + throw cds.error(res.status, errorMessage); + } + + return await res.json(); +} + export async function updateWorkflowStatus( serviceUrl: string, jwt: string, @@ -256,6 +335,11 @@ export function createWorkflowInstanceClient( return getWorkflowsByBusinessKey(serviceUrl, jwt, businessKey, status); }, + getInstances: async (params) => { + const jwt = await getToken(); + return getInstances(serviceUrl, jwt, params); + }, + updateWorkflowStatus: async (instanceId, status, cascade) => { const jwt = await getToken(); return updateWorkflowStatus(serviceUrl, jwt, instanceId, status, cascade); diff --git a/lib/handlers/processService.ts b/lib/handlers/processService.ts index 597659e4..3b381321 100644 --- a/lib/handlers/processService.ts +++ b/lib/handlers/processService.ts @@ -1,7 +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'; +import { WorkflowStatus, GetInstancesParams } from '../api'; const LOG = cds.log(PROCESS_LOGGER_PREFIX); @@ -22,7 +22,7 @@ export function registerProcessServiceHandlers(service: cds.Service): void { registerSuspendHandler(service, definitionId); registerResumeHandler(service, definitionId); registerCancelHandler(service, definitionId); - registerGetInstancesByBusinessKeyHandler(service, definitionId); + registerGetInstancesHandler(service, definitionId); registerGetAttributesHandler(service, definitionId); registerGetOutputsHandler(service, definitionId); } @@ -107,21 +107,16 @@ function registerCancelHandler(service: cds.Service, definitionId: string): void }); } -function registerGetInstancesByBusinessKeyHandler( - service: cds.Service, - definitionId: string, -): void { - service.on('getInstancesByBusinessKey', async (req) => { - LOG.debug(`Getting instances by businessKey for process: ${definitionId}`); +function registerGetInstancesHandler(service: cds.Service, definitionId: string): void { + service.on('getInstances', async (req) => { + LOG.debug(`Getting instances for process: ${definitionId}`); - const { businessKey, status } = req.data; - if (!businessKey) { - return req.reject({ status: 400, message: 'Missing required parameter: businessKey' }); - } - if (status) { + const params = req.data as GetInstancesParams; + + if (params.status) { const validStatuses = Object.values(WorkflowStatus); - const statuses = Array.isArray(status) ? status : [status]; - const invalidStatuses = statuses.filter((s) => !validStatuses.includes(s)); + const statuses = Array.isArray(params.status) ? params.status : [params.status]; + const invalidStatuses = statuses.filter((s) => !validStatuses.includes(s as WorkflowStatus)); if (invalidStatuses.length > 0) { return req.reject({ status: 400, @@ -131,10 +126,7 @@ function registerGetInstancesByBusinessKeyHandler( } const processService = await cds.connect.to(PROCESS_SERVICE); - const result = await processService.send('getInstancesByBusinessKey', { - businessKey, - status, - }); + const result = await processService.send('getInstances', params); return result; }); diff --git a/lib/processImport/csnBuilder.ts b/lib/processImport/csnBuilder.ts index a700aabd..82ee4dd4 100644 --- a/lib/processImport/csnBuilder.ts +++ b/lib/processImport/csnBuilder.ts @@ -123,12 +123,15 @@ function addProcessTypes( kind: 'type', name: instanceName, elements: { + id: { type: csn.CdsBuiltinType.String }, definitionId: { type: csn.CdsBuiltinType.String }, definitionVersion: { type: csn.CdsBuiltinType.String }, - id: { type: csn.CdsBuiltinType.String }, status: { type: csn.CdsBuiltinType.String }, - startedAt: { type: csn.CdsBuiltinType.String }, + startedAt: { type: csn.CdsBuiltinType.Timestamp }, + completedAt: { type: csn.CdsBuiltinType.Timestamp }, startedBy: { type: csn.CdsBuiltinType.String }, + subject: { type: csn.CdsBuiltinType.String }, + businessKey: { type: csn.CdsBuiltinType.String }, }, }; @@ -191,12 +194,30 @@ function addProcessActions( returns: { type: outputsType }, }; - definitions[fqn(serviceName, 'getInstancesByBusinessKey')] = { + definitions[fqn(serviceName, 'getInstances')] = { kind: 'function', - name: fqn(serviceName, 'getInstancesByBusinessKey'), + name: fqn(serviceName, 'getInstances'), params: { - businessKey: { type: csn.CdsBuiltinType.String, notNull: true }, + id: { type: csn.CdsBuiltinType.String }, + businessKey: { type: csn.CdsBuiltinType.String }, status: { items: { type: csn.CdsBuiltinType.String } }, + definitionId: { type: csn.CdsBuiltinType.String }, + definitionVersion: { type: csn.CdsBuiltinType.String }, + startedAt: { type: csn.CdsBuiltinType.Timestamp }, + startedFrom: { type: csn.CdsBuiltinType.Timestamp }, + startedUpTo: { type: csn.CdsBuiltinType.Timestamp }, + completedAt: { type: csn.CdsBuiltinType.Timestamp }, + completedFrom: { type: csn.CdsBuiltinType.Timestamp }, + completedUpTo: { type: csn.CdsBuiltinType.Timestamp }, + startedBy: { type: csn.CdsBuiltinType.String }, + subject: { type: csn.CdsBuiltinType.String }, + containsText: { type: csn.CdsBuiltinType.String }, + rootInstanceId: { type: csn.CdsBuiltinType.String }, + parentInstanceId: { type: csn.CdsBuiltinType.String }, + top: { type: csn.CdsBuiltinType.Integer }, + skip: { type: csn.CdsBuiltinType.Integer }, + orderBy: { type: csn.CdsBuiltinType.String }, + inlinecount: { type: csn.CdsBuiltinType.String }, }, returns: { type: instancesType }, }; diff --git a/srv/BTPProcessService.cds b/srv/BTPProcessService.cds index 6e066e46..a1306368 100644 --- a/srv/BTPProcessService.cds +++ b/srv/BTPProcessService.cds @@ -35,8 +35,26 @@ service ProcessService { @mandatory processInstanceId : String(256) )returns AnyType; - function getInstancesByBusinessKey( - @mandatory businessKey : String(256), - status : many String(256) + function getInstances( + id : String(256), + businessKey : String(256), + status : many String(256), + definitionId : String(256), + definitionVersion : String(256), + startedAt : Timestamp, + startedFrom : Timestamp, + startedUpTo : Timestamp, + completedAt : Timestamp, + completedFrom : Timestamp, + completedUpTo : Timestamp, + startedBy : String(256), + subject : String(256), + containsText : String(256), + rootInstanceId : String(256), + parentInstanceId : String(256), + top : Integer, + skip : Integer, + orderBy : String(256), + inlinecount : String(256) )returns InstancesReturn; } diff --git a/srv/BTPProcessService.ts b/srv/BTPProcessService.ts index e3f05af1..a6a51047 100644 --- a/srv/BTPProcessService.ts +++ b/srv/BTPProcessService.ts @@ -1,6 +1,11 @@ import cds from '@sap/cds'; import { getServiceCredentials, CachingTokenProvider, createXsuaaTokenProvider } from '../lib/auth'; -import { IWorkflowInstanceClient, createWorkflowInstanceClient, WorkflowStatus } from '../lib/api'; +import { + IWorkflowInstanceClient, + createWorkflowInstanceClient, + WorkflowStatus, + GetInstancesParams, +} from '../lib/api'; import { PROCESS_LOGGER_PREFIX, PROCESS_SERVICE } from '../lib'; const LOG = cds.log(PROCESS_LOGGER_PREFIX); @@ -87,28 +92,11 @@ class ProcessService extends cds.ApplicationService { ); }); - this.on('getInstancesByBusinessKey', async (request: cds.Request) => { - const { businessKey } = request.data; - let { status } = request.data; - LOG.info('Getting instances for', businessKey); + this.on('getInstances', async (request: cds.Request) => { + const params = request.data as GetInstancesParams; + LOG.info('Getting instances'); - if (!businessKey) { - return request.reject({ status: 400, message: 'Missing required parameter: businessKey' }); - } - - if (!status) { - status = [ - WorkflowStatus.RUNNING, - WorkflowStatus.SUSPENDED, - WorkflowStatus.COMPLETED, - WorkflowStatus.ERRONEOUS, - ]; - } - - const instances = await this.workflowInstanceClient.getWorkflowsByBusinessKey( - businessKey, - status, - ); + const instances = await this.workflowInstanceClient.getInstances(params); return instances; }); diff --git a/srv/localProcessService.ts b/srv/localProcessService.ts index 7148dabe..1f5f93c5 100644 --- a/srv/localProcessService.ts +++ b/srv/localProcessService.ts @@ -1,6 +1,6 @@ import cds from '@sap/cds'; import { localWorkflowStore } from '../lib/api/local-workflow-store'; -import { WorkflowStatus } from '../lib/api/workflow-client'; +import { WorkflowStatus, GetInstancesParams } from '../lib/api/workflow-client'; import { PROCESS_LOGGER_PREFIX } from '../lib'; const LOG = cds.log(PROCESS_LOGGER_PREFIX); @@ -127,33 +127,19 @@ class ProcessService extends cds.ApplicationService { return; }); - this.on('getInstancesByBusinessKey', async (req: cds.Request) => { - const { businessKey } = req.data; - let { status } = req.data; - LOG.info('Getting instances for', businessKey); + this.on('getInstances', async (req: cds.Request) => { + const params = req.data as GetInstancesParams; + LOG.info('Getting instances'); LOG.debug( `==============================================================\n` + - `Get instances by businessKey: ${businessKey}\n` + + `Get instances\n` + `==============================================================`, ); - if (!businessKey) { - return req.reject({ status: 400, message: 'Missing required parameter: businessKey' }); - } - - if (!status) { - status = [ - WorkflowStatus.RUNNING, - WorkflowStatus.SUSPENDED, - WorkflowStatus.COMPLETED, - WorkflowStatus.ERRONEOUS, - ]; - } - - const instances = localWorkflowStore.getInstancesByBusinessKey(businessKey, status); + const instances = localWorkflowStore.getInstances(params); - LOG.debug(`Found ${instances.length} workflow instance(s) for businessKey: ${businessKey}`); + LOG.debug(`Found ${instances.length} workflow instance(s)`); return instances; }); diff --git a/tests/bookshop/srv/annotation-hybrid-service.cds b/tests/bookshop/srv/annotation-hybrid-service.cds index 861fbbc9..4b16b2e1 100644 --- a/tests/bookshop/srv/annotation-hybrid-service.cds +++ b/tests/bookshop/srv/annotation-hybrid-service.cds @@ -70,7 +70,7 @@ service AnnotationHybridService { year : Integer; } - action getInstancesByBusinessKey(ID: String, - status: many String) returns many ProcessInstance; + action getInstances(ID: String, + status: many String) returns many ProcessInstance; } diff --git a/tests/bookshop/srv/annotation-hybrid-service.ts b/tests/bookshop/srv/annotation-hybrid-service.ts index bb92c04a..f67d6b11 100644 --- a/tests/bookshop/srv/annotation-hybrid-service.ts +++ b/tests/bookshop/srv/annotation-hybrid-service.ts @@ -5,9 +5,9 @@ class AnnotationHybridService extends cds.ApplicationService { async init() { const annotationLifecycleProcess = await cds.connect.to(Annotation_Lifecycle_ProcessService); - this.on('getInstancesByBusinessKey', async (req: cds.Request) => { + this.on('getInstances', async (req: cds.Request) => { const { ID, status } = req.data; - const instances = await annotationLifecycleProcess.getInstancesByBusinessKey({ + const instances = await annotationLifecycleProcess.getInstances({ businessKey: ID, status: status, }); 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..3d877d9d 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 : faa9a4eb5a9c2d28e4c44f34f3faaa20 */ namespace eu12.cdsmunich.capprocesspluginhybridtest; /** DO NOT EDIT. THIS IS A GENERATED SERVICE THAT WILL BE OVERRIDDEN ON NEXT IMPORT. */ @@ -21,12 +21,15 @@ service Annotation_Lifecycle_ProcessService { type ProcessAttributes : many ProcessAttribute; type ProcessInstance { + id : String; definitionId : String; definitionVersion : String; - id : String; status : String; - startedAt : String; + startedAt : Timestamp; + completedAt : Timestamp; startedBy : String; + subject : String; + businessKey : String; }; type ProcessInstances : many ProcessInstance; @@ -43,9 +46,27 @@ service Annotation_Lifecycle_ProcessService { processInstanceId : String not null ) returns ProcessOutputs; - function getInstancesByBusinessKey( - businessKey : String not null, - status : many String + function getInstances( + id : String, + businessKey : String, + status : many String, + definitionId : String, + definitionVersion : String, + startedAt : Timestamp, + startedFrom : Timestamp, + startedUpTo : Timestamp, + completedAt : Timestamp, + completedFrom : Timestamp, + completedUpTo : Timestamp, + startedBy : String, + subject : String, + containsText : String, + rootInstanceId : String, + parentInstanceId : String, + top : Integer, + skip : Integer, + orderBy : String, + inlinecount : String ) returns ProcessInstances; action suspend( 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..6f39a2a8 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 : 8b3ac902513493de35fb2054ebe2f4bf */ namespace eu12.cdsmunich.capprocesspluginhybridtest; /** DO NOT EDIT. THIS IS A GENERATED SERVICE THAT WILL BE OVERRIDDEN ON NEXT IMPORT. */ @@ -21,12 +21,15 @@ service Annotation_Lifecycle_Process_TwoService { type ProcessAttributes : many ProcessAttribute; type ProcessInstance { + id : String; definitionId : String; definitionVersion : String; - id : String; status : String; - startedAt : String; + startedAt : Timestamp; + completedAt : Timestamp; startedBy : String; + subject : String; + businessKey : String; }; type ProcessInstances : many ProcessInstance; @@ -43,9 +46,27 @@ service Annotation_Lifecycle_Process_TwoService { processInstanceId : String not null ) returns ProcessOutputs; - function getInstancesByBusinessKey( - businessKey : String not null, - status : many String + function getInstances( + id : String, + businessKey : String, + status : many String, + definitionId : String, + definitionVersion : String, + startedAt : Timestamp, + startedFrom : Timestamp, + startedUpTo : Timestamp, + completedAt : Timestamp, + completedFrom : Timestamp, + completedUpTo : Timestamp, + startedBy : String, + subject : String, + containsText : String, + rootInstanceId : String, + parentInstanceId : String, + top : Integer, + skip : Integer, + orderBy : String, + inlinecount : String ) returns ProcessInstances; action suspend( 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..3fca6d2c 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 : 99dc8a7a72e25f64ab1b6bdec8fbafa8 */ namespace eu12.cdsmunich.capprocesspluginhybridtest; /** DO NOT EDIT. THIS IS A GENERATED SERVICE THAT WILL BE OVERRIDDEN ON NEXT IMPORT. */ @@ -57,12 +57,15 @@ service ImportProcess_Attributes_And_OutputsService { type ProcessAttributes : many ProcessAttribute; type ProcessInstance { + id : String; definitionId : String; definitionVersion : String; - id : String; status : String; - startedAt : String; + startedAt : Timestamp; + completedAt : Timestamp; startedBy : String; + subject : String; + businessKey : String; }; type ProcessInstances : many ProcessInstance; @@ -79,9 +82,27 @@ service ImportProcess_Attributes_And_OutputsService { processInstanceId : String not null ) returns ProcessOutputs; - function getInstancesByBusinessKey( - businessKey : String not null, - status : many String + function getInstances( + id : String, + businessKey : String, + status : many String, + definitionId : String, + definitionVersion : String, + startedAt : Timestamp, + startedFrom : Timestamp, + startedUpTo : Timestamp, + completedAt : Timestamp, + completedFrom : Timestamp, + completedUpTo : Timestamp, + startedBy : String, + subject : String, + containsText : String, + rootInstanceId : String, + parentInstanceId : String, + top : Integer, + skip : Integer, + orderBy : String, + inlinecount : String ) returns ProcessInstances; action suspend( 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..022b5d13 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 : 679467542ca0d729a5f5f51f59061cbd */ namespace eu12.cdsmunich.capprocesspluginhybridtest; /** DO NOT EDIT. THIS IS A GENERATED SERVICE THAT WILL BE OVERRIDDEN ON NEXT IMPORT. */ @@ -49,12 +49,15 @@ service ImportProcess_Complex_InputsService { type ProcessAttributes : many ProcessAttribute; type ProcessInstance { + id : String; definitionId : String; definitionVersion : String; - id : String; status : String; - startedAt : String; + startedAt : Timestamp; + completedAt : Timestamp; startedBy : String; + subject : String; + businessKey : String; }; type ProcessInstances : many ProcessInstance; @@ -71,9 +74,27 @@ service ImportProcess_Complex_InputsService { processInstanceId : String not null ) returns ProcessOutputs; - function getInstancesByBusinessKey( - businessKey : String not null, - status : many String + function getInstances( + id : String, + businessKey : String, + status : many String, + definitionId : String, + definitionVersion : String, + startedAt : Timestamp, + startedFrom : Timestamp, + startedUpTo : Timestamp, + completedAt : Timestamp, + completedFrom : Timestamp, + completedUpTo : Timestamp, + startedBy : String, + subject : String, + containsText : String, + rootInstanceId : String, + parentInstanceId : String, + top : Integer, + skip : Integer, + orderBy : String, + inlinecount : String ) returns ProcessInstances; action suspend( 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..af469a44 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 : 2a382c5f1ed621b369d90ef1ccdcc4d6 */ namespace eu12.cdsmunich.capprocesspluginhybridtest; /** DO NOT EDIT. THIS IS A GENERATED SERVICE THAT WILL BE OVERRIDDEN ON NEXT IMPORT. */ @@ -26,12 +26,15 @@ service ImportProcess_Simple_InputsService { type ProcessAttributes : many ProcessAttribute; type ProcessInstance { + id : String; definitionId : String; definitionVersion : String; - id : String; status : String; - startedAt : String; + startedAt : Timestamp; + completedAt : Timestamp; startedBy : String; + subject : String; + businessKey : String; }; type ProcessInstances : many ProcessInstance; @@ -48,9 +51,27 @@ service ImportProcess_Simple_InputsService { processInstanceId : String not null ) returns ProcessOutputs; - function getInstancesByBusinessKey( - businessKey : String not null, - status : many String + function getInstances( + id : String, + businessKey : String, + status : many String, + definitionId : String, + definitionVersion : String, + startedAt : Timestamp, + startedFrom : Timestamp, + startedUpTo : Timestamp, + completedAt : Timestamp, + completedFrom : Timestamp, + completedUpTo : Timestamp, + startedBy : String, + subject : String, + containsText : String, + rootInstanceId : String, + parentInstanceId : String, + top : Integer, + skip : Integer, + orderBy : String, + inlinecount : String ) returns ProcessInstances; action suspend( 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..4b53bbb9 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 : 24f98826fee0c33da91132659edd551b */ namespace eu12.cdsmunich.capprocesspluginhybridtest; /** DO NOT EDIT. THIS IS A GENERATED SERVICE THAT WILL BE OVERRIDDEN ON NEXT IMPORT. */ @@ -23,12 +23,15 @@ service Programmatic_Lifecycle_ProcessService { type ProcessAttributes : many ProcessAttribute; type ProcessInstance { + id : String; definitionId : String; definitionVersion : String; - id : String; status : String; - startedAt : String; + startedAt : Timestamp; + completedAt : Timestamp; startedBy : String; + subject : String; + businessKey : String; }; type ProcessInstances : many ProcessInstance; @@ -45,9 +48,27 @@ service Programmatic_Lifecycle_ProcessService { processInstanceId : String not null ) returns ProcessOutputs; - function getInstancesByBusinessKey( - businessKey : String not null, - status : many String + function getInstances( + id : String, + businessKey : String, + status : many String, + definitionId : String, + definitionVersion : String, + startedAt : Timestamp, + startedFrom : Timestamp, + startedUpTo : Timestamp, + completedAt : Timestamp, + completedFrom : Timestamp, + completedUpTo : Timestamp, + startedBy : String, + subject : String, + containsText : String, + rootInstanceId : String, + parentInstanceId : String, + top : Integer, + skip : Integer, + orderBy : String, + inlinecount : String ) returns ProcessInstances; action suspend( 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..57d15d94 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 : 672c9171d8b5797ae70a8e84f1307371 */ namespace eu12.cdsmunich.capprocesspluginhybridtest; /** DO NOT EDIT. THIS IS A GENERATED SERVICE THAT WILL BE OVERRIDDEN ON NEXT IMPORT. */ @@ -30,12 +30,15 @@ service Programmatic_Output_ProcessService { type ProcessAttributes : many ProcessAttribute; type ProcessInstance { + id : String; definitionId : String; definitionVersion : String; - id : String; status : String; - startedAt : String; + startedAt : Timestamp; + completedAt : Timestamp; startedBy : String; + subject : String; + businessKey : String; }; type ProcessInstances : many ProcessInstance; @@ -52,9 +55,27 @@ service Programmatic_Output_ProcessService { processInstanceId : String not null ) returns ProcessOutputs; - function getInstancesByBusinessKey( - businessKey : String not null, - status : many String + function getInstances( + id : String, + businessKey : String, + status : many String, + definitionId : String, + definitionVersion : String, + startedAt : Timestamp, + startedFrom : Timestamp, + startedUpTo : Timestamp, + completedAt : Timestamp, + completedFrom : Timestamp, + completedUpTo : Timestamp, + startedBy : String, + subject : String, + containsText : String, + rootInstanceId : String, + parentInstanceId : String, + top : Integer, + skip : Integer, + orderBy : String, + inlinecount : String ) returns ProcessInstances; action suspend( diff --git a/tests/bookshop/srv/programmatic-service.cds b/tests/bookshop/srv/programmatic-service.cds index 90685fe4..5b3b07ec 100644 --- a/tests/bookshop/srv/programmatic-service.cds +++ b/tests/bookshop/srv/programmatic-service.cds @@ -29,8 +29,10 @@ service ProgrammaticService { action getOutputs(instanceId: String) returns ProcessOutputs; - action getInstancesByBusinessKey(ID: UUID, - status: many String) returns many ProcessInstance; + action getInstances(ID: UUID, + status: many String, + top: Integer, + skip: Integer) returns many ProcessInstance; action startForGetOutputs(ID: UUID, mandatory_datetime: Timestamp, @@ -46,8 +48,28 @@ service ProgrammaticService { action genericCancel(businessKey: String, cascade: Boolean); action genericSuspend(businessKey: String, cascade: Boolean); action genericResume(businessKey: String, cascade: Boolean); - action genericGetInstancesByBusinessKey(businessKey: String, - status: many String) returns many ProcessInstance; + action genericGetInstances( + id: String, + businessKey: String, + status: many String, + definitionId: String, + definitionVersion: String, + startedAt: Timestamp, + startedFrom: Timestamp, + startedUpTo: Timestamp, + completedAt: Timestamp, + completedFrom: Timestamp, + completedUpTo: Timestamp, + startedBy: String, + subject: String, + containsText: String, + rootInstanceId: String, + parentInstanceId: String, + top: Integer, + skip: Integer, + orderBy: String, + inlinecount: String + ) returns many ProcessInstance; action genericGetAttributes(processInstanceId: String) returns many ProcessAttribute; action genericGetOutputs(processInstanceId: String) returns ProcessOutputs; } diff --git a/tests/bookshop/srv/programmatic-service.ts b/tests/bookshop/srv/programmatic-service.ts index d038f650..f9450009 100644 --- a/tests/bookshop/srv/programmatic-service.ts +++ b/tests/bookshop/srv/programmatic-service.ts @@ -33,18 +33,20 @@ class ProgrammaticService extends cds.ApplicationService { await programmaticLifecycleProcess.cancel({ businessKey: ID }); }); - this.on('getInstancesByBusinessKey', async (req: cds.Request) => { - const { ID, status } = req.data; - const instances = await programmaticLifecycleProcess.getInstancesByBusinessKey({ + this.on('getInstances', async (req: cds.Request) => { + const { ID, status, top, skip } = req.data; + const instances = await programmaticLifecycleProcess.getInstances({ businessKey: ID, status: status, + top: top, + skip: skip, }); return instances; }); this.on('getAttributes', async (req: cds.Request) => { const { ID } = req.data; - const processInstances = await programmaticLifecycleProcess.getInstancesByBusinessKey({ + const processInstances = await programmaticLifecycleProcess.getInstances({ businessKey: ID, }); const allAttributes = []; @@ -74,7 +76,7 @@ class ProgrammaticService extends cds.ApplicationService { this.on('getInstanceIDForGetOutputs', async (req: cds.Request) => { const { ID, status } = req.data; - const processInstances = await programmaticOutputProcess.getInstancesByBusinessKey({ + const processInstances = await programmaticOutputProcess.getInstances({ businessKey: ID, status: status, }); @@ -127,12 +129,8 @@ class ProgrammaticService extends cds.ApplicationService { await queuedProcessService.emit('resume', { businessKey, cascade: cascade ?? false }); }); - this.on('genericGetInstancesByBusinessKey', async (req: cds.Request) => { - const { businessKey, status } = req.data; - const result = await processService.send('getInstancesByBusinessKey', { - businessKey, - status, - }); + this.on('genericGetInstances', async (req: cds.Request) => { + const result = await processService.send('getInstances', req.data); return result; }); 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/annotationApproach.test.ts b/tests/hybrid/annotationApproach.test.ts index 876f3981..6002030e 100644 --- a/tests/hybrid/annotationApproach.test.ts +++ b/tests/hybrid/annotationApproach.test.ts @@ -12,7 +12,7 @@ describe('Annotation Approach Hybrid Tests', () => { } async function getInstances(ID: string, status?: string[]): Promise { - const res = await POST('/odata/v4/annotation-hybrid/getInstancesByBusinessKey', { ID, status }); + const res = await POST('/odata/v4/annotation-hybrid/getInstances', { ID, status }); return res.data?.value ?? res.data ?? []; } 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..3fca6d2c 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 : 99dc8a7a72e25f64ab1b6bdec8fbafa8 */ namespace eu12.cdsmunich.capprocesspluginhybridtest; /** DO NOT EDIT. THIS IS A GENERATED SERVICE THAT WILL BE OVERRIDDEN ON NEXT IMPORT. */ @@ -57,12 +57,15 @@ service ImportProcess_Attributes_And_OutputsService { type ProcessAttributes : many ProcessAttribute; type ProcessInstance { + id : String; definitionId : String; definitionVersion : String; - id : String; status : String; - startedAt : String; + startedAt : Timestamp; + completedAt : Timestamp; startedBy : String; + subject : String; + businessKey : String; }; type ProcessInstances : many ProcessInstance; @@ -79,9 +82,27 @@ service ImportProcess_Attributes_And_OutputsService { processInstanceId : String not null ) returns ProcessOutputs; - function getInstancesByBusinessKey( - businessKey : String not null, - status : many String + function getInstances( + id : String, + businessKey : String, + status : many String, + definitionId : String, + definitionVersion : String, + startedAt : Timestamp, + startedFrom : Timestamp, + startedUpTo : Timestamp, + completedAt : Timestamp, + completedFrom : Timestamp, + completedUpTo : Timestamp, + startedBy : String, + subject : String, + containsText : String, + rootInstanceId : String, + parentInstanceId : String, + top : Integer, + skip : Integer, + orderBy : String, + inlinecount : String ) returns ProcessInstances; action suspend( 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..022b5d13 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 : 679467542ca0d729a5f5f51f59061cbd */ namespace eu12.cdsmunich.capprocesspluginhybridtest; /** DO NOT EDIT. THIS IS A GENERATED SERVICE THAT WILL BE OVERRIDDEN ON NEXT IMPORT. */ @@ -49,12 +49,15 @@ service ImportProcess_Complex_InputsService { type ProcessAttributes : many ProcessAttribute; type ProcessInstance { + id : String; definitionId : String; definitionVersion : String; - id : String; status : String; - startedAt : String; + startedAt : Timestamp; + completedAt : Timestamp; startedBy : String; + subject : String; + businessKey : String; }; type ProcessInstances : many ProcessInstance; @@ -71,9 +74,27 @@ service ImportProcess_Complex_InputsService { processInstanceId : String not null ) returns ProcessOutputs; - function getInstancesByBusinessKey( - businessKey : String not null, - status : many String + function getInstances( + id : String, + businessKey : String, + status : many String, + definitionId : String, + definitionVersion : String, + startedAt : Timestamp, + startedFrom : Timestamp, + startedUpTo : Timestamp, + completedAt : Timestamp, + completedFrom : Timestamp, + completedUpTo : Timestamp, + startedBy : String, + subject : String, + containsText : String, + rootInstanceId : String, + parentInstanceId : String, + top : Integer, + skip : Integer, + orderBy : String, + inlinecount : String ) returns ProcessInstances; action suspend( 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..af469a44 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 : 2a382c5f1ed621b369d90ef1ccdcc4d6 */ namespace eu12.cdsmunich.capprocesspluginhybridtest; /** DO NOT EDIT. THIS IS A GENERATED SERVICE THAT WILL BE OVERRIDDEN ON NEXT IMPORT. */ @@ -26,12 +26,15 @@ service ImportProcess_Simple_InputsService { type ProcessAttributes : many ProcessAttribute; type ProcessInstance { + id : String; definitionId : String; definitionVersion : String; - id : String; status : String; - startedAt : String; + startedAt : Timestamp; + completedAt : Timestamp; startedBy : String; + subject : String; + businessKey : String; }; type ProcessInstances : many ProcessInstance; @@ -48,9 +51,27 @@ service ImportProcess_Simple_InputsService { processInstanceId : String not null ) returns ProcessOutputs; - function getInstancesByBusinessKey( - businessKey : String not null, - status : many String + function getInstances( + id : String, + businessKey : String, + status : many String, + definitionId : String, + definitionVersion : String, + startedAt : Timestamp, + startedFrom : Timestamp, + startedUpTo : Timestamp, + completedAt : Timestamp, + completedFrom : Timestamp, + completedUpTo : Timestamp, + startedBy : String, + subject : String, + containsText : String, + rootInstanceId : String, + parentInstanceId : String, + top : Integer, + skip : Integer, + orderBy : String, + inlinecount : String ) returns ProcessInstances; action suspend( diff --git a/tests/hybrid/programmaticApproach.test.ts b/tests/hybrid/programmaticApproach.test.ts index 0413de97..3292e7c3 100644 --- a/tests/hybrid/programmaticApproach.test.ts +++ b/tests/hybrid/programmaticApproach.test.ts @@ -20,7 +20,7 @@ describe('Programmatic Approach Hybrid Tests', () => { } async function getInstances(ID: string, status?: string[]): Promise { - const res = await POST('/odata/v4/programmatic/getInstancesByBusinessKey', { ID, status }); + const res = await POST('/odata/v4/programmatic/getInstances', { ID, status }); return res.data?.value ?? res.data ?? []; } @@ -364,4 +364,54 @@ describe('Programmatic Approach Hybrid Tests', () => { expect(outputs).toHaveProperty('optional_datetime'); }); }); + + describe('getInstances – query params against SBPA', () => { + it('should filter instances by definitionId', async () => { + const ID = generateID(); + await startProcess(ID); + await waitForInstances(ID, ['RUNNING']); + + const res = await POST('/odata/v4/programmatic/genericGetInstances', { + businessKey: ID, + definitionId: 'eu12.cdsmunich.capprocesspluginhybridtest.programmatic_Lifecycle_Process', + status: ['RUNNING'], + }); + const instances = res.data?.value ?? res.data ?? []; + + expect(instances.length).toBe(1); + expect(instances[0]).toHaveProperty( + 'definitionId', + 'eu12.cdsmunich.capprocesspluginhybridtest.programmatic_Lifecycle_Process', + ); + }); + + it('should respect top=1 and return only one instance', async () => { + const idA = generateID(); + const idB = generateID(); + await startProcess(idA); + await startProcess(idB); + await waitForInstances(idA, ['RUNNING']); + await waitForInstances(idB, ['RUNNING']); + + const res = await POST('/odata/v4/programmatic/genericGetInstances', { + status: ['RUNNING'], + top: 1, + }); + const instances = res.data?.value ?? res.data ?? []; + + expect(instances.length).toBe(1); + }); + + it('should return instances when called with no params', async () => { + const ID = generateID(); + await startProcess(ID); + await waitForInstances(ID, ['RUNNING']); + + const res = await POST('/odata/v4/programmatic/genericGetInstances', {}); + const instances = res.data?.value ?? res.data ?? []; + + expect(Array.isArray(instances)).toBe(true); + expect(instances.length).toBeGreaterThan(0); + }); + }); }); diff --git a/tests/integration/getInstances.test.ts b/tests/integration/getInstances.test.ts new file mode 100644 index 00000000..c88a47ff --- /dev/null +++ b/tests/integration/getInstances.test.ts @@ -0,0 +1,127 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import cds from '@sap/cds'; +import * as path from 'path'; + +const app = path.join(__dirname, '../bookshop/'); +const { POST } = cds.test(app); + +const DEFINITION_ID = 'eu12.cdsmunich.capprocesspluginhybridtest.programmatic_Lifecycle_Process'; + +describe('getInstances Integration Tests', () => { + afterAll(async () => { + await (cds as any).flush(); + await new Promise((resolve) => setTimeout(resolve, 5000)); + }); + + function generateID(): string { + return cds.utils.uuid(); + } + + // Start a process directly via ProcessService (synchronous, bypasses outbox) + async function startProcess(businessKey: string) { + const processService = await cds.connect.to('ProcessService'); + await processService.send( + 'start', + { definitionId: DEFINITION_ID, context: { ID: businessKey } }, + { businessKey }, + ); + } + + // Query via programmatic getInstances action (businessKey mapped as ID + status + top/skip) + async function getInstances(params: Record): Promise { + const res = await POST('/odata/v4/programmatic/getInstances', params); + return res.data?.value ?? res.data ?? []; + } + + // Query via genericGetInstances which passes all params directly to ProcessService + async function genericGetInstances(params: Record): Promise { + const res = await POST('/odata/v4/programmatic/genericGetInstances', params); + return res.data?.value ?? res.data ?? []; + } + + describe('filter by businessKey', () => { + it('returns instances matching businessKey', async () => { + const ID = generateID(); + await startProcess(ID); + + const instances = await getInstances({ ID }); + expect(instances.length).toBe(1); + expect(instances[0]).toHaveProperty('status', 'RUNNING'); + }); + + it('returns empty array for unknown businessKey', async () => { + const instances = await getInstances({ ID: generateID() }); + expect(instances).toHaveLength(0); + }); + }); + + describe('filter by status', () => { + it('returns instance when status matches', async () => { + const ID = generateID(); + await startProcess(ID); + + const instances = await getInstances({ ID, status: ['RUNNING'] }); + expect(instances.length).toBe(1); + expect(instances[0]).toHaveProperty('status', 'RUNNING'); + }); + + it('returns empty array when status does not match', async () => { + const ID = generateID(); + await startProcess(ID); + + const instances = await getInstances({ ID, status: ['SUSPENDED'] }); + expect(instances).toHaveLength(0); + }); + + it('returns instance when one of multiple statuses matches', async () => { + const ID = generateID(); + await startProcess(ID); + + const instances = await getInstances({ ID, status: ['RUNNING', 'SUSPENDED'] }); + expect(instances.length).toBe(1); + }); + }); + + describe('pagination', () => { + it('respects top parameter', async () => { + const idA = generateID(); + const idB = generateID(); + await startProcess(idA); + await startProcess(idB); + + const all = await getInstances({ status: ['RUNNING'] }); + const paged = await getInstances({ status: ['RUNNING'], top: 1 }); + + expect(all.length).toBeGreaterThanOrEqual(2); + expect(paged.length).toBe(1); + }); + + it('respects skip parameter', async () => { + const idA = generateID(); + const idB = generateID(); + await startProcess(idA); + await startProcess(idB); + + const all = await getInstances({ status: ['RUNNING'] }); + const skipped = await getInstances({ status: ['RUNNING'], skip: 1 }); + + expect(skipped.length).toBe(all.length - 1); + }); + }); + + describe('generic getInstances with direct params', () => { + it('filters by businessKey and status', async () => { + const ID = generateID(); + await startProcess(ID); + + const instances = await genericGetInstances({ businessKey: ID, status: ['RUNNING'] }); + expect(instances.length).toBe(1); + expect(instances[0]).toHaveProperty('status', 'RUNNING'); + }); + + it('returns empty when no match', async () => { + const instances = await genericGetInstances({ businessKey: generateID() }); + expect(instances).toHaveLength(0); + }); + }); +}); diff --git a/tests/sample/status-management/README.md b/tests/sample/status-management/README.md index 57416fe0..0810224b 100644 --- a/tests/sample/status-management/README.md +++ b/tests/sample/status-management/README.md @@ -182,7 +182,10 @@ this.after('CREATE', Authors, async (author, req) => { this.after('DELETE', Authors, async (author, req) => { if (!author.ID) return; - const instances = await authorProcess.getInstancesByBusinessKey(author.ID, ['RUNNING']); + const instances = await authorProcess.getInstances({ + businessKey: author.ID, + status: ['RUNNING'], + }); if (instances.length > 0) { await authorProcess.cancel({ businessKey: author.ID, cascade: true }); } @@ -193,7 +196,7 @@ this.after('DELETE', Authors, async (author, req) => { Both services use the same pattern to display live process status in the UI. Virtual fields (`processStatus`, `isApproved`, `processCriticality` for Books; `verificationStatus`, `isVerified`, `verificationCriticality` for Authors) are declared in the CDS projections and populated in `after('READ')` handlers: -1. **Look up** the process instance via `getInstancesByBusinessKey(businessKey, statusFilters)` +1. **Look up** the process instance via `getInstances({ businessKey, status: statusFilters })` 2. **Based on status:** - `RUNNING` -- Fetch current step via `getAttributes(instanceId)` - `COMPLETED` -- Fetch final result via `getOutputs(instanceId)` diff --git a/tests/sample/status-management/srv/authors-service.js b/tests/sample/status-management/srv/authors-service.js index d765b946..2adea6f8 100644 --- a/tests/sample/status-management/srv/authors-service.js +++ b/tests/sample/status-management/srv/authors-service.js @@ -31,7 +31,10 @@ module.exports = class AuthorsService extends cds.ApplicationService { this.after('DELETE', Authors, async (author, req) => { if (!author.ID) return; - const instances = await authorProcess.getInstancesByBusinessKey(author.ID, ['RUNNING']); + const instances = await authorProcess.getInstances({ + businessKey: author.ID, + status: ['RUNNING'], + }); if (instances.length > 0) { await authorProcess.cancel({ businessKey: author.ID, cascade: true }); } @@ -48,11 +51,10 @@ module.exports = class AuthorsService extends cds.ApplicationService { return; } - const instances = await authorProcess.getInstancesByBusinessKey(author.ID, [ - 'RUNNING', - 'COMPLETED', - 'CANCELED', - ]); + const instances = await authorProcess.getInstances({ + businessKey: author.ID, + status: ['RUNNING', 'COMPLETED', 'CANCELED'], + }); if (instances[0]?.id && instances[0]?.status) { const { id, status } = instances[0]; diff --git a/tests/sample/status-management/srv/books-service.js b/tests/sample/status-management/srv/books-service.js index 4f6dd9f2..93eb9675 100644 --- a/tests/sample/status-management/srv/books-service.js +++ b/tests/sample/status-management/srv/books-service.js @@ -25,11 +25,10 @@ module.exports = class BooksService extends cds.ApplicationService { return; } - const instances = await bookProcess.getInstancesByBusinessKey(bookID, [ - 'RUNNING', - 'COMPLETED', - 'CANCELED', - ]); + const instances = await bookProcess.getInstances({ + businessKey: bookID, + status: ['RUNNING', 'COMPLETED', 'CANCELED'], + }); if (instances[0]?.id && instances[0]?.status) { const { id, status } = instances[0]; diff --git a/tests/sample/status-management/srv/external/eu12.cdsmunich.sampleapplicationproject.authorVerificationProcess.cds b/tests/sample/status-management/srv/external/eu12.cdsmunich.sampleapplicationproject.authorVerificationProcess.cds index dec633f0..7435f4d0 100644 --- a/tests/sample/status-management/srv/external/eu12.cdsmunich.sampleapplicationproject.authorVerificationProcess.cds +++ b/tests/sample/status-management/srv/external/eu12.cdsmunich.sampleapplicationproject.authorVerificationProcess.cds @@ -31,8 +31,11 @@ service AuthorVerificationProcessService { definitionVersion : String; id : String; status : String; - startedAt : String; + startedAt : Timestamp; + completedAt : Timestamp; startedBy : String; + subject : String; + businessKey : String; }; type ProcessInstances : many ProcessInstance; @@ -49,9 +52,27 @@ service AuthorVerificationProcessService { processInstanceId : String not null ) returns ProcessOutputs; - function getInstancesByBusinessKey( - businessKey : String not null, - status : many String + function getInstances( + id : String, + businessKey : String, + status : many String, + definitionId : String, + definitionVersion : String, + startedAt : Timestamp, + startedFrom : Timestamp, + startedUpTo : Timestamp, + completedAt : Timestamp, + completedFrom : Timestamp, + completedUpTo : Timestamp, + startedBy : String, + subject : String, + containsText : String, + rootInstanceId : String, + parentInstanceId : String, + top : Integer, + skip : Integer, + orderBy : String, + inlinecount : String ) returns ProcessInstances; action suspend( diff --git a/tests/sample/status-management/srv/external/eu12.cdsmunich.sampleapplicationproject.bookApprovalProcess.cds b/tests/sample/status-management/srv/external/eu12.cdsmunich.sampleapplicationproject.bookApprovalProcess.cds index 1c78ad8a..3f0dd793 100644 --- a/tests/sample/status-management/srv/external/eu12.cdsmunich.sampleapplicationproject.bookApprovalProcess.cds +++ b/tests/sample/status-management/srv/external/eu12.cdsmunich.sampleapplicationproject.bookApprovalProcess.cds @@ -37,8 +37,11 @@ service BookApprovalProcessService { definitionVersion : String; id : String; status : String; - startedAt : String; + startedAt : Timestamp; + completedAt : Timestamp; startedBy : String; + subject : String; + businessKey : String; }; type ProcessInstances : many ProcessInstance; @@ -55,9 +58,27 @@ service BookApprovalProcessService { processInstanceId : String not null ) returns ProcessOutputs; - function getInstancesByBusinessKey( - businessKey : String not null, - status : many String + function getInstances( + id : String, + businessKey : String, + status : many String, + definitionId : String, + definitionVersion : String, + startedAt : Timestamp, + startedFrom : Timestamp, + startedUpTo : Timestamp, + completedAt : Timestamp, + completedFrom : Timestamp, + completedUpTo : Timestamp, + startedBy : String, + subject : String, + containsText : String, + rootInstanceId : String, + parentInstanceId : String, + top : Integer, + skip : Integer, + orderBy : String, + inlinecount : String ) returns ProcessInstances; action suspend( diff --git a/tests/unit/workflowClient.test.ts b/tests/unit/workflowClient.test.ts new file mode 100644 index 00000000..834f6be6 --- /dev/null +++ b/tests/unit/workflowClient.test.ts @@ -0,0 +1,122 @@ +import { getInstances, WorkflowStatus } from '../../lib/api/workflow-client'; + +const SERVICE_URL = 'https://example.sbpa.com'; +const JWT = 'test-jwt'; +const BASE = `${SERVICE_URL}/public/workflow/rest/v1/workflow-instances`; + +function mockFetch(instances: unknown[] = []) { + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + json: async () => instances, + } as Response); +} + +function capturedUrl(): string { + return (global.fetch as jest.Mock).mock.calls[0][0] as string; +} + +describe('getInstances – URL building', () => { + beforeEach(() => mockFetch()); + + it('builds URL with no params', async () => { + await getInstances(SERVICE_URL, JWT, {}); + expect(capturedUrl()).toBe(BASE); + }); + + it('filters by businessKey', async () => { + await getInstances(SERVICE_URL, JWT, { businessKey: 'my-key' }); + expect(capturedUrl()).toContain('businessKey=my-key'); + }); + + it('encodes status values', async () => { + await getInstances(SERVICE_URL, JWT, { status: [WorkflowStatus.RUNNING] }); + expect(capturedUrl()).toContain('status=RUNNING'); + // verify it's encoded (no raw spaces or special chars) + expect(capturedUrl()).not.toContain('status= '); + }); + + it('filters by single status', async () => { + await getInstances(SERVICE_URL, JWT, { status: [WorkflowStatus.RUNNING] }); + expect(capturedUrl()).toContain('status=RUNNING'); + }); + + it('filters by multiple statuses', async () => { + await getInstances(SERVICE_URL, JWT, { + status: [WorkflowStatus.RUNNING, WorkflowStatus.SUSPENDED], + }); + const url = capturedUrl(); + expect(url).toContain('status=RUNNING'); + expect(url).toContain('status=SUSPENDED'); + }); + + it('remaps orderBy to $orderby', async () => { + await getInstances(SERVICE_URL, JWT, { orderBy: 'startedAt desc' }); + expect(capturedUrl()).toContain('$orderby=startedAt%20desc'); + }); + + it('remaps top to $top', async () => { + await getInstances(SERVICE_URL, JWT, { top: 10 }); + expect(capturedUrl()).toContain('$top=10'); + }); + + it('remaps skip to $skip', async () => { + await getInstances(SERVICE_URL, JWT, { skip: 5 }); + expect(capturedUrl()).toContain('$skip=5'); + }); + + it('remaps inlinecount to $inlinecount', async () => { + await getInstances(SERVICE_URL, JWT, { inlinecount: 'allpages' }); + expect(capturedUrl()).toContain('$inlinecount=allpages'); + }); + + it('skips null and undefined params', async () => { + await getInstances(SERVICE_URL, JWT, { businessKey: null, definitionId: null }); + expect(capturedUrl()).toBe(BASE); + }); + + it('encodes special characters in param values', async () => { + await getInstances(SERVICE_URL, JWT, { subject: 'hello world & more' }); + expect(capturedUrl()).toContain('subject=hello%20world%20%26%20more'); + }); + + it('combines multiple direct params with status', async () => { + await getInstances(SERVICE_URL, JWT, { + businessKey: 'bk-1', + definitionId: 'def-1', + status: [WorkflowStatus.RUNNING], + top: 20, + }); + const url = capturedUrl(); + expect(url).toContain('businessKey=bk-1'); + expect(url).toContain('definitionId=def-1'); + expect(url).toContain('status=RUNNING'); + expect(url).toContain('$top=20'); + }); + + it('includes startedFrom and startedUpTo as direct params', async () => { + await getInstances(SERVICE_URL, JWT, { + startedFrom: '2024-01-01T00:00:00Z', + startedUpTo: '2024-12-31T23:59:59Z', + }); + const url = capturedUrl(); + expect(url).toContain('startedFrom='); + expect(url).toContain('startedUpTo='); + }); + + it('sends Bearer token in Authorization header', async () => { + await getInstances(SERVICE_URL, JWT, {}); + const headers = (global.fetch as jest.Mock).mock.calls[0][1].headers; + expect(headers.Authorization).toBe(`Bearer ${JWT}`); + }); + + it('throws on non-ok response', async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: false, + status: 403, + statusText: 'Forbidden', + text: async () => 'Not allowed', + } as unknown as Response); + + await expect(getInstances(SERVICE_URL, JWT, {})).rejects.toBeDefined(); + }); +});