From 240c3e46661119d5bdb599a55c1bdf52dd9011b6 Mon Sep 17 00:00:00 2001 From: Daniel O'Grady Date: Fri, 21 Aug 2026 11:24:09 +0200 Subject: [PATCH 1/2] Consider codelists readonly --- lib/compile/csdl2openapi.js | 3 +- test/lib/compile/csdl2openapi.test.js | 8 + test/lib/compile/data/autoexposed-direct.json | 46 ++ .../data/autoexposed-direct.openapi3.json | 678 ++++++++++++++++++ 4 files changed, 733 insertions(+), 2 deletions(-) create mode 100644 test/lib/compile/data/autoexposed-direct.json create mode 100644 test/lib/compile/data/autoexposed-direct.openapi3.json diff --git a/lib/compile/csdl2openapi.js b/lib/compile/csdl2openapi.js index fc3f1f0..2fddefd 100644 --- a/lib/compile/csdl2openapi.js +++ b/lib/compile/csdl2openapi.js @@ -125,8 +125,7 @@ module.exports.csdl2openapi = function ( const type = fullTypeName.startsWith(`${serviceName}.`) ? fullTypeName.substring(serviceName.length + 1) : nameParts(fullTypeName).name; - if ((csdl[serviceName]?.[type]?.['@cds.autoexpose'] || csdl[serviceName]?.[type]?.['@cds.autoexposed']) - && (!entityContainer[type] || type.endsWith('_texts'))) { + if (csdl[serviceName]?.[type]?.['@cds.autoexpose'] || csdl[serviceName]?.[type]?.['@cds.autoexposed']) { entityContainer[element]['$cds.autoexpose'] = true; } } diff --git a/test/lib/compile/csdl2openapi.test.js b/test/lib/compile/csdl2openapi.test.js index 8507c4f..e50aec8 100644 --- a/test/lib/compile/csdl2openapi.test.js +++ b/test/lib/compile/csdl2openapi.test.js @@ -49,6 +49,9 @@ const result11 = require("./data/description-fallback.openapi3.json"); const example12 = require("./data/autoexposed-texts.json"); const result12 = require("./data/autoexposed-texts.openapi3.json"); +const example13 = require("./data/autoexposed-direct.json"); +const result13 = require("./data/autoexposed-direct.openapi3.json"); + describe("Examples", () => { test("csdl-16.1", () => { const openapi = lib.csdl2openapi(example1, { diagram: true }); @@ -103,6 +106,11 @@ describe("Examples", () => { const openapi = lib.csdl2openapi(example12); check(openapi, result12); }); + + test("autoexposed-direct", () => { + const openapi = lib.csdl2openapi(example13, { url: "https://localhost/service-root" }); + check(openapi, result13); + }); }); describe("Edge cases", () => { diff --git a/test/lib/compile/data/autoexposed-direct.json b/test/lib/compile/data/autoexposed-direct.json new file mode 100644 index 0000000..2d79d7e --- /dev/null +++ b/test/lib/compile/data/autoexposed-direct.json @@ -0,0 +1,46 @@ +{ + "$Version": "4.01", + "$EntityContainer": "MyService.EntityContainer", + "$Reference": {}, + "MyService": { + "$Kind": "Schema", + "EntityContainer": { + "$Kind": "EntityContainer", + "Orders": { + "$Collection": true, + "$Type": "MyService.Orders", + "$NavigationPropertyBinding": { + "status": "ScanStates" + } + }, + "ScanStates": { + "$Collection": true, + "$Type": "MyService.ScanStates" + } + }, + "Orders": { + "$Kind": "EntityType", + "$Key": ["ID"], + "ID": { + "$Type": "Edm.Int32" + }, + "status": { + "$Kind": "NavigationProperty", + "$Type": "MyService.ScanStates", + "$Nullable": true + } + }, + "ScanStates": { + "$Kind": "EntityType", + "$Key": ["code"], + "@cds.autoexpose": true, + "code": { + "$MaxLength": 255 + }, + "descr": { + "$Nullable": true, + "$MaxLength": 1000 + } + } + } +} diff --git a/test/lib/compile/data/autoexposed-direct.openapi3.json b/test/lib/compile/data/autoexposed-direct.openapi3.json new file mode 100644 index 0000000..2e8021a --- /dev/null +++ b/test/lib/compile/data/autoexposed-direct.openapi3.json @@ -0,0 +1,678 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Use the title annotation on your CDS service to provide a meaningful title.", + "description": "Use the Core.LongDescription or Core.Description annotation on your CDS service to provide a meaningful description.", + "version": "" + }, + "x-sap-api-type": "ODATAV4", + "x-odata-version": "4.01", + "x-sap-shortText": "Use the Core.Description annotation on your CDS service to provide a meaningful short text.", + "servers": [ + { + "url": "https://localhost/service-root" + } + ], + "tags": [ + { + "name": "Orders" + }, + { + "name": "Scan States" + } + ], + "paths": { + "/$batch": { + "post": { + "summary": "Sends a group of requests", + "description": "Group multiple requests into a single request payload, see [Batch Requests](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_BatchRequests).\n\n*Please note that \"Try it out\" is not supported for this request.*", + "tags": [ + "Batch Requests" + ], + "requestBody": { + "required": true, + "description": "Batch request", + "content": { + "multipart/mixed;boundary=request-separator": { + "schema": { + "type": "string" + }, + "example": "--request-separator\nContent-Type: application/http\nContent-Transfer-Encoding: binary\n\nGET Orders HTTP/1.1\nAccept: application/json\n\n\n--request-separator--" + } + } + }, + "responses": { + "200": { + "description": "Batch response", + "content": { + "multipart/mixed": { + "schema": { + "type": "string" + }, + "example": "--response-separator\nContent-Type: application/http\n\nHTTP/1.1 200 OK\nContent-Type: application/json\n\n{...}\n--response-separator--" + } + } + }, + "4XX": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/Orders": { + "get": { + "summary": "Retrieves a list of orders.", + "tags": [ + "Orders" + ], + "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": [ + "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": [ + "ID" + ] + } + } + }, + { + "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": [ + "*", + "status" + ] + } + } + } + ], + "responses": { + "200": { + "description": "Retrieved orders", + "content": { + "application/json": { + "schema": { + "type": "object", + "title": "Collection of Orders", + "properties": { + "@count": { + "$ref": "#/components/schemas/count" + }, + "value": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MyService.Orders" + } + } + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/error" + } + } + }, + "post": { + "summary": "Creates a single order.", + "tags": [ + "Orders" + ], + "requestBody": { + "description": "New order", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MyService.Orders-create" + } + } + } + }, + "responses": { + "201": { + "description": "Created order", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MyService.Orders" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/Orders({ID})": { + "parameters": [ + { + "description": "key: ID", + "in": "path", + "name": "ID", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "get": { + "summary": "Retrieves a single order.", + "tags": [ + "Orders" + ], + "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": [ + "ID" + ] + } + } + }, + { + "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": [ + "*", + "status" + ] + } + } + } + ], + "responses": { + "200": { + "description": "Retrieved order", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MyService.Orders" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/error" + } + } + }, + "patch": { + "summary": "Changes a single order.", + "tags": [ + "Orders" + ], + "requestBody": { + "description": "New property values", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MyService.Orders-update" + } + } + } + }, + "responses": { + "204": { + "description": "Success" + }, + "4XX": { + "$ref": "#/components/responses/error" + } + } + }, + "delete": { + "summary": "Deletes a single order.", + "tags": [ + "Orders" + ], + "responses": { + "204": { + "description": "Success" + }, + "4XX": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/Orders({ID})/status": { + "parameters": [ + { + "description": "key: ID", + "in": "path", + "name": "ID", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "get": { + "summary": "Retrieves status of a order.", + "tags": [ + "Orders", + "Scan States" + ], + "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": [ + "code", + "descr" + ] + } + } + } + ], + "responses": { + "200": { + "description": "Retrieved status", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MyService.ScanStates" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/ScanStates": { + "get": { + "summary": "Retrieves a list of scan states.", + "tags": [ + "Scan States" + ], + "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": [ + "code", + "code desc", + "descr", + "descr 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": [ + "code", + "descr" + ] + } + } + } + ], + "responses": { + "200": { + "description": "Retrieved scan states", + "content": { + "application/json": { + "schema": { + "type": "object", + "title": "Collection of ScanStates", + "properties": { + "@count": { + "$ref": "#/components/schemas/count" + }, + "value": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MyService.ScanStates" + } + } + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/ScanStates('{code}')": { + "parameters": [ + { + "description": "key: code", + "in": "path", + "name": "code", + "required": true, + "schema": { + "type": "string", + "maxLength": 255 + } + } + ], + "get": { + "summary": "Retrieves a single scan state.", + "tags": [ + "Scan States" + ], + "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": [ + "code", + "descr" + ] + } + } + } + ], + "responses": { + "200": { + "description": "Retrieved scan state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MyService.ScanStates" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/error" + } + } + } + } + }, + "components": { + "schemas": { + "MyService.Orders": { + "title": "Orders", + "type": "object", + "properties": { + "ID": { + "type": "integer", + "format": "int32" + }, + "status": { + "allOf": [ + { + "$ref": "#/components/schemas/MyService.ScanStates" + } + ], + "nullable": true + } + } + }, + "MyService.Orders-create": { + "title": "Orders (for create)", + "type": "object", + "properties": { + "ID": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "ID" + ] + }, + "MyService.Orders-update": { + "title": "Orders (for update)", + "type": "object" + }, + "MyService.ScanStates": { + "title": "ScanStates", + "type": "object", + "properties": { + "code": { + "type": "string", + "maxLength": 255 + }, + "descr": { + "type": "string", + "maxLength": 1000, + "nullable": true + } + } + }, + "count": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string" + } + ], + "description": "The number of entities in the collection. Available when using the [$count](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptioncount) query option." + }, + "error": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "target": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "target": { + "type": "string" + } + } + } + }, + "innererror": { + "type": "object", + "description": "The structure of this object is service-specific" + } + } + } + } + } + }, + "parameters": { + "top": { + "name": "$top", + "in": "query", + "description": "Show only the first n items, see [Paging - Top](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptiontop)", + "schema": { + "type": "integer", + "minimum": 0 + }, + "example": 50 + }, + "skip": { + "name": "$skip", + "in": "query", + "description": "Skip the first n items, see [Paging - Skip](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionskip)", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + "count": { + "name": "$count", + "in": "query", + "description": "Include count of items, see [Count](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptioncount)", + "schema": { + "type": "boolean" + } + }, + "search": { + "name": "$search", + "in": "query", + "description": "Search items by search phrases, see [Searching](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionsearch)", + "schema": { + "type": "string" + } + } + }, + "responses": { + "error": { + "description": "Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/error" + } + } + } + } + } + } +} \ No newline at end of file From 8bf1b0a12600c7a47e4ec5477f12088b8e3b0416 Mon Sep 17 00:00:00 2001 From: Daniel O'Grady Date: Fri, 21 Aug 2026 11:24:54 +0200 Subject: [PATCH 2/2] Changelo --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4285a52..c7be42e 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 +- Entities that are transitively autoexposed and should still be considered readonly, do not generate documentation for write endpoints anymore ### Security ## [1.6.0] - 2026-08-04