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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
### Removed
### Fixed
- Entities that are transitively autoexposed and should still be considered readonly, do not generate documentation for write endpoints anymore
- 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
### Security

## [1.6.0] - 2026-08-04
Expand Down
45 changes: 18 additions & 27 deletions lib/compile/csdl2openapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
const { CSDLMeta, nameParts, isIdentifier } = require('./csdl');
const { camelCaseToWords: splitName } = require('./string-util');
const { Diagram } = require('./diagram')
const propertyUtil = require('./property-util');
const pluralize = require('pluralize')
const DEBUG = cds.debug('openapi'); // Initialize cds.debug with the 'openapi'

Expand Down Expand Up @@ -90,7 +91,7 @@
* @param {{ url?: string, servers?: object, odataVersion?: string, scheme?: string, host?: string, basePath?: string, diagram?: boolean, maxLevels?: number, shortActionPaths?: boolean }} options Optional parameters
* @return {*} OpenAPI description
*/
module.exports.csdl2openapi = function (

Check warning on line 94 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / lint

Function has a complexity of 22. Maximum allowed is 15

Check warning on line 94 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 24

Function has a complexity of 22. Maximum allowed is 15

Check warning on line 94 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 20

Function has a complexity of 22. Maximum allowed is 15
csdl,
{
url: serviceRoot,
Expand All @@ -109,6 +110,7 @@
csdl = structuredClone(csdl)
csdl.$Version = odataVersion ? odataVersion : '4.01'
const meta = new CSDLMeta(csdl)
const { isNavBackReference, isNavWritable, isScalarReadOnly, isScalarWritable, isMandatoryField, isDeltaSupported } = propertyUtil(meta);
serviceRoot = serviceRoot ?? (`${scheme}://${host}${basePath}`);
const queryOptionPrefix = csdl.$Version <= '4.01' ? '$' : '';
const typesToInline = {}; // filled in schema() and used in inlineTypes()
Expand Down Expand Up @@ -471,7 +473,7 @@
* @param {number} options.level Number of navigation segments so far
* @param {string} options.navigationPath Path for finding navigation restrictions
*/
function pathItems({ paths, prefix, prefixParameters, element, root, sourceName, targetName, target, level, navigationPath }) {

Check warning on line 476 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / lint

Function 'pathItems' has a complexity of 19. Maximum allowed is 15

Check warning on line 476 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 24

Function 'pathItems' has a complexity of 19. Maximum allowed is 15

Check warning on line 476 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 20

Function 'pathItems' has a complexity of 19. Maximum allowed is 15
const name = prefix.substring(prefix.lastIndexOf('/') + 1);
const type = meta.modelElement(element.$Type);
const pathItem = {};
Expand Down Expand Up @@ -749,7 +751,7 @@
* @param {boolean} options.byKey Read by key
* @param {array} options.nonExpandable Non-expandable navigation properties
*/
function operationRead({ pathItem, element, name, sourceName, targetName, target, level, restrictions, byKey, nonExpandable }) {

Check warning on line 754 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / lint

Function 'operationRead' has a complexity of 30. Maximum allowed is 15

Check warning on line 754 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 24

Function 'operationRead' has a complexity of 30. Maximum allowed is 15

Check warning on line 754 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 20

Function 'operationRead' has a complexity of 30. Maximum allowed is 15
const targetRestrictions = target?.[meta.voc.Capabilities.ReadRestrictions];
const readRestrictions = restrictions.ReadRestrictions || targetRestrictions || {};
const readByKeyRestrictions = readRestrictions.ReadByKeyRestrictions;
Expand Down Expand Up @@ -784,7 +786,7 @@
isCount: !countRestrictions
})
};
const deltaSupported = element[meta.voc.Capabilities.ChangeTracking] && element[meta.voc.Capabilities.ChangeTracking].Supported;
const deltaSupported = isDeltaSupported(element);
if (!byKey && deltaSupported) {
// @ts-expect-error - set above
operation.responses[200].content['application/json'].schema.properties['@odata.deltaLink'] = {
Expand Down Expand Up @@ -1219,7 +1221,7 @@
* @param {*} options.restrictions Navigation property restrictions of navigation segment
* @param {boolean} [options.byKey=false] Update by key
*/
function operationUpdate({ pathItem, element, name, sourceName, target, level, restrictions, byKey = false }) {

Check warning on line 1224 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / lint

Function 'operationUpdate' has a complexity of 16. Maximum allowed is 15

Check warning on line 1224 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 24

Function 'operationUpdate' has a complexity of 16. Maximum allowed is 15

Check warning on line 1224 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 20

Function 'operationUpdate' has a complexity of 16. Maximum allowed is 15
const updateRestrictions = restrictions.UpdateRestrictions || target?.[meta.voc.Capabilities.UpdateRestrictions] || {};
const countRestrictions = target?.[meta.voc.Capabilities.CountRestrictions]?.Countable === false;
if (updateRestrictions.Updatable !== false && !element[meta.voc.Core.Immutable]) {
Expand Down Expand Up @@ -1310,7 +1312,7 @@
*/
function pathItemsForMediaStream({ paths, prefix, prefixParameters, type, name, sourceName }) {
if (type.$HasStream) {
const mediaTypes = type[meta.voc.Core.AcceptableMediaTypes]?.map(t => t['$EnumMember'] ?? t) ?? [];

Check warning on line 1315 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / lint

["$EnumMember"] is better written in dot notation

Check warning on line 1315 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 24

["$EnumMember"] is better written in dot notation

Check warning on line 1315 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 20

["$EnumMember"] is better written in dot notation
const contentTypes = mediaTypes.length > 0 ? mediaTypes : ['*/*'];
const mediaContent = Object.fromEntries(contentTypes.map(ct => [ct, { schema: { type: 'string', format: 'binary' } }]));
const lname = splitName(name);
Expand Down Expand Up @@ -1640,7 +1642,7 @@
* @param {string} options.sourceName Name of path source
* @param {*} [options.actionImport={}] Action import
*/
function pathItemAction({ paths, prefix, prefixParameters, actionName, overload, sourceName, actionImport = {} }) {

Check warning on line 1645 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / lint

Function 'pathItemAction' has a complexity of 18. Maximum allowed is 15

Check warning on line 1645 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 24

Function 'pathItemAction' has a complexity of 18. Maximum allowed is 15

Check warning on line 1645 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 20

Function 'pathItemAction' has a complexity of 18. Maximum allowed is 15
const name = actionName.indexOf('.') === -1 ? actionName : nameParts(actionName).name;
const pathItem = {
post: {
Expand Down Expand Up @@ -1730,7 +1732,7 @@
* @param {string} options.sourceName Name of path source
* @param {*} [options.functionImport={}] Function Import
*/
function pathItemFunction({ paths, prefix, prefixParameters, functionName, overload, sourceName, functionImport = {} }) {

Check warning on line 1735 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / lint

Function 'pathItemFunction' has a complexity of 16. Maximum allowed is 15

Check warning on line 1735 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 24

Function 'pathItemFunction' has a complexity of 16. Maximum allowed is 15

Check warning on line 1735 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 20

Function 'pathItemFunction' has a complexity of 16. Maximum allowed is 15
const name = functionName.indexOf('.') === -1 ? functionName : nameParts(functionName).name;
let parameters = overload.$Parameter || [];
if (overload.$IsBound) parameters = parameters.slice(1);
Expand All @@ -1739,7 +1741,7 @@

const implicitAliases = csdl.$Version > '4.0' || parameters.some(p => p[meta.voc.Core.OptionalParameter]);

parameters.forEach(p => {

Check warning on line 1744 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / lint

Arrow function has a complexity of 29. Maximum allowed is 15

Check warning on line 1744 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 24

Arrow function has a complexity of 29. Maximum allowed is 15

Check warning on line 1744 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 20

Arrow function has a complexity of 29. Maximum allowed is 15
const description = getDescriptionWithFallback(p);
/** @type {Parameter} */
const param = {
Expand Down Expand Up @@ -2119,7 +2121,7 @@
* @param {string} options.suffix Suffix for read/create/update
* @return {*} Map of Schemas Objects
*/
function schemasForStructuredType({ schemas, qualifier, name, type, suffix }) {

Check warning on line 2124 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / lint

Function 'schemasForStructuredType' has a complexity of 18. Maximum allowed is 15

Check warning on line 2124 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 24

Function 'schemasForStructuredType' has a complexity of 18. Maximum allowed is 15

Check warning on line 2124 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 20

Function 'schemasForStructuredType' has a complexity of 18. Maximum allowed is 15
const schemaName = `${qualifier}.${name}${suffix}`;
const baseName = `${qualifier}.${name}`;
const isKey = keyMap(type);
Expand All @@ -2135,36 +2137,25 @@
const properties = propertiesOfStructuredType(type);
const expandRestrictions = type[meta.voc.Capabilities.ExpandRestrictions] ?? {};
const nonExpandableProperties = expandRestrictions.NonExpandableProperties ?? [];
Object.keys(properties).forEach(iName => {

Check warning on line 2140 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / lint

Arrow function has a complexity of 23. Maximum allowed is 15

Check warning on line 2140 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 24

Arrow function has a complexity of 23. Maximum allowed is 15

Check warning on line 2140 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 20

Arrow function has a complexity of 23. Maximum allowed is 15
if (nonExpandableProperties.includes(iName)) {
return;
}
if (nonExpandableProperties.includes(iName)) return;

const property = properties[iName];
if (suffix === SUFFIX.read) schemaProperties[iName] = getSchema(property);
if ((Object.prototype.hasOwnProperty.call(property, '@Common.FieldControl')) && property['@Common.FieldControl'] === 'Mandatory') { required.push(iName) }

if (isMandatoryField(property)) required.push(iName);

if (property.$Kind == 'NavigationProperty') {
if (property.$Collection && suffix === "" && isCount === true) {
schemaProperties[`${iName}@${csdl.$Version === '4.0' ? 'odata.' : ''}count`] = ref('count');
}
if (property[meta.voc.Core.Permissions] != "Read" && !property[meta.voc.Core.Computed] && (property.$ContainsTarget || property.$OnDelete === 'Cascade')) {
if (suffix === SUFFIX.create) {
schemaProperties[iName] = getSchema(property, SUFFIX.create);
}
if (suffix === SUFFIX.update && !property[meta.voc.Core.Immutable]) {
schemaProperties[iName] = getSchema(property, SUFFIX.create);
}
}
if (isNavBackReference(property)) return;

if (suffix === SUFFIX.read) schemaProperties[iName] = getSchema(property);
if (property.$Collection && suffix === SUFFIX.read && isCount) schemaProperties[`${iName}@${csdl.$Version === '4.0' ? 'odata.' : ''}count`] = ref('count');
if (isNavWritable(property) && suffix === SUFFIX.create) schemaProperties[iName] = getSchema(property, SUFFIX.create);
if (isNavWritable(property) && suffix === SUFFIX.update && !property[meta.voc.Core.Immutable]) schemaProperties[iName] = getSchema(property, SUFFIX.create);
} else {
if (property[meta.voc.Core.Permissions] === "Read" || property[meta.voc.Core.Computed] || property[meta.voc.Core.ComputedDefaultValue]) {
const index = required.indexOf(iName);
if (index != -1) required.splice(index, 1);
}
if (!(property[meta.voc.Core.Permissions] === "Read" || property[meta.voc.Core.Computed])) {
if (suffix === SUFFIX.create)
schemaProperties[iName] = getSchema(property, SUFFIX.create);
if (suffix === SUFFIX.update && !isKey[iName] && !property[meta.voc.Core.Immutable])
schemaProperties[iName] = getSchema(property, SUFFIX.update);
}
if (suffix === SUFFIX.read) schemaProperties[iName] = getSchema(property);
if (isScalarReadOnly(property)) required.splice(required.indexOf(iName), 1);
if (isScalarWritable(property) && suffix === SUFFIX.create) schemaProperties[iName] = getSchema(property, SUFFIX.create);
if (isScalarWritable(property) && suffix === SUFFIX.update && !isKey[iName] && !property[meta.voc.Core.Immutable]) schemaProperties[iName] = getSchema(property, SUFFIX.update);
}
});

Expand Down
51 changes: 51 additions & 0 deletions lib/compile/property-util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* @typedef {object} CSDLProperty
* @property {string} [$Kind]
* @property {string} [$Type]
* @property {string} [$Partner]
* @property {boolean} [$ContainsTarget]
* @property {boolean} [$Collection]
* @property {string} [$OnDelete]
*/

/**
* @typedef {object} PropertyPredicates
* @property {(property: CSDLProperty) => boolean} isNavBackReference - True when the nav property is the back-reference side of a composition (child -> parent)
* @property {(property: CSDLProperty) => boolean} isNavWritable - True when the nav property may be written by the client (create/update)
* @property {(property: CSDLProperty) => boolean} isScalarReadOnly - True when the scalar property is read-only or computed (must be removed from required)
* @property {(property: CSDLProperty) => boolean} isScalarWritable - True when the scalar property may be written by the client (create/update)
* @property {(property: CSDLProperty) => boolean} isMandatoryField - True when the property is annotated as a mandatory field control
* @property {(element: CSDLProperty) => boolean} isDeltaSupported - True when the element has change tracking with delta support enabled
*/

/**
* Returns property predicate functions bound to the given CSDL metadata instance.
* @param {InstanceType<typeof import('./csdl').CSDLMeta>} meta
* @returns {PropertyPredicates}
*/
module.exports = (meta) => ({
isNavBackReference: (property) =>
!!property.$Partner
&& !!property.$Type
&& !!meta.modelElement(property.$Type)?.[property.$Partner]?.$ContainsTarget,

isNavWritable: (property) =>
property[meta.voc.Core.Permissions] !== "Read"
&& !property[meta.voc.Core.Computed]
&& (property.$ContainsTarget || property.$OnDelete === 'Cascade'),

isScalarReadOnly: (property) =>
property[meta.voc.Core.Permissions] === "Read"
|| property[meta.voc.Core.Computed]
|| property[meta.voc.Core.ComputedDefaultValue],

isScalarWritable: (property) =>
property[meta.voc.Core.Permissions] !== "Read"
&& !property[meta.voc.Core.Computed],

isMandatoryField: (property) =>
property['@Common.FieldControl'] === 'Mandatory',

isDeltaSupported: (element) =>
!!element[meta.voc.Capabilities.ChangeTracking]?.Supported,
});
8 changes: 8 additions & 0 deletions test/lib/compile/csdl2openapi.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ const result12 = require("./data/autoexposed-texts.openapi3.json");
const example13 = require("./data/autoexposed-direct.json");
const result13 = require("./data/autoexposed-direct.openapi3.json");

const exampleBackRef = require("./data/back-reference.json");
const resultBackRef = require("./data/back-reference.openapi3.json");

describe("Examples", () => {
test("csdl-16.1", () => {
const openapi = lib.csdl2openapi(example1, { diagram: true });
Expand Down Expand Up @@ -111,6 +114,11 @@ describe("Examples", () => {
const openapi = lib.csdl2openapi(example13, { url: "https://localhost/service-root" });
check(openapi, result13);
});

test("back-reference", () => {
const openapi = lib.csdl2openapi(exampleBackRef, { diagram: false });
check(openapi, resultBackRef);
});
});

describe("Edge cases", () => {
Expand Down
60 changes: 60 additions & 0 deletions test/lib/compile/data/back-reference.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"$Version": "4.01",
"$Reference": {
"https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Core.V1.json": {
"$Include": [
{
"$Namespace": "Org.OData.Core.V1",
"$Alias": "Core",
"@Core.DefaultNamespace": true
}
]
},
"https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Capabilities.V1.json": {
"$Include": [
{
"$Namespace": "Org.OData.Capabilities.V1",
"$Alias": "Capabilities"
}
]
}
},
"$EntityContainer": "UsageService.Container",
"UsageService": {
"$Alias": "self",
"UsageRecords": {
"$Kind": "EntityType",
"$Key": ["ID"],
"ID": { "$Type": "Edm.Guid" },
"description": { "$Nullable": true },
"customReferences": {
"$Kind": "NavigationProperty",
"$Collection": true,
"$Type": "self.UsageRecordCustomReferences",
"$ContainsTarget": true,
"$Partner": "usageRecord"
}
},
"UsageRecordCustomReferences": {
"$Kind": "EntityType",
"$Key": ["ID"],
"ID": { "$Type": "Edm.Guid" },
"type": { "$Nullable": true },
"value": { "$Nullable": true },
"usageRecord": {
"$Kind": "NavigationProperty",
"$Type": "self.UsageRecords",
"$Nullable": false,
"$Partner": "customReferences"
}
},
"Container": {
"$Kind": "EntityContainer",
"@Core.Description": "Usage Record Service",
"UsageRecords": {
"$Collection": true,
"$Type": "self.UsageRecords"
}
}
}
}
Loading
Loading