Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
### Removed
### Fixed
- Restore bound actions/functions on containment navigation paths (regression since v1.2.0)
- Function parameters annotated with `@mandatory` no longer lead to "Unexpected mandatory after optional parameter" error
### Security

## [1.4.2] - 2026-05-18
Expand Down
20 changes: 20 additions & 0 deletions lib/compile/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ function processor(csn, options = {}) {
, ...options
}

_applyMandatoryToFunctionParams(csn);

// must not be part of function* otherwise thrown errors are swallowed
const csdl = cds.compile.to.edm(csn, edmOptions);
let openApiDocs = {};
Expand Down Expand Up @@ -229,6 +231,24 @@ function _readConfigFile(configFilePath) {
return result;
}

/**
* The cds-compiler's edmPreprocessor determines function parameter optionality
* from the type's nullability (p.notNull), ignoring the CDS `@mandatory` annotation.
* A nullable-typed parameter annotated @mandatory` is incorrectly treated as optional,
* which then triggers an odata-parameter-order error if a non-nullable parameter follows.
*
* Fix: honour `@mandatory` by setting notNull on the CSN param before EDM compilation.
*/
function _applyMandatoryToFunctionParams(csn) {
const functions = Object.values(csn.definitions ?? {})
.filter(def => def.kind === 'function')
for (const f of functions) {
for (const p of Object.values(f.params ?? {})) {
p.notNull ??= p['@mandatory'] === true
}
}
}

// we're attaching the events to the main function so they become automatically exposed through the cds facade:
// cds.compile.to.openapi(...)
// cds.compile.to.openapi.events.before
Expand Down
18 changes: 18 additions & 0 deletions test/lib/compile/openapi.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -684,4 +684,22 @@ service CatalogService {
cds.removeListener(events.after, handler);
}
});

test('@mandatory on function parameters with nullable types compiles without error', () => {
const csn = cds.compile.to.csn(`
service ReproService {
function getMapping(
@mandatory paramA: String,
@mandatory paramB: String not null
) returns String;
}
`);
const openAPI = toOpenApi(csn);
const params = openAPI.paths['/getMapping'].get.parameters;
assert.strictEqual(params.length, 2);
assert.strictEqual(params[0].name, 'paramA');
assert.strictEqual(params[0].required, true);
assert.strictEqual(params[1].name, 'paramB');
assert.strictEqual(params[1].required, true);
});
});
Loading