From 9a9a1a741bd7a6f1a67748e2ce441a75d39427fa Mon Sep 17 00:00:00 2001 From: Daniel O'Grady Date: Fri, 12 Jun 2026 12:14:25 +0200 Subject: [PATCH 1/5] Include PATCH and DELETE for _texts --- lib/compile/csdl2openapi.js | 3 +- test/lib/compile/data/autoexposed-texts.json | 11 +- .../data/autoexposed-texts.openapi3.json | 346 ++++++++++++++++++ 3 files changed, 358 insertions(+), 2 deletions(-) diff --git a/lib/compile/csdl2openapi.js b/lib/compile/csdl2openapi.js index 0e7b9f3..e2b9b5d 100644 --- a/lib/compile/csdl2openapi.js +++ b/lib/compile/csdl2openapi.js @@ -499,7 +499,8 @@ module.exports.csdl2openapi = function ( } pathItemsForBoundOperations({ paths, prefix, prefixParameters, element, sourceName }); - if (element.$ContainsTarget) { + const targetIsTexts = target?.['$cds.autoexpose'] || (target?.$Type && nameParts(target.$Type).name.endsWith('_texts')); + if (element.$ContainsTarget || element.$Collection && targetIsTexts) { if (element.$Collection) { if (level < maxLevels) pathItemsWithKey({ diff --git a/test/lib/compile/data/autoexposed-texts.json b/test/lib/compile/data/autoexposed-texts.json index cdea38b..ee73ca1 100644 --- a/test/lib/compile/data/autoexposed-texts.json +++ b/test/lib/compile/data/autoexposed-texts.json @@ -8,7 +8,10 @@ "$Kind": "EntityContainer", "Books": { "$Collection": true, - "$Type": "AdminService.Books" + "$Type": "AdminService.Books", + "$NavigationPropertyBinding": { + "texts": "Books_texts" + } }, "Books_texts": { "$Collection": true, @@ -20,6 +23,12 @@ "$Key": ["ID"], "ID": { "$Type": "Edm.Int32" + }, + "texts": { + "$Kind": "NavigationProperty", + "$Type": "AdminService.Books_texts", + "$Collection": true, + "$OnDelete": "Cascade" } }, "Books_texts": { diff --git a/test/lib/compile/data/autoexposed-texts.openapi3.json b/test/lib/compile/data/autoexposed-texts.openapi3.json index 9a1ab24..2e0e9c3 100644 --- a/test/lib/compile/data/autoexposed-texts.openapi3.json +++ b/test/lib/compile/data/autoexposed-texts.openapi3.json @@ -115,6 +115,23 @@ ] } } + }, + { + "name": "$expand", + "description": "The value of $expand query option is a comma-separated list of navigation property names, stream property names, or $value indicating the stream content of a media-entity. The corresponding related entities and stream values will be represented inline, see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionexpand)", + "in": "query", + "explode": false, + "schema": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "*", + "texts" + ] + } + } } ], "responses": { @@ -212,6 +229,23 @@ ] } } + }, + { + "name": "$expand", + "description": "The value of $expand query option is a comma-separated list of navigation property names, stream property names, or $value indicating the stream content of a media-entity. The corresponding related entities and stream values will be represented inline, see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionexpand)", + "in": "query", + "explode": false, + "schema": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "*", + "texts" + ] + } + } } ], "responses": { @@ -269,6 +303,259 @@ } } } + }, + "/Books({ID})/texts": { + "parameters": [ + { + "description": "key: ID", + "in": "path", + "name": "ID", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "get": { + "summary": "Retrieves a list of texts of a book.", + "tags": [ + "Books", + "Books texts" + ], + "parameters": [ + { + "$ref": "#/components/parameters/top" + }, + { + "$ref": "#/components/parameters/skip" + }, + { + "$ref": "#/components/parameters/search" + }, + { + "name": "$filter", + "description": "Filter items by property values, see [Filtering](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionfilter)", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/count" + }, + { + "name": "$orderby", + "description": "Order items by property values, see [Sorting](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionorderby)", + "in": "query", + "explode": false, + "schema": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "locale", + "locale desc", + "ID", + "ID desc" + ] + } + } + }, + { + "name": "$select", + "description": "Select properties to be returned, see [Select](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionselect)", + "in": "query", + "explode": false, + "schema": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "locale", + "ID" + ] + } + } + } + ], + "responses": { + "200": { + "description": "Retrieved texts", + "content": { + "application/json": { + "schema": { + "type": "object", + "title": "Collection of Books_texts", + "properties": { + "@count": { + "$ref": "#/components/schemas/count" + }, + "value": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AdminService.Books_texts" + } + } + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/error" + } + } + }, + "post": { + "summary": "Creates a single text of a book.", + "tags": [ + "Books", + "Books texts" + ], + "requestBody": { + "description": "New text", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminService.Books_texts-create" + } + } + } + }, + "responses": { + "201": { + "description": "Created text", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminService.Books_texts" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/Books({ID})/texts(locale='{locale_1}',ID={ID_1})": { + "parameters": [ + { + "description": "key: ID", + "in": "path", + "name": "ID", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "description": "key: locale", + "in": "path", + "name": "locale_1", + "required": true, + "schema": { + "type": "string", + "maxLength": 14 + } + }, + { + "description": "key: ID", + "in": "path", + "name": "ID_1", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "get": { + "summary": "Retrieves a single text of a book.", + "tags": [ + "Books", + "Books texts" + ], + "parameters": [ + { + "name": "$select", + "description": "Select properties to be returned, see [Select](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionselect)", + "in": "query", + "explode": false, + "schema": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "locale", + "ID" + ] + } + } + } + ], + "responses": { + "200": { + "description": "Retrieved text", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminService.Books_texts" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/error" + } + } + }, + "patch": { + "summary": "Changes a single text of a book.", + "tags": [ + "Books" + ], + "requestBody": { + "description": "New property values", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminService.Books_texts-update" + } + } + } + }, + "responses": { + "204": { + "description": "Success" + }, + "4XX": { + "$ref": "#/components/responses/error" + } + } + }, + "delete": { + "summary": "Deletes a single text of a book.", + "tags": [ + "Books" + ], + "responses": { + "204": { + "description": "Success" + }, + "4XX": { + "$ref": "#/components/responses/error" + } + } + } } }, "components": { @@ -280,6 +567,15 @@ "ID": { "type": "integer", "format": "int32" + }, + "texts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AdminService.Books_texts" + } + }, + "texts@count": { + "$ref": "#/components/schemas/count" } } }, @@ -290,6 +586,12 @@ "ID": { "type": "integer", "format": "int32" + }, + "texts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AdminService.Books_texts-create" + } } }, "required": [ @@ -298,6 +600,50 @@ }, "AdminService.Books-update": { "title": "Books (for update)", + "type": "object", + "properties": { + "texts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AdminService.Books_texts-create" + } + } + } + }, + "AdminService.Books_texts": { + "title": "Books_texts", + "type": "object", + "properties": { + "locale": { + "type": "string", + "maxLength": 14 + }, + "ID": { + "type": "integer", + "format": "int32" + } + } + }, + "AdminService.Books_texts-create": { + "title": "Books_texts (for create)", + "type": "object", + "properties": { + "locale": { + "type": "string", + "maxLength": 14 + }, + "ID": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "locale", + "ID" + ] + }, + "AdminService.Books_texts-update": { + "title": "Books_texts (for update)", "type": "object" }, "count": { From a8982b4229cb8af0de5a195d9cb75aec77729637 Mon Sep 17 00:00:00 2001 From: Daniel O'Grady Date: Fri, 12 Jun 2026 12:17:22 +0200 Subject: [PATCH 2/5] Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e42a2c..4c30496 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). ### 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 +- `GET`, `PATCH`, and `DELETE` endpoints are now exposed as navigation paths for `*_text` entities ### Security ## [1.4.2] - 2026-05-18 From e2369b0b6d0998b075930c95f3300d78de6ef153 Mon Sep 17 00:00:00 2001 From: Daniel O'Grady Date: Fri, 12 Jun 2026 12:26:02 +0200 Subject: [PATCH 3/5] Fix types --- lib/compile/csdl2openapi.js | 27 ++++++++++++--------------- lib/compile/types.d.ts | 1 + 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/lib/compile/csdl2openapi.js b/lib/compile/csdl2openapi.js index e2b9b5d..6c1ae31 100644 --- a/lib/compile/csdl2openapi.js +++ b/lib/compile/csdl2openapi.js @@ -468,7 +468,7 @@ module.exports.csdl2openapi = function ( * @param {object} options.root Root model element * @param {string} options.sourceName Name of path source * @param {string} options.targetName Name of path target - * @param {null | TargetRestrictions[]} options.target Target container child of path + * @param {null | TargetRestrictions} options.target Target container child of path * @param {number} options.level Number of navigation segments so far * @param {string} options.navigationPath Path for finding navigation restrictions */ @@ -670,7 +670,7 @@ module.exports.csdl2openapi = function ( * @param {string} options.name Name of navigation segment * @param {string} options.sourceName Name of path source * @param {string} options.targetName Name of path target - * @param {null | TargetRestrictions[]} options.target Target container child of path + * @param {null | TargetRestrictions} options.target Target container child of path * @param {number} options.level Number of navigation segments so far * @param {object} options.restrictions Navigation property restrictions of navigation segment */ @@ -744,7 +744,7 @@ module.exports.csdl2openapi = function ( * @param {string} options.name Name of navigation segment * @param {string} options.sourceName Name of path source * @param {string} options.targetName Name of path target - * @param {null | TargetRestrictions[]} options.target Target container child of path + * @param {null | TargetRestrictions} options.target Target container child of path * @param {number} options.level Number of navigation segments so far * @param {object} options.restrictions Navigation property restrictions of navigation segment * @param {boolean} options.byKey Read by key @@ -798,12 +798,9 @@ module.exports.csdl2openapi = function ( customParameters(operation, byKey ? readByKeyRestrictions || readRestrictions : readRestrictions); if (collection) { - // @ts-expect-error - see FIXME in optionTop and optionSkip optionTop(operation.parameters, target, restrictions); - // @ts-expect-error optionSkip(operation.parameters, target, restrictions); if (csdl.$Version >= '4.0') optionSearch(operation.parameters, target, restrictions); - // @ts-expect-error optionFilter(operation.parameters, target, restrictions); optionCount(operation.parameters, target); optionOrderBy(operation.parameters, element, target, restrictions); @@ -860,7 +857,7 @@ module.exports.csdl2openapi = function ( /** * Add parameter for query option $count * @param {Array} parameters Array of parameters to augment - * @param {null | TargetRestrictions[]} target Target container child of path + * @param {null | TargetRestrictions} target Target container child of path */ function optionCount(parameters, target) { const targetRestrictions = target?.[meta.voc.Capabilities.CountRestrictions]; @@ -880,7 +877,7 @@ module.exports.csdl2openapi = function ( * Add parameter for query option $expand * @param {Array} parameters Array of parameters to augment * @param {object} element Model element of navigation segment - * @param {null | TargetRestrictions[]} target Target container child of path + * @param {null | TargetRestrictions} target Target container child of path * @param {array} nonExpandable Non-expandable navigation properties */ function optionExpand(parameters, element, target, nonExpandable) { @@ -967,7 +964,7 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot * Add parameter for query option $orderby * @param {Array} parameters Array of parameters to augment * @param {object} element Model element of navigation segment - * @param {null | TargetRestrictions[]} target Target container child of path + * @param {null | TargetRestrictions} target Target container child of path * @param {object} restrictions Navigation property restrictions of navigation segment */ function optionOrderBy(parameters, element, target, restrictions) { @@ -1119,7 +1116,7 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot /** * Add parameter for query option $search * @param {Array} parameters Array of parameters to augment - * @param {null | TargetRestrictions[]} target Target container child of path + * @param {null | TargetRestrictions} target Target container child of path * @param {object} restrictions Navigation property restrictions of navigation segment */ function optionSearch(parameters, target, restrictions) { @@ -1143,7 +1140,7 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot * Add parameter for query option $select * @param {Array} parameters Array of parameters to augment * @param {object} element Model element of navigation segment - * @param {null | TargetRestrictions[]} target Target container child of path + * @param {null | TargetRestrictions} target Target container child of path * @param {object} restrictions Navigation property restrictions of navigation segment */ function optionSelect(parameters, element, target, restrictions) { @@ -1178,7 +1175,7 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot /** * Add parameter for query option $skip * @param {Array} parameters Array of parameters to augment - * @param {Record} target Target container child of path FIXME: this seems to be an incorrect use of TargetRestrictions + * @param {null | TargetRestrictions} target Target container child of path * @param {object} restrictions Navigation property restrictions of navigation segment */ function optionSkip(parameters, target, restrictions) { @@ -1196,7 +1193,7 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot /** * Add parameter for query option $top * @param {Array} parameters Array of parameters to augment - * @param {Record} target Target container child of path FIXME: this seems to be an incorrect use of TargetRestrictions + * @param {null | TargetRestrictions} target Target container child of path * @param {object} restrictions Navigation property restrictions of navigation segment */ function optionTop(parameters, target, restrictions) { @@ -1218,7 +1215,7 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot * @param {object} options.element Model element of navigation segment * @param {string} options.name Name of navigation segment * @param {string} options.sourceName Name of path source - * @param {null | TargetRestrictions[]} options.target Target container child of path + * @param {null | TargetRestrictions} options.target Target container child of path * @param {number} options.level Number of navigation segments so far * @param {object} options.restrictions Navigation property restrictions of navigation segment * @param {boolean} [options.byKey=false] Update by key @@ -1270,7 +1267,7 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot * @param {object} options.element Model element of navigation segment * @param {string} options.name Name of navigation segment * @param {string} options.sourceName Name of path source - * @param {null | TargetRestrictions[]} options.target Target container child of path + * @param {null | TargetRestrictions} options.target Target container child of path * @param {number} options.level Number of navigation segments so far * @param {object} options.restrictions Navigation property restrictions of navigation segment * @param {boolean} [options.byKey=false] Delete by key diff --git a/lib/compile/types.d.ts b/lib/compile/types.d.ts index a6bb6b1..b774ade 100644 --- a/lib/compile/types.d.ts +++ b/lib/compile/types.d.ts @@ -54,6 +54,7 @@ export type Schema = (SingleSchema | MultiSchema) export type TargetRestrictions = { Countable?: boolean Expandable?: boolean + $Type?: string } // despite how CSDL is defined in the standard, From ed50f3b1d06509d91ba52dd09fd10880940f8461 Mon Sep 17 00:00:00 2001 From: Daniel O'Grady <103028279+daogrady@users.noreply.github.com> Date: Fri, 12 Jun 2026 17:45:34 +0200 Subject: [PATCH 4/5] Update CHANGELOG.md Co-authored-by: Tim Schulze-Hartung <108271660+tim-sh@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c30496..9d80786 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). ### 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 -- `GET`, `PATCH`, and `DELETE` endpoints are now exposed as navigation paths for `*_text` entities +- `GET`, `PATCH`, and `DELETE` endpoints are now exposed as navigation paths for `*_texts` entities ### Security ## [1.4.2] - 2026-05-18 From ff5e5ddede8bf84471160e48d685b436f2f70c03 Mon Sep 17 00:00:00 2001 From: Daniel O'Grady Date: Fri, 12 Jun 2026 17:46:44 +0200 Subject: [PATCH 5/5] Dummy