Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 17 additions & 12 deletions lib/compile/csdl2openapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 (
Expand All @@ -131,6 +133,7 @@ module.exports.csdl2openapi = function (
url: serviceRoot,
servers: serversObject,
odataVersion,
protocol,
scheme = 'https',
host = 'localhost',
basePath = '/service-root',
Expand Down Expand Up @@ -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), {});
}

Expand Down Expand Up @@ -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;
}
Expand Down
1 change: 1 addition & 0 deletions lib/compile/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ function _getOpenApi(csdl, options, serviceName = "") {
}

sOptions.url = url;
sOptions.protocol = protocol;

const openapi = csdl2openapi.csdl2openapi(csdl, sOptions);

Expand Down
30 changes: 30 additions & 0 deletions test/lib/compile/openapi.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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; };};`
Expand Down
Loading