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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
tests/bookshop/workflows/*
tests/bookshop/srv/workflows/*
dist/*
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -400,27 +400,27 @@ To use the programmatic approach with types, you need to import an existing SBPA

Import your SBPA process directly from the API:

**Note:** For remote imports, you must have ProcessService credentials bound. Run with `cds bind --exec` if needed:
**Note:** For remote imports, you must have ProcessService credentials bound (e.g., via `cds bind process -2 <instance>`). The plugin will automatically resolve the bindings at import time.

```bash
cds bind --exec -- cds-tsx import --from process --name eu12.bpm-horizon-walkme.sdshipmentprocessor.shipmentHandler --no-copy
cds import --from process --name eu12.bpm-horizon-walkme.sdshipmentprocessor.shipmentHandler --no-copy
```

If you want to have it as a cds instead of a csn you can add --as cds at the end. If you want to reimport the process use the --force flag at the end. The flag `no-copy` is very important, as otherwise the process will be saved locally on both `./workflows`and `./srv/external` folder which would result in cds runtime issues, as the json is not a valid csn model and cannot be stored in the `.srv/external` directory.
If you want to have it as a cds instead of a csn you can add --as cds at the end. If you want to reimport the process use the --force flag at the end. The flag `no-copy` is very important, as otherwise the process will be saved locally on both `./srv/workflows`and `./srv/external` folder which would result in cds runtime issues, as the json is not a valid csn model and cannot be stored in the `.srv/external` directory.

### From Local JSON File

If you already have a process definition JSON file (e.g., exported or previously fetched), you can generate the CSN model directly from it without needing credentials:

```bash
cds import --from process ./workflows/eu12.bpm-horizon-walkme.sdshipmentprocessor.shipmentHandler.json --no-copy
cds import --from process ./srv/workflows/eu12.bpm-horizon-walkme.sdshipmentprocessor.shipmentHandler.json --no-copy
```

### What Gets Generated

This will generate:

- A CDS service definition in `./workflows/`
- A CDS service definition in `./srv/workflows/`
- Types via `cds-typer` for full TypeScript support
- Generic handlers for the actions and functions in the imported service

Expand Down
28 changes: 25 additions & 3 deletions lib/processImport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,17 +75,39 @@ async function fetchAndSaveProcessDefinition(processName: string): Promise<Fetch
processHeader.dataTypes.forEach((dt) => dataTypeCache.set(dt.uid, dt));
}

const outputPath = path.join(cds.root, 'workflows', `${processName}.json`);
const outputPath = path.join(cds.root, 'srv', 'workflows', `${processName}.json`);
await fs.promises.mkdir(path.dirname(outputPath), { recursive: true });
await fs.promises.writeFile(outputPath, JSON.stringify(processHeader, null, 2), 'utf8');

return { filePath: outputPath, processHeader };
}

async function createApiClient(): Promise<IProcessApiClient> {
const credentials = getServiceCredentials(PROCESS_SERVICE);
let credentials = getServiceCredentials(PROCESS_SERVICE);

if (!credentials) {
// Try to resolve cloud bindings automatically (same as cds bind --exec does)
// REVISIT: once merged in core
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const cdsDk = cds as any;
const resolve = cdsDk._localOrGlobal ?? cdsDk._local ?? require;
const { env: bindingEnv } = resolve('@sap/cds-dk/lib/bind/shared');
process.env.CDS_ENV ??= 'hybrid';
cdsDk.env = cds.env.for('cds');
Object.assign(process.env, await bindingEnv());
cdsDk.env = cds.env.for('cds');
cdsDk.requires = cds.env.requires;
credentials = getServiceCredentials(PROCESS_SERVICE);
} catch (e) {
LOG.debug('Auto-resolve bindings failed:', e);
}
}

if (!credentials) {
throw new Error('No ProcessService credentials found. Run with: cds bind --exec -- ...');
throw new Error(
'No ProcessService credentials found. Ensure you have bound a process service instance (e.g., via cds bind process -2 <instance>).',
);
}

const apiUrl = credentials.endpoints?.api;
Expand Down
8 changes: 7 additions & 1 deletion lib/processImportRegistration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ export function registerProcessImport() {
// @ts-expect-error: cds type does not exist
cds.import.options ??= {};
// @ts-expect-error: cds type does not exist
cds.import.options.process = { no_copy: true, as: 'cds', config: 'kind=process-service' };
cds.import.options.process = {
no_copy: true,
as: 'cds',
config: 'kind=process-service',
profile: 'hybrid',
'resolve-bindings': true,
};
// @ts-expect-error: process does not exist on cds.import type
cds.import.from ??= {};
// @ts-expect-error: from does not exist on cds.import type
Expand Down
14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cap-js/process",
"version": "1.0.0",
"version": "0.1.0",
"description": "",
"main": "cds-plugin.js",
"files": [
Expand Down Expand Up @@ -28,12 +28,12 @@
"prettier": "npx -y prettier@3 --write .",
"prettier:check": "npx -y prettier@3 --check .",
"import:process": "npm run import:process:annotationLifeCycle && npm run import:process:programmaticLifecycle && npm run import:process:programmaticOutput && npm run import:process:importAttributesOutputs && npm run import:process:importComplex && npm run import:process:importSimple",
"import:process:annotationLifeCycle": "cd tests/bookshop && cds bind --exec -- cds-tsx import --from process --name eu12.cdsmunich.capprocesspluginhybridtest.annotation_Lifecycle_Process --force --no-copy --as cds --config kind=process-service",
"import:process:programmaticLifecycle": "cd tests/bookshop && cds bind --exec -- cds-tsx import --from process --name eu12.cdsmunich.capprocesspluginhybridtest.programmatic_Lifecycle_Process --force --no-copy --as cds --config kind=process-service",
"import:process:programmaticOutput": "cd tests/bookshop && cds bind --exec -- cds-tsx import --from process --name eu12.cdsmunich.capprocesspluginhybridtest.programmatic_Output_Process --force --no-copy --as cds --config kind=process-service",
"import:process:importAttributesOutputs": "cd tests/bookshop && cds bind --exec -- cds-tsx import --from process --name eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Attributes_And_Outputs --force --no-copy --as cds --config kind=process-service",
"import:process:importComplex": "cd tests/bookshop && cds bind --exec -- cds-tsx import --from process --name eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Complex_Inputs --force --no-copy --as cds --config kind=process-service",
"import:process:importSimple": "cd tests/bookshop && cds bind --exec -- cds-tsx import --from process --name eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Simple_Inputs --force --no-copy --as cds --config kind=process-service"
"import:process:annotationLifeCycle": "cd tests/bookshop && cds import --from process --name eu12.cdsmunich.capprocesspluginhybridtest.annotation_Lifecycle_Process --force",
"import:process:programmaticLifecycle": "cd tests/bookshop && cds import --from process --name eu12.cdsmunich.capprocesspluginhybridtest.programmatic_Lifecycle_Process --force",
"import:process:programmaticOutput": "cd tests/bookshop && cds import --from process --name eu12.cdsmunich.capprocesspluginhybridtest.programmatic_Output_Process --force",
"import:process:importAttributesOutputs": "cd tests/bookshop && cds import --from process --name eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Attributes_And_Outputs --force",
"import:process:importComplex": "cd tests/bookshop && cds import --from process --name eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Complex_Inputs --force",
"import:process:importSimple": "cd tests/bookshop && cds import --from process --name eu12.cdsmunich.capprocesspluginhybridtest.importProcess_Simple_Inputs --force"
},
"keywords": [],
"author": "",
Expand Down
2 changes: 1 addition & 1 deletion tests/bookshop/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
import cds from '@sap/cds/eslint.config.mjs';
export default [...cds.recommended];
export default [{ ignores: ['gen/**'] }, ...cds.recommended];
3 changes: 2 additions & 1 deletion tests/bookshop/srv/annotation-hybrid-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import Annotation_Lifecycle_ProcessService from '#cds-models/eu12/cdsmunich/capp

class AnnotationHybridService extends cds.ApplicationService {
async init() {
const annotationLifecycleProcess = await cds.connect.to(Annotation_Lifecycle_ProcessService);

this.on('getInstancesByBusinessKey', async (req: cds.Request) => {
const { ID, status } = req.data;
const annotationLifecycleProcess = await cds.connect.to(Annotation_Lifecycle_ProcessService);
const instances = await annotationLifecycleProcess.getInstancesByBusinessKey({
businessKey: ID,
status: status,
Expand Down
31 changes: 6 additions & 25 deletions tests/bookshop/srv/programmatic-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@ import Programmatic_Outputs_ProcessService from '#cds-models/eu12/cdsmunich/capp

class ProgrammaticService extends cds.ApplicationService {
async init() {
const programmaticLifecycleProcess = await cds.connect.to(
Programmatic_Lifecycle_ProcessService,
);
const programmaticOutputProcess = await cds.connect.to(Programmatic_Outputs_ProcessService);
const processService = await cds.connect.to('ProcessService');

this.on('startLifeCycleProcess', async (req: cds.Request) => {
const programmaticLifecycleProcess = await cds.connect.to(
Programmatic_Lifecycle_ProcessService,
);
const { ID } = req.data;
await programmaticLifecycleProcess.start({ ID });
});

this.on('updateProcess', async (req: cds.Request) => {
const { ID, newStatus } = req.data;
const programmaticLifecycleProcess = await cds.connect.to(
Programmatic_Lifecycle_ProcessService,
);
if (newStatus === 'SUSPEND') {
await programmaticLifecycleProcess.suspend({
businessKey: ID,
Expand All @@ -30,17 +30,11 @@ class ProgrammaticService extends cds.ApplicationService {

this.on('cancelProcess', async (req: cds.Request) => {
const { ID } = req.data;
const programmaticLifecycleProcess = await cds.connect.to(
Programmatic_Lifecycle_ProcessService,
);
await programmaticLifecycleProcess.cancel({ businessKey: ID });
});

this.on('getInstancesByBusinessKey', async (req: cds.Request) => {
const { ID, status } = req.data;
const programmaticLifecycleProcess = await cds.connect.to(
Programmatic_Lifecycle_ProcessService,
);
const instances = await programmaticLifecycleProcess.getInstancesByBusinessKey({
businessKey: ID,
status: status,
Expand All @@ -50,9 +44,6 @@ class ProgrammaticService extends cds.ApplicationService {

this.on('getAttributes', async (req: cds.Request) => {
const { ID } = req.data;
const programmaticLifecycleProcess = await cds.connect.to(
Programmatic_Lifecycle_ProcessService,
);
const processInstances = await programmaticLifecycleProcess.getInstancesByBusinessKey({
businessKey: ID,
});
Expand All @@ -72,7 +63,6 @@ class ProgrammaticService extends cds.ApplicationService {
this.on('startForGetOutputs', async (req: cds.Request) => {
const { ID, mandatory_datetime, mandatory_string, optional_datetime, optional_string } =
req.data;
const programmaticOutputProcess = await cds.connect.to(Programmatic_Outputs_ProcessService);
await programmaticOutputProcess.start({
ID,
mandatory_datetime,
Expand All @@ -84,7 +74,6 @@ class ProgrammaticService extends cds.ApplicationService {

this.on('getInstanceIDForGetOutputs', async (req: cds.Request) => {
const { ID, status } = req.data;
const programmaticOutputProcess = await cds.connect.to(Programmatic_Outputs_ProcessService);
const processInstances = await programmaticOutputProcess.getInstancesByBusinessKey({
businessKey: ID,
status: status,
Expand All @@ -104,15 +93,13 @@ class ProgrammaticService extends cds.ApplicationService {

this.on('getOutputs', async (req: cds.Request) => {
const { instanceId } = req.data;
const programmaticOutputProcess = await cds.connect.to(Programmatic_Outputs_ProcessService);
const outputs = await programmaticOutputProcess.getOutputs(instanceId);
return outputs;
});

// Generic ProcessService handlers (using cds.connect.to('ProcessService'))
this.on('genericStart', async (req: cds.Request) => {
const { definitionId, businessKey, context } = req.data;
const processService = await cds.connect.to('ProcessService');
const queuedProcessService = cds.queued(processService);
const parsedContext = context ? JSON.parse(context) : {};
await queuedProcessService.emit(
Expand All @@ -124,28 +111,24 @@ class ProgrammaticService extends cds.ApplicationService {

this.on('genericCancel', async (req: cds.Request) => {
const { businessKey, cascade } = req.data;
const processService = await cds.connect.to('ProcessService');
const queuedProcessService = cds.queued(processService);
await queuedProcessService.emit('cancel', { businessKey, cascade: cascade ?? false });
});

this.on('genericSuspend', async (req: cds.Request) => {
const { businessKey, cascade } = req.data;
const processService = await cds.connect.to('ProcessService');
const queuedProcessService = cds.queued(processService);
await queuedProcessService.emit('suspend', { businessKey, cascade: cascade ?? false });
});

this.on('genericResume', async (req: cds.Request) => {
const { businessKey, cascade } = req.data;
const processService = await cds.connect.to('ProcessService');
const queuedProcessService = cds.queued(processService);
await queuedProcessService.emit('resume', { businessKey, cascade: cascade ?? false });
});

this.on('genericGetInstancesByBusinessKey', async (req: cds.Request) => {
const { businessKey, status } = req.data;
const processService = await cds.connect.to('ProcessService');
const result = await processService.send('getInstancesByBusinessKey', {
businessKey,
status,
Expand All @@ -155,14 +138,12 @@ class ProgrammaticService extends cds.ApplicationService {

this.on('genericGetAttributes', async (req: cds.Request) => {
const { processInstanceId } = req.data;
const processService = await cds.connect.to('ProcessService');
const result = await processService.send('getAttributes', { processInstanceId });
return result;
});

this.on('genericGetOutputs', async (req: cds.Request) => {
const { processInstanceId } = req.data;
const processService = await cds.connect.to('ProcessService');
const result = await processService.send('getOutputs', { processInstanceId });
return result;
});
Expand Down
4 changes: 2 additions & 2 deletions tests/bookshop/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
"strict": true,
"skipLibCheck": true,
"sourceMap": true,
"allowJs": true,
"paths": {
"@sap/cds": ["./node_modules/@cap-js/cds-types"],
"#cds-models/*": ["./@cds-models/*"]
}
}
},
"exclude": ["**/node_modules"]
}
2 changes: 0 additions & 2 deletions tests/integration/annotations/lifeCycleAnnotation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ describe('Integration tests for Process Annotation Combinations', () => {

beforeAll(async () => {
const db = await cds.connect.to('db');
// Warmup: ensure DB connection and ProcessService are fully initialized before tests
await cds.connect.to('ProcessService');
db.before('*', (req) => {
if (req.event === 'CREATE' && req.target?.name === 'cds.outbox.Messages') {
const msg = JSON.parse(req.query?.INSERT?.entries[0].msg);
Expand Down
Loading