Skip to content
Merged
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
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,19 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).

### Added
### Changed
- Removed special characters from placeholders for fields like short text, description, etc.
### Deprecated
### Removed
### Fixed
### Security

## [1.4.0] - tbd

### Added
- Opt-in option `shortActionPaths` to use simplified bound action paths (e.g., `.../Discount` instead of `.../ODataDemo.Discount`). Enable via `{ "cds": { "openapi": { "shortActionPaths": true } } }` in `package.json`.

### Changed
- Removed special characters from placeholders for fields like short text, description, etc.

### Fixed
- Autoexposed `.texts` entities are now excluded from OpenAPI document
- Generate navigation paths to CRUD-disabled entities
Expand Down
21 changes: 12 additions & 9 deletions lib/compile/csdl2openapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,27 +87,29 @@
/**
* 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 }} options Optional parameters
* @param {{ url?: string, servers?: object, odataVersion?: string, scheme?: string, host?: string, basePath?: string, diagram?: boolean, maxLevels?: number, shortActionPaths?: boolean }} options Optional parameters
* @return {object} OpenAPI description
*/
module.exports.csdl2openapi = function (

Check warning on line 93 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

Check warning on line 93 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 93 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / lint

Function has a complexity of 22. Maximum allowed is 15
csdl,
{
url: serviceRoot,
servers: serversObject,
odataVersion: odataVersion,
scheme: scheme = 'https',
host: host = 'localhost',
basePath: basePath = '/service-root',
diagram: diagram = false,
maxLevels: maxLevels = 5
odataVersion,
scheme = 'https',
host = 'localhost',
basePath = '/service-root',
diagram = false,
maxLevels = 5,
shortActionPaths = false
} = {}
) {
diagram = /** @type {unknown} */(diagram) !== "false" && !!diagram;

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

View workflow job for this annotation

GitHub Actions / Node.js 20

Assignment to function parameter 'diagram'

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

View workflow job for this annotation

GitHub Actions / Node.js 24

Assignment to function parameter 'diagram'

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

View workflow job for this annotation

GitHub Actions / lint

Assignment to function parameter 'diagram'
// as preProcess below mutates the csdl, copy it before, to avoid side-effects on the caller side
csdl = JSON.parse(JSON.stringify(csdl))

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

View workflow job for this annotation

GitHub Actions / Node.js 20

Assignment to function parameter 'csdl'

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

View workflow job for this annotation

GitHub Actions / Node.js 24

Assignment to function parameter 'csdl'

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

View workflow job for this annotation

GitHub Actions / lint

Assignment to function parameter 'csdl'
csdl.$Version = odataVersion ? odataVersion : '4.01'
const meta = new CSDLMeta(csdl)
serviceRoot = serviceRoot ?? (`${scheme}://${host}${basePath}`);

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

View workflow job for this annotation

GitHub Actions / Node.js 20

Assignment to function parameter 'serviceRoot'

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

View workflow job for this annotation

GitHub Actions / Node.js 24

Assignment to function parameter 'serviceRoot'

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

View workflow job for this annotation

GitHub Actions / lint

Assignment to function parameter 'serviceRoot'
const queryOptionPrefix = csdl.$Version <= '4.01' ? '$' : '';
const typesToInline = {}; // filled in schema() and used in inlineTypes()

Expand All @@ -120,7 +122,7 @@
Object.keys(entityContainer).forEach(element => {
if (entityContainer[element].$Type) {
const fullTypeName = entityContainer[element].$Type;
const type = fullTypeName.startsWith(serviceName + '.')

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

View workflow job for this annotation

GitHub Actions / Node.js 20

Unexpected string concatenation

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

View workflow job for this annotation

GitHub Actions / Node.js 24

Unexpected string concatenation

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

View workflow job for this annotation

GitHub Actions / lint

Unexpected string concatenation
? fullTypeName.substring(serviceName.length + 1)
: nameParts(fullTypeName).name;
if ((csdl[serviceName]?.[type]?.['@cds.autoexpose'] || csdl[serviceName]?.[type]?.['@cds.autoexposed'])
Expand Down Expand Up @@ -406,7 +408,7 @@
.replaceAll('_', ' ')
.replace(/([a-z])([A-Z])/g, '$1 $2'); // "camelCase" to "camel Case"
if (typeof tag === 'string') {
tag = normalise(tag);

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

View workflow job for this annotation

GitHub Actions / Node.js 20

Assignment to function parameter 'tag'

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

View workflow job for this annotation

GitHub Actions / Node.js 24

Assignment to function parameter 'tag'

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

View workflow job for this annotation

GitHub Actions / lint

Assignment to function parameter 'tag'
} else {
tag.name = normalise(tag.name);
}
Expand Down Expand Up @@ -458,7 +460,7 @@
* @param {number} level Number of navigation segments so far
* @param {string} navigationPath Path for finding navigation restrictions
*/
function pathItems(paths, prefix, prefixParameters, element, root, sourceName, targetName, target, level, navigationPath) {

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

View workflow job for this annotation

GitHub Actions / Node.js 20

Function 'pathItems' has too many parameters (10). Maximum allowed is 4

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

View workflow job for this annotation

GitHub Actions / Node.js 24

Function 'pathItems' has too many parameters (10). Maximum allowed is 4

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

View workflow job for this annotation

GitHub Actions / lint

Function 'pathItems' has too many parameters (10). Maximum allowed is 4
const name = prefix.substring(prefix.lastIndexOf('/') + 1);
const type = meta.modelElement(element.$Type);
const pathItem = {};
Expand Down Expand Up @@ -540,7 +542,7 @@
* @param {object} restrictions Navigation property restrictions of navigation segment
* @param {array} nonExpandable Non-expandable navigation properties
*/
function pathItemsWithKey(paths, prefix, prefixParameters, element, root, sourceName, targetName, target, level, navigationPath, restrictions, nonExpandable) {

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

View workflow job for this annotation

GitHub Actions / Node.js 20

Function 'pathItemsWithKey' has too many parameters (12). Maximum allowed is 4

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

View workflow job for this annotation

GitHub Actions / Node.js 24

Function 'pathItemsWithKey' has too many parameters (12). Maximum allowed is 4

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

View workflow job for this annotation

GitHub Actions / lint

Function 'pathItemsWithKey' has too many parameters (12). Maximum allowed is 4
const targetIndexable = target == null || target[meta.voc.Capabilities.IndexableByKey] != false;
if (restrictions.IndexableByKey == true || restrictions.IndexableByKey != false && targetIndexable) {
const name = prefix.substring(prefix.lastIndexOf('/') + 1);
Expand Down Expand Up @@ -578,7 +580,7 @@
* @param {number} level Number of navigation segments so far
* @param {object} restrictions Navigation property restrictions of navigation segment
*/
function operationCreate(pathItem, element, name, sourceName, targetName, target, level, restrictions) {

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

View workflow job for this annotation

GitHub Actions / Node.js 20

Function 'operationCreate' has too many parameters (8). Maximum allowed is 4

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

View workflow job for this annotation

GitHub Actions / Node.js 24

Function 'operationCreate' has too many parameters (8). Maximum allowed is 4

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

View workflow job for this annotation

GitHub Actions / lint

Function 'operationCreate' has too many parameters (8). Maximum allowed is 4
const insertRestrictions = restrictions.InsertRestrictions || target?.[meta.voc.Capabilities.InsertRestrictions] || {};
const countRestrictions = target?.[meta.voc.Capabilities.CountRestrictions]?.Countable === false // count property will be added if CountRestrictions is false
if (insertRestrictions.Insertable !== false) {
Expand Down Expand Up @@ -614,7 +616,7 @@
* @param {boolean} byKey Access by key
* @return Operation Text
*/
function operationSummary(operation, name, sourceName, level, collection, byKey) {

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

View workflow job for this annotation

GitHub Actions / Node.js 20

Function 'operationSummary' has too many parameters (6). Maximum allowed is 4

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

View workflow job for this annotation

GitHub Actions / Node.js 24

Function 'operationSummary' has too many parameters (6). Maximum allowed is 4

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

View workflow job for this annotation

GitHub Actions / lint

Function 'operationSummary' has too many parameters (6). Maximum allowed is 4
const lname = splitName(name);
const sname = splitName(sourceName);

Expand Down Expand Up @@ -1343,10 +1345,11 @@
}
const overloads = meta.boundOverloads[element.$Type + (!byKey && element.$Collection ? '-c' : '')] || [];
overloads.forEach(item => {
const pathName = shortActionPaths && item.name.includes('.') ? nameParts(item.name).name : item.name;
if (item.overload.$Kind == 'Action')
pathItemAction(paths, `${prefix}/${item.name}`, prefixParameters, item.name, item.overload, sourceName);
pathItemAction(paths, `${prefix}/${pathName}`, prefixParameters, item.name, item.overload, sourceName);
else
pathItemFunction(paths, `${prefix}/${item.name}`, prefixParameters, item.name, item.overload, sourceName);
pathItemFunction(paths, `${prefix}/${pathName}`, prefixParameters, item.name, item.overload, sourceName);
});
}

Expand Down
57 changes: 31 additions & 26 deletions lib/compile/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const csdl2openapi = require('./csdl2openapi')
const cds = require('@sap/cds');
const fs = require("fs");
const fs = require('fs');
const DEBUG = cds.debug('openapi');
const supportedProtocols = ["rest", "odata", "odata-v4"];

Expand Down Expand Up @@ -108,37 +108,19 @@ function _getOpenApi(csdl, options, serviceName = "") {
}

function toOpenApiOptions(csdl, csn, options = {}) {
const result = {};
const callerOptions = {};
for (const key in options) {
if (/^openapi:(.*)/.test(key) && RegExp.$1) {
result[RegExp.$1] = options[key];
callerOptions[RegExp.$1] = options[key];
} else if (key === "odata-version") {
result.odataVersion = options[key];
callerOptions.odataVersion = options[key];
}
}

if (result["config-file"]) {
if (!fs.existsSync(result["config-file"])) {
throw new Error(`Unable to find openapi config file ${result["config-file"]}`);
}

try{
const fileData = fs.readFileSync(result["config-file"], "utf-8");
const fileContent = JSON.parse(fileData);

Object.keys(fileContent).forEach((key) => {
if (key === "odata-version" && !result.odataVersion) {
result.odataVersion = fileContent[key];
} else if (key === "diagram") {
result.diagram = !result.diagram && fileContent[key] === "true";
} else if (!(key in result)) { // inline options take precedence
result[key] = JSON.stringify(fileContent[key]);
}
});
}catch(err){
throw new Error(`Unable to parse OpenAPI config ${result["config-file"]}`, { cause: err });
}
}
const envOptions = cds.env.openapi instanceof Object && !Array.isArray(cds.env.openapi) ? cds.env.openapi : {};
const fileOptions = _readConfigFile(callerOptions["config-file"]);
const result = { ...envOptions, ...fileOptions, ...callerOptions };
delete result["config-file"];

const protocols = _getProtocols(csdl, csn, result.odataVersion);

Expand Down Expand Up @@ -226,6 +208,29 @@ function _servicePath(csdl, csn, protocols) {
return {}
}

function _readConfigFile(configFilePath) {
if (!configFilePath) return {};

if (!fs.existsSync(configFilePath)) {
throw new Error(`Unable to find openapi config file ${configFilePath}`);
}

let fileContent;
try {
fileContent = JSON.parse(fs.readFileSync(configFilePath, 'utf-8'));
} catch (err) {
throw new Error(`Unable to parse OpenAPI config ${configFilePath}`, { cause: err });
}

const result = {};
for (const key of Object.keys(fileContent)) {
const normalizedKey = key === "odata-version" ? "odataVersion" : key;
const value = fileContent[key];
result[normalizedKey] = typeof value === 'object' && value !== null ? JSON.stringify(value) : value;
}
return result;
}

// we're attaching the events to the main function so they become automatically exposed through the cds facade:
// cds.compile.to.openapi(...)
// cds.compile.to.openapi.events.before
Expand Down
5 changes: 0 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,5 @@
"eslint": "^9.33.0",
"typescript": "^5.9.2",
"@mermaid-js/mermaid-cli": "^11.12.0"
},
"cds": {
"openapi": {
"foo": "bar"
}
}
}
101 changes: 101 additions & 0 deletions test/lib/compile/csdl2openapi.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2703,6 +2703,107 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot
});
});

describe("Bound action path naming", () => {
const baseCsdl = {
$Version: "4.01",
$Reference: {
dummy: {
$Include: [
{ $Namespace: "Org.OData.Capabilities.V1", $Alias: "Capabilities" },
],
},
},
$EntityContainer: "TestService.Container",
TestService: {
Product: {
$Kind: "EntityType",
$Key: ["ID"],
ID: { $Type: "Edm.Int32" },
Name: { $Type: "Edm.String" },
},
Discount: [
{
$Kind: "Action",
$IsBound: true,
$Parameter: [
{ $Name: "in", $Type: "TestService.Product" },
{ $Name: "percent", $Type: "Edm.Decimal" },
],
$ReturnType: { $Type: "Edm.Decimal" },
},
],
Container: {
"@Capabilities.KeyAsSegmentSupported": true,
Products: {
$Type: "TestService.Product",
$Collection: true,
},
},
},
};

test("Fully qualified action names by default", () => {
const csdl = JSON.parse(JSON.stringify(baseCsdl));
const actual = lib.csdl2openapi(csdl);

assert.ok(
actual.paths["/Products/{ID}/TestService.Discount"],
"Path should use fully qualified name with namespace prefix"
);
assert.strictEqual(
actual.paths["/Products/{ID}/Discount"],
undefined,
"Path with simplified name should not exist"
);
assert.strictEqual(
actual.paths["/Products/{ID}/TestService.Discount"].post.summary,
"Invokes action Discount",
"Action summary should be present"
);
});

test("Simplified action names with shortActionPaths option", () => {
const csdl = JSON.parse(JSON.stringify(baseCsdl));
const actual = lib.csdl2openapi(csdl, { shortActionPaths: true });

assert.ok(
actual.paths["/Products/{ID}/Discount"],
"Path should use simplified name without namespace prefix"
);
assert.strictEqual(
actual.paths["/Products/{ID}/TestService.Discount"],
undefined,
"Path with namespace prefix should not exist"
);
assert.strictEqual(
actual.paths["/Products/{ID}/Discount"].post.summary,
"Invokes action Discount",
"Action summary should be present"
);
});

test("Simplified names with DefaultNamespace even without shortActionPaths", () => {
const csdl = JSON.parse(JSON.stringify(baseCsdl));
csdl.$Reference.dummy.$Include.push({
$Namespace: "Org.OData.Core.V1",
$Alias: "Core",
});
csdl.TestService["@Core.DefaultNamespace"] = true;

const actual = lib.csdl2openapi(csdl);

assert.ok(
actual.paths["/Products/{ID}/Discount"],
"Path should use simplified name when DefaultNamespace is true"
);
assert.strictEqual(
actual.paths["/Products/{ID}/TestService.Discount"],
undefined,
"Path with namespace prefix should not exist with DefaultNamespace"
);
});
});

it("AllowedValues on various Edm types", () => {
const csdl = {
$Reference: {
Expand Down
Loading