diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a40a6f..03d2d5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). - Back-reference navigation properties on child entities in compositions (the generated `parent` nav pointing back up) are no longer emitted in the OpenAPI read schema - Implicitly auto-exposed composition targets (annotated `@cds.autoexposed` by the CDS compiler) no longer generate top-level GET paths that CAP would reject with 405 - OpenAPI compilation is now pure: the input CSN is no longer mutated during compilation +- Services annotated with `@protocol: 'rest'` no longer include a `/$batch` path in the generated OpenAPI document +- Services annotated with `@protocol: 'rest'` no longer include OData query options (`$filter`, `$top`, `$skip`, `$search`, `$count`, `$orderby`, `$select`, `$expand`) in collection GET operations, as CAP's REST adapter does not support them ### Security ## [1.6.0] - 2026-08-04 diff --git a/lib/compile/csdl2openapi.js b/lib/compile/csdl2openapi.js index fb7aafc..f1997c2 100644 --- a/lib/compile/csdl2openapi.js +++ b/lib/compile/csdl2openapi.js @@ -9,6 +9,8 @@ const propertyUtil = require('./property-util'); const pluralize = require('pluralize') const DEBUG = cds.debug('openapi'); // Initialize cds.debug with the 'openapi' +const isODataProtocol = (protocol) => !protocol || protocol.startsWith('odata'); + //TODO // - Core.Example for complex types // - reduce number of loops over schemas @@ -122,7 +124,7 @@ function normalizeDiagramFlag(diagram) { /** * Construct an OpenAPI description from a CSDL document * @param {CSDL} csdl CSDL document - * @param {{ url?: string, servers?: object, odataVersion?: string, scheme?: string, host?: string, basePath?: string, diagram?: boolean, maxLevels?: number, shortActionPaths?: boolean }} options Optional parameters + * @param {{ url?: string, servers?: object, odataVersion?: string, protocol?: string, scheme?: string, host?: string, basePath?: string, diagram?: boolean, maxLevels?: number, shortActionPaths?: boolean }} options Optional parameters * @return {*} OpenAPI description */ module.exports.csdl2openapi = function ( @@ -131,6 +133,7 @@ module.exports.csdl2openapi = function ( url: serviceRoot, servers: serversObject, odataVersion, + protocol, scheme = 'https', host = 'localhost', basePath = '/service-root', @@ -511,7 +514,7 @@ module.exports.csdl2openapi = function ( DEBUG?.(`Unrecognized entity container child: ${name}`); } }) - if (resources.length > 0) pathItemBatch(paths, container); + if (resources.length > 0 && isODataProtocol(protocol)) pathItemBatch(paths, container); return Object.keys(paths).sort().reduce((p, c) => (p[c] = paths[c], p), {}); } @@ -820,17 +823,19 @@ module.exports.csdl2openapi = function ( }); customParameters(operation, byKey ? readByKeyRestrictions || readRestrictions : readRestrictions); - if (collection) { - optionTop(operation.parameters, target, restrictions); - optionSkip(operation.parameters, target, restrictions); - if (csdl.$Version >= '4.0') optionSearch(operation.parameters, target, restrictions); - optionFilter(operation.parameters, target, restrictions); - optionCount(operation.parameters, target); - optionOrderBy(operation.parameters, element, target, restrictions); - } + if (isODataProtocol(protocol)) { + if (collection) { + optionTop(operation.parameters, target, restrictions); + optionSkip(operation.parameters, target, restrictions); + if (csdl.$Version >= '4.0') optionSearch(operation.parameters, target, restrictions); + optionFilter(operation.parameters, target, restrictions); + optionCount(operation.parameters, target); + optionOrderBy(operation.parameters, element, target, restrictions); + } - optionSelect(operation.parameters, element, target, restrictions); - optionExpand(operation.parameters, element, target, nonExpandable); + optionSelect(operation.parameters, element, target, restrictions); + optionExpand(operation.parameters, element, target, nonExpandable); + } pathItem.get = operation; } diff --git a/lib/compile/index.js b/lib/compile/index.js index d93acd4..3585800 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -94,6 +94,7 @@ function _getOpenApi(csdl, options, serviceName = "") { } sOptions.url = url; + sOptions.protocol = protocol; const openapi = csdl2openapi.csdl2openapi(csdl, sOptions); diff --git a/test/lib/compile/openapi.test.js b/test/lib/compile/openapi.test.js index e21e0b2..8a0c7a6 100644 --- a/test/lib/compile/openapi.test.js +++ b/test/lib/compile/openapi.test.js @@ -305,6 +305,36 @@ service CatalogService { '@protocol must not be mutated by toOpenApi'); }); + test('REST service does not include /$batch path', () => { + const csn = cds.compile.to.csn(` + @path: '/rest/v1/myRestAPI' + @protocol: 'rest' + service MyRestAPI { + entity Items { key ID : UUID; } + }` + ); + const openapi = toOpenApi(csn); + assert.strictEqual(openapi.paths?.['/$batch'], undefined, + '/$batch must not be present in OpenAPI output for a REST service'); + }); + + test('REST service collection GET does not include OData query options', () => { + const csn = cds.compile.to.csn(` + @path: '/rest/v1/myRestAPI' + @protocol: 'rest' + service MyRestAPI { + entity Items { key ID : UUID; name : String; } + }` + ); + const openapi = toOpenApi(csn); + const getParams = openapi.paths?.['/Items']?.get?.parameters ?? []; + const odataQueryOptions = ['$filter', '$top', '$skip', '$search', '$count', '$orderby', '$select', '$expand']; + const found = getParams + .map(p => p.name ?? p.$ref) + .filter(n => odataQueryOptions.some(o => typeof n === 'string' && (n === o || n.endsWith(o)))); + assert.deepStrictEqual(found, [], `OData query options must not appear on REST collection GET: ${found}`); + }); + test('options: Multiple servers', () => { const csn = cds.compile.to.csn(` service A {entity E { key ID : UUID; };};`