diff --git a/CHANGELOG.md b/CHANGELOG.md index 82991c4..1f93290 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). ### Deprecated ### Removed ### Fixed +- Autoexposed `.texts` entities are now excluded from OpenAPI document + ### Security ## [1.3.1] - 2026-01-08 diff --git a/lib/compile/csdl2openapi.js b/lib/compile/csdl2openapi.js index 04f7ebd..7ed02a7 100644 --- a/lib/compile/csdl2openapi.js +++ b/lib/compile/csdl2openapi.js @@ -119,9 +119,14 @@ module.exports.csdl2openapi = function ( const serviceName = nameParts(csdl.$EntityContainer).qualifier; Object.keys(entityContainer).forEach(element => { if (entityContainer[element].$Type) { - const type = nameParts(entityContainer[element].$Type).name; - if ((csdl[serviceName]?.[type]?.['@cds.autoexpose'] || csdl[serviceName]?.[type]?.['@cds.autoexposed']) && !entityContainer[type]) + const fullTypeName = entityContainer[element].$Type; + 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'))) { entityContainer[element]['$cds.autoexpose'] = true; + } } }); } @@ -374,6 +379,9 @@ module.exports.csdl2openapi = function ( Object.keys(container) .filter(name => isIdentifier(name) && container[name].$Type) .forEach(child => { + if (child.endsWith('_texts') && container[child]['$cds.autoexpose']) { + return; + } const type = meta.modelElement(container[child].$Type) || {}; const tag = { name: type[meta.voc.Common.Label] || child @@ -416,6 +424,9 @@ module.exports.csdl2openapi = function ( const resources = Object.keys(container).filter(name => isIdentifier(name)); resources.forEach(name => { const child = container[name]; + if (name.endsWith('_texts') && child['$cds.autoexpose']) { + return; + } if (child.$Type) { const type = meta.modelElement(child.$Type); const sourceName = (type && type[meta.voc.Common.Label]) || name; diff --git a/test/lib/compile/csdl2openapi.test.js b/test/lib/compile/csdl2openapi.test.js index 441973c..010098f 100644 --- a/test/lib/compile/csdl2openapi.test.js +++ b/test/lib/compile/csdl2openapi.test.js @@ -46,6 +46,9 @@ const result10 = require("./data/immutable-composition.openapi3.json"); const example11 = require("./data/description-fallback.json"); const result11 = require("./data/description-fallback.openapi3.json"); +const example12 = require("./data/autoexposed-texts.json"); +const result12 = require("./data/autoexposed-texts.openapi3.json"); + describe("Examples", () => { test("csdl-16.1", () => { const openapi = lib.csdl2openapi(example1, { diagram: true }); @@ -95,6 +98,11 @@ describe("Examples", () => { const openapi = lib.csdl2openapi(example11, { diagram: true }); check(openapi, result11); }); + + test("autoexposed-texts", () => { + const openapi = lib.csdl2openapi(example12); + check(openapi, result12); + }); }); describe("Edge cases", () => { diff --git a/test/lib/compile/data/autoexposed-texts.json b/test/lib/compile/data/autoexposed-texts.json new file mode 100644 index 0000000..cdea38b --- /dev/null +++ b/test/lib/compile/data/autoexposed-texts.json @@ -0,0 +1,37 @@ +{ + "$Version": "4.01", + "$EntityContainer": "AdminService.EntityContainer", + "$Reference": {}, + "AdminService": { + "$Kind": "Schema", + "EntityContainer": { + "$Kind": "EntityContainer", + "Books": { + "$Collection": true, + "$Type": "AdminService.Books" + }, + "Books_texts": { + "$Collection": true, + "$Type": "AdminService.Books_texts" + } + }, + "Books": { + "$Kind": "EntityType", + "$Key": ["ID"], + "ID": { + "$Type": "Edm.Int32" + } + }, + "Books_texts": { + "$Kind": "EntityType", + "$Key": ["locale", "ID"], + "@cds.autoexposed": true, + "locale": { + "$MaxLength": 14 + }, + "ID": { + "$Type": "Edm.Int32" + } + } + } +} diff --git a/test/lib/compile/data/autoexposed-texts.openapi3.json b/test/lib/compile/data/autoexposed-texts.openapi3.json new file mode 100644 index 0000000..38145cc --- /dev/null +++ b/test/lib/compile/data/autoexposed-texts.openapi3.json @@ -0,0 +1,416 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Use @title: '...' on your CDS service to provide a meaningful title.", + "description": "Use @Core.LongDescription: '...' or @Core.Description: '...' on your CDS service to provide a meaningful description.", + "version": "" + }, + "x-sap-api-type": "ODATAV4", + "x-odata-version": "4.01", + "x-sap-shortText": "Use @Core.Description: '...' on your CDS service to provide a meaningful short text.", + "servers": [ + { + "url": "https://localhost/service-root" + } + ], + "tags": [ + { + "name": "Books" + } + ], + "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 Books 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" + } + } + } + }, + "/Books": { + "get": { + "summary": "Retrieves a list of books.", + "tags": [ + "Books" + ], + "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" + ] + } + } + } + ], + "responses": { + "200": { + "description": "Retrieved books", + "content": { + "application/json": { + "schema": { + "type": "object", + "title": "Collection of Books", + "properties": { + "@count": { + "$ref": "#/components/schemas/count" + }, + "value": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AdminService.Books" + } + } + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/error" + } + } + }, + "post": { + "summary": "Creates a single book.", + "tags": [ + "Books" + ], + "requestBody": { + "description": "New book", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminService.Books-create" + } + } + } + }, + "responses": { + "201": { + "description": "Created book", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminService.Books" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/Books({ID})": { + "parameters": [ + { + "description": "key: ID", + "in": "path", + "name": "ID", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "get": { + "summary": "Retrieves a single book.", + "tags": [ + "Books" + ], + "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" + ] + } + } + } + ], + "responses": { + "200": { + "description": "Retrieved book", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminService.Books" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/error" + } + } + }, + "patch": { + "summary": "Changes a single book.", + "tags": [ + "Books" + ], + "requestBody": { + "description": "New property values", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminService.Books-update" + } + } + } + }, + "responses": { + "204": { + "description": "Success" + }, + "4XX": { + "$ref": "#/components/responses/error" + } + } + }, + "delete": { + "summary": "Deletes a single book.", + "tags": [ + "Books" + ], + "responses": { + "204": { + "description": "Success" + }, + "4XX": { + "$ref": "#/components/responses/error" + } + } + } + } + }, + "components": { + "schemas": { + "AdminService.Books": { + "title": "Books", + "type": "object", + "properties": { + "ID": { + "type": "integer", + "format": "int32" + } + } + }, + "AdminService.Books-create": { + "title": "Books (for create)", + "type": "object", + "properties": { + "ID": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "ID" + ] + }, + "AdminService.Books-update": { + "title": "Books (for update)", + "type": "object" + }, + "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" + } + } + } + } + } + } +}