Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
53951cf
initial logic added
tilwbr Mar 5, 2026
c0c3a47
initial rework of delete, but need to be improved
tilwbr Mar 5, 2026
62f0e94
removed workaround for multiple annotations on DELETE
tilwbr Mar 5, 2026
dbf7a07
updated requests
tilwbr Mar 5, 2026
f40637d
added error handling
tilwbr Mar 5, 2026
cb07690
removed business key generation from processStart
tilwbr Mar 5, 2026
abdc999
update existing tests
tilwbr Mar 5, 2026
195ff5e
added tests for new business key logic
tilwbr Mar 5, 2026
5486b3a
reworked logic again
tilwbr Mar 5, 2026
a5dd04e
removed imports from merge
tilwbr Mar 5, 2026
4dec210
moved constants
tilwbr Mar 5, 2026
cc1623d
initial test fixed for new business key annotation
tilwbr Mar 5, 2026
54185b6
refactoring
tilwbr Mar 6, 2026
1b578e6
removed unneccessary types
tilwbr Mar 6, 2026
6450b1e
initial validation logic
tilwbr Mar 6, 2026
5063ffc
updated readme
tilwbr Mar 6, 2026
0ef10f2
updated imports from merge
tilwbr Mar 6, 2026
1d7e45e
fixed tests and added new tests for business key validation
tilwbr Mar 6, 2026
3805f51
inital code for business key in imported process
tilwbr Mar 6, 2026
d937777
small fix
tilwbr Mar 6, 2026
a390cb0
refactoring and code improvements
tilwbr Mar 6, 2026
f362cc1
improvements
tilwbr Mar 6, 2026
d51cce0
code improvement
tilwbr Mar 6, 2026
730af35
add comment
tilwbr Mar 6, 2026
5871aef
prettier fix
tilwbr Mar 6, 2026
37cc01b
fix pr
tilwbr Mar 6, 2026
ef694f9
fix
tilwbr Mar 6, 2026
a165295
initial validation logic
tilwbr Mar 9, 2026
1897006
improved validation handling
tilwbr Mar 9, 2026
359ed5e
some improvements
tilwbr Mar 9, 2026
7d57e4a
lint fix
tilwbr Mar 9, 2026
55ee0cc
new logic
tilwbr Mar 10, 2026
ae1dc82
removed bkey logic from validation plugin
tilwbr Mar 10, 2026
631f1df
merge conflicts
tilwbr Mar 10, 2026
d399d0e
removed imported bkey expr
tilwbr Mar 10, 2026
b258b4d
fixed failing tests
tilwbr Mar 10, 2026
0701a4c
added business key character limit to process start
tilwbr Mar 10, 2026
f3d05a2
updated readme
tilwbr Mar 10, 2026
923b5c8
fix and added new tests
tilwbr Mar 10, 2026
694eefa
fixed issue with on DELETE for process start
tilwbr Mar 10, 2026
ed871d8
prettier changes
tilwbr Mar 10, 2026
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,9 @@ When both `@bpm.process.start.id` and `@bpm.process.start.on` are present and th
- A bound action defined on the entity
- `@bpm.process.<cancel|suspend|resume>.cascade` is optional (defaults to false); if provided, must be a boolean
- `@bpm.process.<cancel|suspend|resume>.if` must be a valid CDS expression (if present)
- if any annotation with `@bpm.process.<cancel|suspend|resume>` is defined, a valid businessKey expression must be defined using `@bpm.process.businessKey`
- example: `@bpm.process.businessKey: (id || '-' || name)` would concatenate id and name with a '-' string as a business key
- the businessKey definition here must reflect the one configured in SBPA Process Builder

### Warnings

Expand Down
10 changes: 10 additions & 0 deletions lib/build/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ export const ERROR_IF_MUST_BE_EXPRESSION = (entityName: string, annotationIf: st
return `${entityName}: ${annotationIf} must be a valid expression`;
};

export const ERROR_BUSINESS_KEY_MUST_BE_EXPRESSION = (
entityName: string,
annotationBKey: string,
): string => {
return `${entityName}: ${annotationBKey} must be a valid expression`;
};

// =============================================================================
// Start Annotation Validation Messages
// =============================================================================
Expand Down Expand Up @@ -84,6 +91,9 @@ export const ERROR_ON_REQUIRED = (
return `${entityName}: ${annotationPrefix} requires ${annotationOn} to be defined`;
};

export const ERROR_BUSINESS_KEY_REQUIRED = (entityName: string, annotationPrefix: string) =>
`Entity "${entityName}" must have a business key defined when using the "${annotationPrefix}" annotation.`;

export const ERROR_CASCADE_MUST_BE_BOOLEAN = (
entityName: string,
annotationCascade: string,
Expand Down
8 changes: 8 additions & 0 deletions lib/build/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
validateRequiredGenericAnnotations,
validateRequiredStartAnnotations,
validateIfAnnotation,
validateBusinessKeyAnnotation,
} from './index';
import {
PROCESS_START_ID,
Expand All @@ -29,6 +30,7 @@ import {
PROCESS_RESUME,
PROCESS_START,
PROCESS_PREFIX,
BUSINESS_KEY,
} from '../constants';

import { CsnDefinition, CsnEntity } from '../../types/csn-extensions';
Expand Down Expand Up @@ -183,6 +185,7 @@ export class ProcessValidationPlugin extends BuildPluginBase {
const hasOn = def[annotationOn] !== undefined;
const hasCascade = def[annotationCascade] !== undefined;
const hasIf = def[annotationIf] !== undefined;
const hasBusinessKey = def[BUSINESS_KEY] !== undefined;

const hasAnyAnnotationWithPrefix = Object.keys(def).some((key) =>
key.startsWith(annotationPrefix + '.'),
Expand All @@ -195,6 +198,7 @@ export class ProcessValidationPlugin extends BuildPluginBase {
entityName,
annotationOn,
annotationPrefix,
hasBusinessKey,
this,
);

Expand All @@ -209,5 +213,9 @@ export class ProcessValidationPlugin extends BuildPluginBase {
if (hasIf) {
validateIfAnnotation(def, entityName, annotationIf, this);
}

if (hasOn && hasBusinessKey) {
validateBusinessKeyAnnotation(def, entityName, annotationPrefix, this);
}
}
}
21 changes: 20 additions & 1 deletion lib/build/validations.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import cds from '@sap/cds';
import { ProcessValidationPlugin } from './plugin';
import { CsnDefinition, CsnElement, CsnEntity } from '../../types/csn-extensions';
import { PROCESS_START_ID, PROCESS_START_ON } from '../constants';
import { BUSINESS_KEY, PROCESS_START_ID, PROCESS_START_ON } from '../constants';
import {
createCsnEntityContext,
ElementType,
Expand All @@ -18,6 +18,7 @@ import {
ERROR_START_ID_REQUIRES_ON,
ERROR_START_ID_MUST_BE_STRING,
ERROR_ON_REQUIRED,
ERROR_BUSINESS_KEY_REQUIRED,
WARNING_TYPE_MISMATCH,
WARNING_ARRAY_MISMATCH,
ERROR_MISSING_MANDATORY_PROCESS_INPUT,
Expand All @@ -27,6 +28,7 @@ import {
WARNING_NO_PROCESS_DEFINITION,
ERROR_START_BUSINESSKEY_INPUT_MISSING,
WARNING_INPUT_PATH_NOT_IN_ENTITY,
ERROR_BUSINESS_KEY_MUST_BE_EXPRESSION,
} from './constants';
import { EntityContext, ParsedInputEntry } from '../shared/input-parser';

Expand Down Expand Up @@ -83,6 +85,17 @@ export function validateIfAnnotation(
buildPlugin.pushMessage(ERROR_IF_MUST_BE_EXPRESSION(entityName, annotationIf), ERROR);
}
}
export function validateBusinessKeyAnnotation(
def: CsnEntity,
entityName: string,
annotationIf: string,
buildPlugin: ProcessValidationPlugin,
) {
const bKeyExpr = def[BUSINESS_KEY];
if (!bKeyExpr || !bKeyExpr['='] || (!bKeyExpr['xpr'] && !bKeyExpr['ref'])) {
buildPlugin.pushMessage(ERROR_BUSINESS_KEY_MUST_BE_EXPRESSION(entityName, BUSINESS_KEY), ERROR);
}
}

export function validateCascadeAnnotation(
def: CsnEntity,
Expand Down Expand Up @@ -138,12 +151,18 @@ export function validateRequiredGenericAnnotations(
entityName: string,
annotationOn: string,
annotationPrefix: string,
hasBusinessKey: boolean,
buildPlugin: ProcessValidationPlugin,
) {
// If any annotation with this prefix is defined, .on is required
if (hasAnyAnnotationWithPrefix && !hasOn) {
buildPlugin.pushMessage(ERROR_ON_REQUIRED(entityName, annotationPrefix, annotationOn), ERROR);
}
// If .on is defined or any annotation with this prefix is defined,
// businessKey must exist
if ((hasOn || hasAnyAnnotationWithPrefix) && !hasBusinessKey) {
buildPlugin.pushMessage(ERROR_BUSINESS_KEY_REQUIRED(entityName, annotationPrefix), ERROR);
}
}

export function validateInputTypes(
Expand Down
11 changes: 11 additions & 0 deletions lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ export const PROCESS_LOGGER_PREFIX = 'process' as const;
*/
export const CUD_EVENTS = ['CREATE', 'UPDATE', 'DELETE'] as const;

/** Business Key Annotations
*
*/
export const BUSINESS_KEY = '@bpm.process.businessKey' as const;
export const BUSINESS_KEY_MAX_LENGTH = 255;

/**
* Business key alias const
*/
export const BUSINESS_KEY_ALIAS = 'as businessKey' as const;

/**
* Process Start Annotations
*/
Expand Down
9 changes: 1 addition & 8 deletions lib/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,5 @@ export { handleProcessSuspend, addDeletedEntityToRequestSuspend } from './proces
export { handleProcessResume, addDeletedEntityToRequestResume } from './processResume';
export { createProcessActionHandler } from './processActionHandler';
export { registerProcessServiceHandlers } from './processService';
export { getKeyFieldsForEntity, concatenateBusinessKey } from './utils';
export type {
EntityRow,
ProcessStartPayload,
ProcessLifecyclePayload,
ProcessEventType,
AnnotatedTarget,
} from './utils';
export type { EntityRow, ProcessStartPayload, ProcessLifecyclePayload } from './utils';
export type { ProcessDeleteRequest } from './onDeleteUtils';
22 changes: 20 additions & 2 deletions lib/handlers/onDeleteUtils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import cds, { column_expr, expr, Results } from '@sap/cds';
import { PROCESS_LOGGER_PREFIX } from '../constants';
import cds, { column_expr, expr, Results, Target } from '@sap/cds';
import { BUSINESS_KEY, PROCESS_LOGGER_PREFIX } from '../constants';
import { EntityRow } from './utils';
import { WILDCARD } from '../shared/input-parser';
import { getColumnsForProcessStart } from './processStart';
import { getBusinessKeyColumnProcessStart } from '../shared/businessKey-helper';

const LOG = cds.log(PROCESS_LOGGER_PREFIX);

Expand Down Expand Up @@ -90,3 +92,19 @@ export function createAddDeletedEntityHandler(config: AddDeletedEntityConfig) {
}
};
}

export function addBusinessKeyToStartColumns(req: cds.Request): (column_expr | string)[] {
const target = req.target as Target;
const startColumns = getColumnsForProcessStart(target);

const businessKeyColumns = getBusinessKeyColumnProcessStart(
req,
(target[BUSINESS_KEY] as { '=': string })?.['='],
);

if (businessKeyColumns) {
startColumns.push(businessKeyColumns);
}

return startColumns;
}
36 changes: 22 additions & 14 deletions lib/handlers/processActionHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ import { expr, Target } from '@sap/cds';
import {
emitProcessEvent,
EntityRow,
getBusinessKeyOrReject,
getEntityDataFromRequest,
getKeyFieldsForEntity,
ProcessLifecyclePayload,
resolveEntityRowOrReject,
} from './utils';
Expand All @@ -15,13 +13,16 @@ import {
PROCESS_EVENT_MAP,
ProcessDeleteRequest,
} from './onDeleteUtils';
import { getBusinessKeyColumnOrReject } from '../shared/businessKey-helper';
import { BUSINESS_KEY } from '../constants';

type ProcessActionType = 'cancel' | 'resume' | 'suspend';

interface ProcessActionSpec {
on?: string;
cascade: boolean;
conditionExpr: expr | undefined;
businessKey: string | undefined;
}

interface ProcessActionConfig {
Expand Down Expand Up @@ -57,6 +58,7 @@ function initSpecs(
conditionExpr: targetAnnotations[annotations.IF]
? (targetAnnotations[annotations.IF] as { xpr: expr }).xpr
: undefined,
businessKey: targetAnnotations[BUSINESS_KEY]?.['='],
};
}

Expand All @@ -71,36 +73,42 @@ export function createProcessActionHandler(config: ProcessActionConfig) {
// Initialize specifications from annotations
const specs = initSpecs(target, config.annotations);

// Get business key column
const businessKeyColumn = getBusinessKeyColumnOrReject(req, specs.businessKey);
if (!businessKeyColumn) return;

// fetch entity
const row = await resolveEntityRowOrReject(
req,
data,
specs.conditionExpr,
config.logMessages.FETCH_FAILED,
config.logMessages.NOT_TRIGGERED,
[businessKeyColumn],
);
if (!row) return;

// Get business key
const businessKey = getBusinessKeyOrReject(
target as cds.entity,
row,
// Emit process event
const payload: ProcessLifecyclePayload = {
businessKey: (row as { businessKey: string }).businessKey,
cascade: specs.cascade,
};
await emitProcessEvent(
config.action,
req,
config.logMessages.INVALID_KEY,
config.logMessages.EMPTY_KEY,
payload,
config.logMessages.FAILED,
(row as { businessKey: string }).businessKey,
);
if (!businessKey) return;

// Emit process event
const payload: ProcessLifecyclePayload = { businessKey, cascade: specs.cascade };
await emitProcessEvent(config.action, req, payload, config.logMessages.FAILED, businessKey);
};
}

export function createProcessActionAddDeletedEntityHandler(config: ProcessActionDeleteConfig) {
return createAddDeletedEntityHandler({
action: config.action,
ifAnnotation: config.annotations.IF,
getColumns: (req) => getKeyFieldsForEntity(req.target as cds.entity),
getColumns: (req) => [
getBusinessKeyColumnOrReject(req, (req.target as cds.entity)[BUSINESS_KEY]?.['=']),
],
});
}
35 changes: 21 additions & 14 deletions lib/handlers/processStart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { column_expr, expr, Target } from '@sap/cds';
import {
emitProcessEvent,
EntityRow,
getBusinessKeyOrReject,
getEntityDataFromRequest,
resolveEntityRowOrReject,
} from './utils';
Expand All @@ -13,6 +12,8 @@ import {
PROCESS_START_INPUTS,
LOG_MESSAGES,
PROCESS_LOGGER_PREFIX,
BUSINESS_KEY,
BUSINESS_KEY_MAX_LENGTH,
} from './../constants';
import {
InputCSNEntry,
Expand All @@ -25,11 +26,13 @@ import {

import cds from '@sap/cds';
import {
addBusinessKeyToStartColumns,
createAddDeletedEntityHandler,
isDeleteWithoutProcess,
PROCESS_EVENT_MAP,
ProcessDeleteRequest,
} from './onDeleteUtils';
import { getBusinessKeyColumnProcessStart } from '../shared/businessKey-helper';
const LOG = cds.log(PROCESS_LOGGER_PREFIX);

// Use InputTreeNode as ProcessStartInput (same structure)
Expand Down Expand Up @@ -73,6 +76,14 @@ export async function handleProcessStart(req: cds.Request, data: EntityRow): Pro
columns = convertToColumnsExpr(startSpecs.inputs);
}

const businessKeyColumn = getBusinessKeyColumnProcessStart(
req,
(target[BUSINESS_KEY] as { '=': string })?.['='],
);
if (businessKeyColumn) {
columns.push(businessKeyColumn);
}

// fetch entity
const row = await resolveEntityRowOrReject(
req,
Expand All @@ -84,20 +95,16 @@ export async function handleProcessStart(req: cds.Request, data: EntityRow): Pro
);
if (!row) return;

// get business key
const businessKey = getBusinessKeyOrReject(
target as cds.entity,
row,
req,
'Failed to build business key for process start.',
'Business key is empty for process start.',
);
if (!businessKey) return;

const context = { ...row, businesskey: businessKey };
if ((row.businessKey as string)?.length > BUSINESS_KEY_MAX_LENGTH) {
const msg = `Business key value exceeds maximum length of ${BUSINESS_KEY_MAX_LENGTH} characters. Process start will fail.`;
LOG.error(msg);
return req.reject({ status: 400, message: msg });
} else {
row.businessKey = undefined;
}

// emit process start
const payload = { definitionId: startSpecs.id!, context };
const payload = { definitionId: startSpecs.id!, context: row };
await emitProcessEvent(
'start',
req,
Expand All @@ -113,7 +120,7 @@ export async function handleProcessStart(req: cds.Request, data: EntityRow): Pro
export const addDeletedEntityToRequestStart = createAddDeletedEntityHandler({
action: 'start',
ifAnnotation: PROCESS_START_IF,
getColumns: (req) => getColumnsForProcessStart(req.target as Target),
getColumns: (req) => addBusinessKeyToStartColumns(req),
});

function initStartSpecs(target: Target): ProcessStartSpec {
Expand Down
Loading