Skip to content

Commit

Permalink
[CDSADAPTERS-2186] Fixed openapi filename issue when there's only one…
Browse files Browse the repository at this point in the history
… service in the CDL source (#60)

* Fixed openapi filename issue when there's only one service in the CDL source file

* Update test cases to handle generator function in openAPI

* Add changelog entry

* Add version in changelog

* Fixed openapi filename issue when there's only one service in the CDL source

* Fix and add comment
  • Loading branch information
Dipto-at-sap authored Jan 13, 2025
1 parent e4a25cd commit 6b12cc3
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 130 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
The format is based on [Keep a Changelog](http://keepachangelog.com/).

## Version 1.1.2 - tbd

### Fixed

Expand Down
25 changes: 10 additions & 15 deletions lib/compile/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,18 @@ module.exports = function processor(csn, options = {}) {
let openApiDocs = {};

if (csdl[Symbol.iterator]) { // generator function means multiple services
openApiDocs = _getOpenApiForMultipleServices(csdl, csn, options)
openApiDocs = _getOpenApiForMultipleServices(csdl, csn, options);
return _iterate(openApiDocs);
} else {
const openApiOptions = toOpenApiOptions(csdl, csn, options)
openApiDocs = _getOpenApi(csdl, openApiOptions);
const openApiOptions = toOpenApiOptions(csdl, csn, options);
const serviceName = csdl.$EntityContainer.replace(/\.[^.]+$/, "");
openApiDocs = _getOpenApi(csdl, openApiOptions,serviceName);
if(Object.keys(openApiDocs).length===1){
return openApiDocs[serviceName];
}else{
return _iterate(openApiDocs);
}
}

return _iterate(openApiDocs);
}

function _getOpenApiForMultipleServices(csdl, csn, options) {
Expand All @@ -46,12 +51,6 @@ function* _iterate(openApiDocs) {
}
}

function nameParts(qualifiedName) {
const pos = qualifiedName.lastIndexOf('.');
console.assert(pos > 0, 'Invalid qualified name ' + qualifiedName);
return qualifiedName.substring(0, pos);
}

function _getOpenApi(csdl, options, serviceName = "") {
const openApiDocs = {};
let filename;
Expand All @@ -70,10 +69,6 @@ function _getOpenApi(csdl, options, serviceName = "") {

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

if(!serviceName) {
serviceName= nameParts(csdl.$EntityContainer);
}

if (protocols.length > 1) {
filename = serviceName + "." + protocol;
} else {
Expand Down
172 changes: 57 additions & 115 deletions test/lib/compile/openapi.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,10 @@ const SCENARIO = Object.freeze({

function checkAnnotations(csn, annotations, scenario = SCENARIO.positive, property = '') {
const openApi = toOpenApi(csn);
if (openApi.next) {
const { value } = openApi.next();
const schemas = Object.entries(value[0].components.schemas).filter(([key]) => key.startsWith('sap.odm.test.A.E1'))
const schemas = Object.entries(openApi.components.schemas).filter(([key]) => key.startsWith('sap.odm.test.A.E1'))
// Test if the openAPI document was generated with some schemas.
expect(value[0].components.schemas).toBeDefined()
expect(value[0]).toBeDefined()
expect(openApi.components.schemas).toBeDefined()
expect(openApi).toBeDefined()
expect(schemas.length > 0).toBeTruthy()

// Expect that not-allowed ODM annotations are unavailable in the schema.
Expand Down Expand Up @@ -59,7 +57,7 @@ function checkAnnotations(csn, annotations, scenario = SCENARIO.positive, proper
// Test that no other places contain the ODM extensions in the OpenAPI document.

// components.schemas where the schemas are not from entity E1.
const notE1 = Object.entries(value[0].components.schemas).filter(([key]) => !key.startsWith('sap.odm.test.A.E1'))
const notE1 = Object.entries(openApi.components.schemas).filter(([key]) => !key.startsWith('sap.odm.test.A.E1'))
for (const [, schema] of notE1) {
const schemaString = JSON.stringify(schema)
for (const [annKey] of annotations) {
Expand All @@ -68,11 +66,11 @@ function checkAnnotations(csn, annotations, scenario = SCENARIO.positive, proper
}

// all other components of the OpenAPI document except the schemas.
const openApiNoSchemas = JSON.stringify({ ...value[0], components: { parameters: { ...value[0].components.parameters }, responses: { ...value[0].components.responses } } })
const openApiNoSchemas = JSON.stringify({ ...openApi, components: { parameters: { ...openApi.components.parameters }, responses: { ...openApi.components.responses } } })
for (const [annKey] of annotations) {
expect(openApiNoSchemas).not.toContain(annKey)
}
}

}

describe('OpenAPI export', () => {
Expand All @@ -90,12 +88,9 @@ describe('OpenAPI export', () => {
service A {entity E { key ID : UUID; };};`
);
const openapi = toOpenApi(csn);
if (openapi.next) {
const { value } = openapi.next();
expect(value[0]).toMatchObject(someOpenApi);
// UUID elements are not required
expect(value[0].components.schemas['A.E-create']).not.toHaveProperty('required');
}
expect(openapi).toMatchObject(someOpenApi);
// UUID elements are not required
expect(openapi.components.schemas['A.E-create']).not.toHaveProperty('required');
});

test('one service, namespace', () => {
Expand All @@ -104,10 +99,7 @@ describe('OpenAPI export', () => {
service A {entity E { key ID : UUID; };};`
);
const openapi = toOpenApi(csn);
if (openapi.next) {
const { value } = openapi.next();
expect(value[0]).toMatchObject(someOpenApi);
}
expect(openapi).toMatchObject(someOpenApi);
});

test('one service, multiple protocols', () => {
Expand Down Expand Up @@ -154,11 +146,8 @@ service CatalogService {
`);

const openAPI = toOpenApi(csn);
if (openAPI.next) {
const { value } = openAPI.next();
expect(value[0]).toBeDefined();
expect(value[0].tags.length).toBe(1);
}
expect(openAPI).toBeDefined();
expect(openAPI.tags.length).toBe(1);
});

test('multiple services', () => {
Expand All @@ -169,16 +158,10 @@ service CatalogService {
expect(() => toOpenApi(csn, { service: 'foo' })).toThrowError(/no service/si)

let openapi = toOpenApi(csn, { service: 'A' });
if (openapi.next) {
const { value } = openapi.next();
expect(value[0]).toMatchObject(someOpenApi);
}
expect(openapi).toMatchObject(someOpenApi);

openapi = toOpenApi(csn, { service: 'B' });
if (openapi.next) {
const { value } = openapi.next();
expect(value[0]).toMatchObject(someOpenApi);
}
expect(openapi).toMatchObject(someOpenApi);

openapi = toOpenApi(csn, { service: 'all' });
const filesFound = new Set();
Expand All @@ -198,16 +181,11 @@ service CatalogService {
expect(() => toOpenApi(csn, { service: 'foo' })).toThrowError(/no service/si)

let openapi = toOpenApi(csn, { service: 'com.sap.A' });
if (openapi.next) {
const { value } = openapi.next();
expect(value[0]).toMatchObject(someOpenApi);
}
expect(openapi).toMatchObject(someOpenApi);

openapi = toOpenApi(csn, { service: 'com.sap.B' });
if (openapi.next) {
const { value } = openapi.next();
expect(value[0]).toMatchObject(someOpenApi);
}
expect(openapi).toMatchObject(someOpenApi);

openapi = toOpenApi(csn, { service: 'all' });
const filesFound = new Set();
for (const [content, metadata] of openapi) {
Expand Down Expand Up @@ -240,62 +218,44 @@ service CatalogService {
service B {entity F { key ID : UUID; };};`
);
let openapi = toOpenApi(csn, { service: 'A' });
if (openapi.next) {
const { value } = openapi.next();
expect(value[0]).toMatchObject({ servers: [{ url: '/a' }] });
expect(value[0].info.description).toBe("Use @Core.LongDescription: '...' on your CDS service to provide a meaningful description.")
}
expect(openapi).toMatchObject({ servers: [{ url: '/a' }] });
expect(openapi.info.description).toBe("Use @Core.LongDescription: '...' on your CDS service to provide a meaningful description.")

openapi = toOpenApi(csn, { service: 'A', 'openapi:url': 'http://foo.bar:8080' });
if (openapi.next) {
const { value } = openapi.next();
expect(value[0]).toMatchObject({ servers: [{ url: 'http://foo.bar:8080' }] });
}
expect(openapi).toMatchObject({ servers: [{ url: 'http://foo.bar:8080' }] });


openapi = toOpenApi(csn, { service: 'A', 'openapi:url': 'http://foo.bar:8080//${service-path}/foo' });
if (openapi.next) {
const { value } = openapi.next();
expect(value[0]).toMatchObject({ servers: [{ url: 'http://foo.bar:8080/a/foo' }] });
}
expect(openapi).toMatchObject({ servers: [{ url: 'http://foo.bar:8080/a/foo' }] });

});

test('options: diagram', () => {
const csn = cds.compile.to.csn(`
service A {entity E { key ID : UUID; };};`
);
let openapi = toOpenApi(csn);
if (openapi.next) {
const { value } = openapi.next();
expect(value[0].info.description).not.toMatch(/yuml.*diagram/i);
}
expect(openapi.info.description).not.toMatch(/yuml.*diagram/i);

openapi = toOpenApi(csn, { 'openapi:diagram': true });
if (openapi.next) {
const { value } = openapi.next();
expect(value[0].info.description).toMatch(/yuml.*diagram/i);
}
expect(openapi.info.description).toMatch(/yuml.*diagram/i);
});

test('options: servers', () => {
const csn = cds.compile.to.csn(`
service A {entity E { key ID : UUID; };};`
);
const serverObj = "[{\n \"url\": \"https://{customerId}.saas-app.com:{port}/v2\",\n \"variables\": {\n \"customerId\": \"demo\",\n \"description\": \"Customer ID assigned by the service provider\"\n }\n}]"
const openapi = toOpenApi(csn, { 'openapi:servers': serverObj });
if (openapi.next) {
const { value } = openapi.next();
expect(value[0].servers).toBeTruthy();
}
const openapi = toOpenApi(csn, { 'openapi:servers': serverObj })
expect(openapi.servers).toBeTruthy();
});

test('options: odata-version check server URL', () => {
const csn = cds.compile.to.csn(`
service A {entity E { key ID : UUID; };};`
);
const openapi = toOpenApi(csn, { 'odata-version': '4.0' });
if (openapi.next) {
const { value } = openapi.next();
expect(value[0].servers[0].url).toMatch('odata');
}
expect(openapi.servers[0].url).toMatch('odata');
});

test('options: Multiple servers', () => {
Expand All @@ -304,11 +264,8 @@ service CatalogService {
);
const serverObj = "[{\n \"url\": \"https://{customer1Id}.saas-app.com:{port}/v2\",\n \"variables\": {\n \"customer1Id\": \"demo\",\n \"description\": \"Customer1 ID assigned by the service provider\"\n }\n}, {\n \"url\": \"https://{customer2Id}.saas-app.com:{port}/v2\",\n \"variables\": {\n \"customer2Id\": \"demo\",\n \"description\": \"Customer2 ID assigned by the service provider\"\n }\n}]"
const openapi = toOpenApi(csn, { 'openapi:servers': serverObj });
if (openapi.next) {
const { value } = openapi.next();
expect(value[0].servers).toBeTruthy();
expect(value[0].servers[0].url).toMatch('https://{customer1Id}.saas-app.com:{port}/v2')
}
expect(openapi.servers).toBeTruthy();
expect(openapi.servers[0].url).toMatch('https://{customer1Id}.saas-app.com:{port}/v2')
});


Expand All @@ -330,13 +287,10 @@ service CatalogService {
service A {entity E { key ID : UUID; };};`
);
const openapi = toOpenApi(csn, { 'openapi:config-file': path.resolve("./test/lib/compile/data/configFile.json") });
if (openapi.next) {
const { value } = openapi.next();
expect(value[0].servers).toBeTruthy();
expect(value[0]).toMatchObject({ servers: [{ url: 'http://foo.bar:8080' }, { url: "http://foo.bar:8080/a/foo" }] });
expect(value[0].info.description).toMatch(/yuml.*diagram/i);
expect(value[0]['x-odata-version']).toMatch('4.1');
}
expect(openapi.servers).toBeTruthy();
expect(openapi).toMatchObject({ servers: [{ url: 'http://foo.bar:8080' }, { url: "http://foo.bar:8080/a/foo" }] });
expect(openapi.info.description).toMatch(/yuml.*diagram/i);
expect(openapi['x-odata-version']).toMatch('4.1');
});

test('options: config-file - with inline options, inline options given precedence', () => {
Expand All @@ -350,13 +304,10 @@ service CatalogService {
'openapi:diagram': "false"
}
const openapi = toOpenApi(csn, options);
if (openapi.next) {
const { value } = openapi.next();
expect(value[0].info.title).toMatch(/http:\/\/example.com:8080/i)
expect(value[0].info.description).not.toMatch(/yuml.*diagram/i);
expect(value[0]['x-odata-version']).toMatch('4.0');
expect(value[0]).toMatchObject({ servers: [{ url: 'http://foo.bar:8080' }, { url: "http://foo.bar:8080/a/foo" }] });
}
expect(openapi.info.title).toMatch(/http:\/\/example.com:8080/i)
expect(openapi.info.description).not.toMatch(/yuml.*diagram/i);
expect(openapi['x-odata-version']).toMatch('4.0');
expect(openapi).toMatchObject({ servers: [{ url: 'http://foo.bar:8080' }, { url: "http://foo.bar:8080/a/foo" }] });
});

test('annotations: root entity property', () => {
Expand All @@ -370,13 +321,10 @@ service CatalogService {
`)

const openAPI = toOpenApi(csn)
if (openAPI.next) {
const { value } = openAPI.next();
expect(value[0]).toBeDefined()
expect(value[0].components.schemas["sap.odm.test.A.E1"]).toMatchObject({ "x-sap-root-entity": true })
expect(value[0].components.schemas["sap.odm.test.A.E1-create"]["x-sap-root-entity"]).toBeUndefined()
expect(value[0].components.schemas["sap.odm.test.A.E1-update"]["x-sap-root-entity"]).toBeUndefined()
}
expect(openAPI).toBeDefined()
expect(openAPI.components.schemas["sap.odm.test.A.E1"]).toMatchObject({ "x-sap-root-entity": true })
expect(openAPI.components.schemas["sap.odm.test.A.E1-create"]["x-sap-root-entity"]).toBeUndefined()
expect(openAPI.components.schemas["sap.odm.test.A.E1-update"]["x-sap-root-entity"]).toBeUndefined()
});

test('odm annotations: entity name and oid property', () => {
Expand Down Expand Up @@ -482,12 +430,9 @@ service CatalogService {
}
}`);
const openAPI = toOpenApi(csn);
if (openAPI.next) {
const { value } = openAPI.next();
expect(value[0].externalDocs).toBeDefined();
expect(value[0].externalDocs.description).toBe('API Guide');
expect(value[0].externalDocs.url).toBe('https://help.sap.com/docs/product/123.html');
}
expect(openAPI.externalDocs).toBeDefined();
expect(openAPI.externalDocs.description).toBe('API Guide');
expect(openAPI.externalDocs.url).toBe('https://help.sap.com/docs/product/123.html');
}
);

Expand Down Expand Up @@ -530,19 +475,16 @@ service CatalogService {
}`);
const openAPI = toOpenApi(csn);
if(openAPI.next){
const {value}=openAPI.next();
expect(value[0]).toBeDefined();
expect(value[0]['x-sap-compliance-level']).toBe('sap:base:v1');
expect(value[0]['x-sap-ext-overview'].name).toBe('Communication Scenario');
expect(value[0]['x-sap-ext-overview'].values.text).toBe('Planning Calendar API Integration');
expect(value[0]['x-sap-ext-overview'].values.format).toBe('plain');
expect(value[0].components.schemas["sap.OpenAPI.test.A.E1"]["x-sap-dpp-is-potentially-sensitive"]).toBe('true');
expect(value[0].paths["/F1"].get["x-sap-operation-intent"]).toBe('read-collection for function');
expect(value[0].paths["/A1"].post["x-sap-operation-intent"]).toBe('read-collection for action');
expect(value[0].paths["/F1"].get["x-sap-deprecated-operation"].deprecationDate).toBe('2022-12-31');
expect(value[0].paths["/F1"].get["x-sap-deprecated-operation"].successorOperationId).toBe('successorOperation');
expect(value[0].paths["/F1"].get["x-sap-deprecated-operation"].notValidKey).toBeUndefined();
}
expect(openAPI).toBeDefined();
expect(openAPI['x-sap-compliance-level']).toBe('sap:base:v1');
expect(openAPI['x-sap-ext-overview'].name).toBe('Communication Scenario');
expect(openAPI['x-sap-ext-overview'].values.text).toBe('Planning Calendar API Integration');
expect(openAPI['x-sap-ext-overview'].values.format).toBe('plain');
expect(openAPI.components.schemas["sap.OpenAPI.test.A.E1"]["x-sap-dpp-is-potentially-sensitive"]).toBe('true');
expect(openAPI.paths["/F1"].get["x-sap-operation-intent"]).toBe('read-collection for function');
expect(openAPI.paths["/A1"].post["x-sap-operation-intent"]).toBe('read-collection for action');
expect(openAPI.paths["/F1"].get["x-sap-deprecated-operation"].deprecationDate).toBe('2022-12-31');
expect(openAPI.paths["/F1"].get["x-sap-deprecated-operation"].successorOperationId).toBe('successorOperation');
expect(openAPI.paths["/F1"].get["x-sap-deprecated-operation"].notValidKey).toBeUndefined();
});
});

0 comments on commit 6b12cc3

Please sign in to comment.