diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f2381a..def3f97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/lib/compile/index.js b/lib/compile/index.js index d321158..1bfb40f 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -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 = {}; @@ -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 diff --git a/test/lib/compile/openapi.test.js b/test/lib/compile/openapi.test.js index 812ec23..79e5374 100644 --- a/test/lib/compile/openapi.test.js +++ b/test/lib/compile/openapi.test.js @@ -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); + }); });