From 0cee7930987ffd0c12f4d4f87a98dbcddf432900 Mon Sep 17 00:00:00 2001 From: Tim Schulze-Hartung Date: Mon, 4 May 2026 10:55:23 +0200 Subject: [PATCH] fix: restore bound operations on containment navigation paths --- CHANGELOG.md | 1 + lib/compile/csdl2openapi.js | 3 +- scripts/regenerate.js | 3 +- test/lib/compile/data/TripPin.openapi3.json | 103 ++++ .../compile/data/containment.openapi3.json | 448 ++++++++++++++++++ .../compile/data/descriptions.openapi3.json | 234 +++++++++ 6 files changed, 789 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90b2dea..47093bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). ### Deprecated ### Removed ### Fixed +- Restore bound actions/functions on containment navigation paths (regression since v1.2.0) ### Security ## [1.4.0] - 2026-03-18 diff --git a/lib/compile/csdl2openapi.js b/lib/compile/csdl2openapi.js index 75a34a2..664dffb 100644 --- a/lib/compile/csdl2openapi.js +++ b/lib/compile/csdl2openapi.js @@ -1339,8 +1339,7 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot * @param {boolean} byKey read by key */ function pathItemsForBoundOperations(paths, prefix, prefixParameters, element, sourceName, byKey = false) { - //ignore operations on navigation path - if (element.$Kind === "NavigationProperty") { + if (element.$Kind === "NavigationProperty" && !element.$ContainsTarget) { return; } const overloads = meta.boundOverloads[element.$Type + (!byKey && element.$Collection ? '-c' : '')] || []; diff --git a/scripts/regenerate.js b/scripts/regenerate.js index bd408f1..9cc4418 100644 --- a/scripts/regenerate.js +++ b/scripts/regenerate.js @@ -13,7 +13,8 @@ const specialOptions = { host: 'services.odata.org', basePath: '/V4/(S(cnbm44wtbc1v5bgrlek5lpcc))/TripPinServiceRW', diagram: true - } + }, + 'autoexposed-texts': {} }; // Default options for all other test cases. diff --git a/test/lib/compile/data/TripPin.openapi3.json b/test/lib/compile/data/TripPin.openapi3.json index 49307f8..90623be 100644 --- a/test/lib/compile/data/TripPin.openapi3.json +++ b/test/lib/compile/data/TripPin.openapi3.json @@ -1216,6 +1216,53 @@ } } }, + "/Me/Trips({TripId_1})/Microsoft.OData.SampleService.Models.TripPin.GetInvolvedPeople": { + "get": { + "summary": "Invokes function GetInvolvedPeople", + "tags": [ + "Me" + ], + "parameters": [ + { + "description": "key: TripId", + "in": "path", + "name": "TripId_1", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "title": "Collection of Person", + "properties": { + "@count": { + "$ref": "#/components/schemas/count" + }, + "value": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Microsoft.OData.SampleService.Models.TripPin.Person" + } + } + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/error" + } + } + } + }, "/Me/Trips({TripId_1})/Photos": { "parameters": [ { @@ -2518,6 +2565,62 @@ } } }, + "/People('{UserName}')/Trips({TripId_1})/Microsoft.OData.SampleService.Models.TripPin.GetInvolvedPeople": { + "get": { + "summary": "Invokes function GetInvolvedPeople", + "tags": [ + "People" + ], + "parameters": [ + { + "description": "key: UserName", + "in": "path", + "name": "UserName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "key: TripId", + "in": "path", + "name": "TripId_1", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "title": "Collection of Person", + "properties": { + "@count": { + "$ref": "#/components/schemas/count" + }, + "value": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Microsoft.OData.SampleService.Models.TripPin.Person" + } + } + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/error" + } + } + } + }, "/People('{UserName}')/Trips({TripId_1})/Photos": { "parameters": [ { diff --git a/test/lib/compile/data/containment.openapi3.json b/test/lib/compile/data/containment.openapi3.json index 719a920..458a9e7 100644 --- a/test/lib/compile/data/containment.openapi3.json +++ b/test/lib/compile/data/containment.openapi3.json @@ -2452,6 +2452,143 @@ } } }, + "/TheWhole/Many({index_1})/self.Like": { + "post": { + "summary": "I like this part", + "tags": [ + "The Whole" + ], + "responses": { + "204": { + "description": "Success" + }, + "4XX": { + "$ref": "#/components/responses/error" + } + }, + "parameters": [ + { + "description": "key: index", + "in": "path", + "name": "index_1", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + } + }, + "/TheWhole/Many({index_1})/self.Likes": { + "get": { + "summary": "How many like this part", + "tags": [ + "The Whole" + ], + "parameters": [ + { + "description": "key: index", + "in": "path", + "name": "index_1", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "integer", + "format": "int32", + "nullable": true + } + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/TheWhole/Many/self.Like": { + "post": { + "summary": "I like all of these parts", + "tags": [ + "The Whole" + ], + "responses": { + "204": { + "description": "Success" + }, + "4XX": { + "$ref": "#/components/responses/error" + } + }, + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + } + }, + "/TheWhole/Many/self.Likes": { + "get": { + "summary": "How many like these parts", + "tags": [ + "The Whole" + ], + "parameters": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "integer", + "format": "int32", + "nullable": true + } + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/error" + } + } + } + }, "/TheWhole/One": { "get": { "summary": "Retrieves one of a the whole.", @@ -2861,6 +2998,63 @@ } } }, + "/TheWhole/One/self.Like": { + "post": { + "summary": "I like this part", + "tags": [ + "The Whole" + ], + "responses": { + "204": { + "description": "Success" + }, + "4XX": { + "$ref": "#/components/responses/error" + } + }, + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + } + }, + "/TheWhole/One/self.Likes": { + "get": { + "summary": "How many like this part", + "tags": [ + "The Whole" + ], + "parameters": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "integer", + "format": "int32", + "nullable": true + } + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/error" + } + } + } + }, "/TheWhole/self.Like": { "post": { "summary": "I like this whole", @@ -3994,6 +4188,182 @@ } } }, + "/Wholes('{ID}')/Many({index_1})/self.Like": { + "post": { + "summary": "I like this part", + "tags": [ + "Wholes" + ], + "responses": { + "204": { + "description": "Success" + }, + "4XX": { + "$ref": "#/components/responses/error" + } + }, + "parameters": [ + { + "description": "key: ID", + "in": "path", + "name": "ID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "key: index", + "in": "path", + "name": "index_1", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + } + }, + "/Wholes('{ID}')/Many({index_1})/self.Likes": { + "get": { + "summary": "How many like this part", + "tags": [ + "Wholes" + ], + "parameters": [ + { + "description": "key: ID", + "in": "path", + "name": "ID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "key: index", + "in": "path", + "name": "index_1", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "integer", + "format": "int32", + "nullable": true + } + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/Wholes('{ID}')/Many/self.Like": { + "post": { + "summary": "I like all of these parts", + "tags": [ + "Wholes" + ], + "responses": { + "204": { + "description": "Success" + }, + "4XX": { + "$ref": "#/components/responses/error" + } + }, + "parameters": [ + { + "description": "key: ID", + "in": "path", + "name": "ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + } + }, + "/Wholes('{ID}')/Many/self.Likes": { + "get": { + "summary": "How many like these parts", + "tags": [ + "Wholes" + ], + "parameters": [ + { + "description": "key: ID", + "in": "path", + "name": "ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "integer", + "format": "int32", + "nullable": true + } + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/error" + } + } + } + }, "/Wholes('{ID}')/One": { "parameters": [ { @@ -4445,6 +4815,84 @@ } } }, + "/Wholes('{ID}')/One/self.Like": { + "post": { + "summary": "I like this part", + "tags": [ + "Wholes" + ], + "responses": { + "204": { + "description": "Success" + }, + "4XX": { + "$ref": "#/components/responses/error" + } + }, + "parameters": [ + { + "description": "key: ID", + "in": "path", + "name": "ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + } + }, + "/Wholes('{ID}')/One/self.Likes": { + "get": { + "summary": "How many like this part", + "tags": [ + "Wholes" + ], + "parameters": [ + { + "description": "key: ID", + "in": "path", + "name": "ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "integer", + "format": "int32", + "nullable": true + } + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/error" + } + } + } + }, "/Wholes('{ID}')/self.Like": { "post": { "summary": "I like this whole", diff --git a/test/lib/compile/data/descriptions.openapi3.json b/test/lib/compile/data/descriptions.openapi3.json index 8b94166..7bba0a5 100644 --- a/test/lib/compile/data/descriptions.openapi3.json +++ b/test/lib/compile/data/descriptions.openapi3.json @@ -813,6 +813,70 @@ "description": "Delete Contained - LongDescription" } }, + "/entities('{id}')/contained('{id_1}')/self.action": { + "post": { + "summary": "Action Bound Overload Ext subEntity - Description", + "tags": [ + "entities" + ], + "responses": { + "204": { + "description": "Success" + }, + "418": { + "description": "Out of coffee on bound action call on subEntity", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/error" + } + } + } + } + }, + "description": "Action Bound Overload subEntity - LongDescription", + "parameters": [ + { + "description": "Property - LongDescription", + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string", + "maxLength": 70, + "default": "0000" + } + }, + { + "description": "key: id", + "in": "path", + "name": "id_1", + "required": true, + "schema": { + "type": "string", + "maxLength": 70 + } + } + ], + "requestBody": { + "description": "Action parameters", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "nonbinding": { + "type": "string", + "nullable": true, + "description": "Action Bound Overload Non-Binding Parameter subEntity - LongDescription" + } + } + } + } + } + } + } + }, "/entities('{id}')/related": { "parameters": [ { @@ -1600,6 +1664,70 @@ "description": "Delete Contained Ext - LongDescription" } }, + "/entities_ext('{id}')/contained('{id_1}')/self.action": { + "post": { + "summary": "Action Bound Overload Ext subEntity - Description", + "tags": [ + "entities ext" + ], + "responses": { + "204": { + "description": "Success" + }, + "418": { + "description": "Out of coffee on bound action call on subEntity", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/error" + } + } + } + } + }, + "description": "Action Bound Overload subEntity - LongDescription", + "parameters": [ + { + "description": "Property - LongDescription", + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string", + "maxLength": 70, + "default": "0000" + } + }, + { + "description": "key: id", + "in": "path", + "name": "id_1", + "required": true, + "schema": { + "type": "string", + "maxLength": 70 + } + } + ], + "requestBody": { + "description": "Action parameters", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "nonbinding": { + "type": "string", + "nullable": true, + "description": "Action Bound Overload Non-Binding Parameter subEntity - LongDescription" + } + } + } + } + } + } + } + }, "/entities_ext('{id}')/related": { "parameters": [ { @@ -2385,6 +2513,59 @@ "description": "Singleton Delete Contained - LongDescription" } }, + "/single/contained('{id_1}')/self.action": { + "post": { + "summary": "Action Bound Overload Ext subEntity - Description", + "tags": [ + "single" + ], + "responses": { + "204": { + "description": "Success" + }, + "418": { + "description": "Out of coffee on bound action call on subEntity", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/error" + } + } + } + } + }, + "description": "Action Bound Overload subEntity - LongDescription", + "parameters": [ + { + "description": "key: id", + "in": "path", + "name": "id_1", + "required": true, + "schema": { + "type": "string", + "maxLength": 70 + } + } + ], + "requestBody": { + "description": "Action parameters", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "nonbinding": { + "type": "string", + "nullable": true, + "description": "Action Bound Overload Non-Binding Parameter subEntity - LongDescription" + } + } + } + } + } + } + } + }, "/single/related": { "get": { "summary": "Singleton Query Related - Description", @@ -2941,6 +3122,59 @@ "description": "Singleton Delete Contained Ext - LongDescription" } }, + "/single_ext/contained('{id_1}')/self.action": { + "post": { + "summary": "Action Bound Overload Ext subEntity - Description", + "tags": [ + "single ext" + ], + "responses": { + "204": { + "description": "Success" + }, + "418": { + "description": "Out of coffee on bound action call on subEntity", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/error" + } + } + } + } + }, + "description": "Action Bound Overload subEntity - LongDescription", + "parameters": [ + { + "description": "key: id", + "in": "path", + "name": "id_1", + "required": true, + "schema": { + "type": "string", + "maxLength": 70 + } + } + ], + "requestBody": { + "description": "Action parameters", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "nonbinding": { + "type": "string", + "nullable": true, + "description": "Action Bound Overload Non-Binding Parameter subEntity - LongDescription" + } + } + } + } + } + } + } + }, "/single_ext/related": { "get": { "summary": "Singleton Query Related Ext - Description",