From b946582032018dad15c6189f65bb6beb243e896f Mon Sep 17 00:00:00 2001 From: Daniel O'Grady Date: Mon, 8 Jun 2026 09:33:27 +0200 Subject: [PATCH 1/2] Honor annotation --- CHANGELOG.md | 1 + lib/compile/index.js | 20 ++++++++++++++++++++ test/lib/compile/openapi.test.js | 18 ++++++++++++++++++ 3 files changed, 39 insertions(+) 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..31dc8a8 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); + }); }); From d506507138c8154217cb5b79310fc5be279a1a2f Mon Sep 17 00:00:00 2001 From: Daniel O'Grady Date: Wed, 10 Jun 2026 11:20:23 +0200 Subject: [PATCH 2/2] Apply review feedback --- lib/compile/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/compile/index.js b/lib/compile/index.js index 31dc8a8..1bfb40f 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -241,10 +241,10 @@ function _readConfigFile(configFilePath) { */ function _applyMandatoryToFunctionParams(csn) { const functions = Object.values(csn.definitions ?? {}) - .filter(def => def.kind === 'function') + .filter(def => def.kind === 'function'); for (const f of functions) { for (const p of Object.values(f.params ?? {})) { - p.notNull ??= p['@mandatory'] === true + p.notNull ??= p['@mandatory'] === true; } } }