From 20ffc0fae54849e459e0d6dc5d139f89e9863565 Mon Sep 17 00:00:00 2001 From: Roshni Naveena S Date: Wed, 23 Oct 2024 09:03:27 +0530 Subject: [PATCH 01/10] adding the support for extension annotation object --- lib/compile/csdl2openapi.js | 76 +++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/lib/compile/csdl2openapi.js b/lib/compile/csdl2openapi.js index 3cacb3a..a1f7706 100644 --- a/lib/compile/csdl2openapi.js +++ b/lib/compile/csdl2openapi.js @@ -106,7 +106,58 @@ module.exports.csdl2openapi = function ( if (externalDocs && Object.keys(externalDocs).length > 0) { openapi.externalDocs = externalDocs; } + const extensions = getExtensions(csdl, 'root'); + if (extensions && Object.keys(extensions).length > 0) { + Object.assign(openapi, extensions); + } + + // function to read @OpenAPI.Extensions and get them in the generated openAPI document + function getExtensions(csdl, level) { + let extensionObj = {}; + let containerSchema = {}; + if (level ==='root'){ + const namespace = csdl.$EntityContainer ? nameParts(csdl.$EntityContainer).qualifier : null; + containerSchema = csdl.$EntityContainer ? csdl[namespace] : {}; + } + else if(level === 'schema' || level === 'operation'){ + containerSchema = csdl; + } + + for (const [key, value] of Object.entries(containerSchema)) { + if (key.startsWith('@OpenAPI.Extensions')) { + const annotationProperties = key.split('@OpenAPI.Extensions.')[1]; + const keys = annotationProperties.split('.'); + keys[0] = "x-sap-" + keys[0]; + if (keys.length === 1) { + extensionObj[keys[0]] = value; + } else { + nestedAnnotation(extensionObj, keys[0], keys, value); + } + } + } + return extensionObj; + } + + function nestedAnnotation(resObj, openapiProperty, keys, value) { + if (resObj[openapiProperty] === undefined) { + resObj[openapiProperty] = {}; + } + + let node = resObj[openapiProperty]; + // traverse the annotation property and define the objects if they're not defined + for (let nestedIndex = 1; nestedIndex < keys.length - 1; nestedIndex++) { + const nestedElement = keys[nestedIndex]; + if (node[nestedElement] === undefined) { + node[nestedElement] = {}; + } + node = node[nestedElement]; + } + + // set value annotation property + node[keys[keys.length - 1]] = value; + } + if (!csdl.$EntityContainer) { delete openapi.servers; delete openapi.tags; @@ -1464,6 +1515,10 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot : response(204, "Success", undefined, overload[voc.Capabilities.OperationRestrictions]?.ErrorResponses), } }; + const actionExtension = getExtensions(overload, 'operation'); + if (Object.keys(actionExtension).length > 0) { + Object.assign(pathItem.post, actionExtension); + } const description = actionImport[voc.Core.LongDescription] || overload[voc.Core.LongDescription]; if (description) pathItem.post.description = description; if (prefixParameters.length > 0) pathItem.post.parameters = [...prefixParameters]; @@ -1574,6 +1629,10 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot responses: response(200, "Success", overload.$ReturnType, overload[voc.Capabilities.OperationRestrictions]?.ErrorResponses), } }; + const functionExtension = getExtensions(overload, 'operation'); + if (Object.keys(functionExtension).length > 0) { + Object.assign(pathItem.get, functionExtension); + } const iDescription = functionImport[voc.Core.LongDescription] || overload[voc.Core.LongDescription]; if (iDescription) pathItem.get.description = iDescription; customParameters(pathItem.get, overload[voc.Capabilities.OperationRestrictions] || {}); @@ -1757,6 +1816,23 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot break; } } + + // Add @OpenAPI.Extensions at entity level to schema object + Object.keys(csdl).filter(name => isIdentifier(name)).forEach(namespace => { + const schema = csdl[namespace]; + Object.keys(schema).filter(name => isIdentifier(name)).forEach(name => { + const type = schema[name]; + if (type.$Kind === 'EntityType' || type.$Kind === 'ComplexType') { + const schemaName = namespace + "." + name + SUFFIX.read; + const extensions = getExtensions(type, 'schema'); + if (Object.keys(extensions).length > 0) { + unordered[schemaName] = unordered[schemaName] || {}; + Object.assign(unordered[schemaName], extensions); + } + } + }); + }); + const ordered = {}; for (const name of Object.keys(unordered).sort()) { ordered[name] = unordered[name]; From 383284aac40314132d3dc142279abc9336b2d76b Mon Sep 17 00:00:00 2001 From: Roshni Naveena S Date: Wed, 23 Oct 2024 09:34:27 +0530 Subject: [PATCH 02/10] added test case --- test/lib/compile/openapi.test.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/lib/compile/openapi.test.js b/test/lib/compile/openapi.test.js index e9b0656..43c9285 100644 --- a/test/lib/compile/openapi.test.js +++ b/test/lib/compile/openapi.test.js @@ -391,4 +391,32 @@ describe('OpenAPI export', () => { expect(openAPI.externalDocs.url).toBe('https://help.sap.com/docs/product/123.html'); } ); + + test('OpenAPI annotations: @OpenAPI.Extensions annotation is added to the openapi document', () => { + const csn = cds.compile.to.csn(` + namespace sap.OpenAPI.test; + @OpenAPI.Extensions: { + ![compliance-level]: 'sap:base:v1' + } + service A { + @OpenAPI.Extensions: { + ![dpp-is-potentially-sensitive]: 'true' + } + entity E1 { + key id: String(4); + oid: String(128); + } + + @OpenAPI.Extensions: { + ![operation-intent]: 'read-collection' + } + function F1(param: String) returns String; + + }`); + const openAPI = toOpenApi(csn); + expect(openAPI).toBeDefined(); + expect(openAPI['x-sap-compliance-level']).toBe('sap:base:v1'); + 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'); + }); }); From 57e08602b74f3c9ea0dea30ceb3e12c0b3e3a273 Mon Sep 17 00:00:00 2001 From: Roshni Naveena S Date: Sat, 2 Nov 2024 18:52:00 +0530 Subject: [PATCH 03/10] Update CHANGELOG.md --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39ef72a..ae70785 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ 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.0 - tbd + +### Added + +- Now supports `x-sap` extensions using `@OpenAPI.Extensions` annotations in service, entity and function/action level. + ## Version 1.0.7 - 17.10.2024 ### Fixed From a33bb40d21d7129c759dad06fa77d7800e21b640 Mon Sep 17 00:00:00 2001 From: Roshni Naveena S Date: Mon, 11 Nov 2024 15:04:45 +0530 Subject: [PATCH 04/10] Update csdl2openapi.js --- lib/compile/csdl2openapi.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/compile/csdl2openapi.js b/lib/compile/csdl2openapi.js index 75b28cc..1968066 100644 --- a/lib/compile/csdl2openapi.js +++ b/lib/compile/csdl2openapi.js @@ -127,7 +127,9 @@ module.exports.csdl2openapi = function ( if (key.startsWith('@OpenAPI.Extensions')) { const annotationProperties = key.split('@OpenAPI.Extensions.')[1]; const keys = annotationProperties.split('.'); - keys[0] = "x-sap-" + keys[0]; + if (!keys[0].startsWith("x-sap-")) { + keys[0] = (keys[0].startsWith("sap-") ? "x-" : "x-sap-") + keys[0]; + } if (keys.length === 1) { extensionObj[keys[0]] = value; } else { From 4a5b185b947b342c6bd303fcec5cac4e77cb2405 Mon Sep 17 00:00:00 2001 From: Roshni Naveena S Date: Mon, 11 Nov 2024 15:09:21 +0530 Subject: [PATCH 05/10] Update openapi.test.js --- test/lib/compile/openapi.test.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/lib/compile/openapi.test.js b/test/lib/compile/openapi.test.js index 43c9285..1d0c76b 100644 --- a/test/lib/compile/openapi.test.js +++ b/test/lib/compile/openapi.test.js @@ -408,7 +408,11 @@ describe('OpenAPI export', () => { } @OpenAPI.Extensions: { - ![operation-intent]: 'read-collection' + ![x-sap-operation-intent]: 'read-collection', + ![sap-deprecated-operation] : { + deprecationDate: '2022-12-31', + successorOperationId: 'successorOperation' + } } function F1(param: String) returns String; @@ -418,5 +422,6 @@ describe('OpenAPI export', () => { expect(openAPI['x-sap-compliance-level']).toBe('sap:base:v1'); 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'); + expect(openAPI.paths["/F1"].get["x-sap-deprecated-operation"].deprecationDate).toBe('2022-12-31'); }); }); From fdbbb9c481d5e7143230feaa0093d60a75feb5e0 Mon Sep 17 00:00:00 2001 From: Roshni Naveena S Date: Mon, 11 Nov 2024 22:28:54 +0530 Subject: [PATCH 06/10] Update openapi.test.js --- test/lib/compile/openapi.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/lib/compile/openapi.test.js b/test/lib/compile/openapi.test.js index 1d0c76b..6f330cc 100644 --- a/test/lib/compile/openapi.test.js +++ b/test/lib/compile/openapi.test.js @@ -423,5 +423,6 @@ describe('OpenAPI export', () => { 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'); 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'); }); }); From 11a9d3dec65b827122cc193349fa113a0f0f6626 Mon Sep 17 00:00:00 2001 From: Roshni Naveena S Date: Mon, 18 Nov 2024 15:01:08 +0530 Subject: [PATCH 07/10] adding schema and enum check --- lib/compile/csdl2openapi.js | 47 ++++++++++++++++++++++++++++++++ test/lib/compile/openapi.test.js | 4 ++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/lib/compile/csdl2openapi.js b/lib/compile/csdl2openapi.js index aa36c96..0d07f53 100644 --- a/lib/compile/csdl2openapi.js +++ b/lib/compile/csdl2openapi.js @@ -137,9 +137,56 @@ module.exports.csdl2openapi = function ( } } } + let extensionEnums = { + "x-sap-compliance-level": {allowedValues: ["sap:base:v1", "sap:core:v1", "sap:core:v2" ] } , + "x-sap-api-type": {allowedValues: [ "ODATA", "ODATAV4", "REST" , "SOAP"] }, + "x-sap-direction": {allowedValues: ["inbound", "outbound", "mixed"] , default : "inbound" }, + "x-sap-dpp-entity-semantics": {allowedValues: ["sap:DataSubject", "sap:DataSubjectDetails", "sap:Other"] }, + "x-sap-dpp-field-semantics": {allowedValues: ["sap:DataSubjectID", "sap:ConsentID", "sap:PurposeID", "sap:ContractRelatedID", "sap:LegalEntityID", "sap:DataControllerID", "sap:UserID", "sap:EndOfBusinessDate", "sap:BlockingDate", "sap:EndOfRetentionDate"] }, + }; + checkForExtentionEnums(extensionObj, extensionEnums); + + let extenstionSchema = { + "x-sap-stateInfo": ['state', 'deprecationDate', 'decomissionedDate', 'link'], + "x-sap-ext-overview": ['name', 'values'], + "x-sap-deprecated-operation" : ['deprecationDate', 'successorOperationRef', "successorOperationId"], + "x-sap-odm-semantic-key" : ['name', 'values'], + }; + + checkForExtentionSchema(extensionObj, extenstionSchema); return extensionObj; } + function checkForExtentionEnums(extensionObj, extensionEnums){ + for (const [key, value] of Object.entries(extensionObj)) { + if(extensionEnums[key] && extensionEnums[key].allowedValues && !extensionEnums[key].allowedValues.includes(value)){ + if(extensionEnums[key].default){ + extensionObj[key] = extensionEnums[key].default; + } + else{ + delete extensionObj[key]; + } + } + } + } + + function checkForExtentionSchema(extensionObj, extenstionSchema) { + for (const [key, value] of Object.entries(extensionObj)) { + if (extenstionSchema[key]) { + if (Array.isArray(value)) { + extensionObj[key] = value.filter((v) => extenstionSchema[key].includes(v)); + } else if (typeof value === "object" && value !== null) { + for (const field in value) { + if (!extenstionSchema[key].includes(field)) { + delete extensionObj[key][field]; + } + } + } + } + } + } + + function nestedAnnotation(resObj, openapiProperty, keys, value) { if (resObj[openapiProperty] === undefined) { resObj[openapiProperty] = {}; diff --git a/test/lib/compile/openapi.test.js b/test/lib/compile/openapi.test.js index 629c3e9..56300bb 100644 --- a/test/lib/compile/openapi.test.js +++ b/test/lib/compile/openapi.test.js @@ -444,7 +444,8 @@ service CatalogService { ![x-sap-operation-intent]: 'read-collection', ![sap-deprecated-operation] : { deprecationDate: '2022-12-31', - successorOperationId: 'successorOperation' + successorOperationId: 'successorOperation', + notValidKey: 'notValidValue' } } function F1(param: String) returns String; @@ -457,5 +458,6 @@ service CatalogService { expect(openAPI.paths["/F1"].get["x-sap-operation-intent"]).toBe('read-collection'); 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(); }); }); From f9dfb3ac35f6d0ea7b6228c36f37b01425901534 Mon Sep 17 00:00:00 2001 From: Roshni Naveena S Date: Mon, 25 Nov 2024 19:32:50 +0530 Subject: [PATCH 08/10] Update openapi.test.js --- test/lib/compile/openapi.test.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/test/lib/compile/openapi.test.js b/test/lib/compile/openapi.test.js index 56300bb..d260ba2 100644 --- a/test/lib/compile/openapi.test.js +++ b/test/lib/compile/openapi.test.js @@ -429,8 +429,15 @@ service CatalogService { const csn = cds.compile.to.csn(` namespace sap.OpenAPI.test; @OpenAPI.Extensions: { - ![compliance-level]: 'sap:base:v1' - } + ![compliance-level]: 'sap:base:v1', + ![x-sap-ext-overview]: { + name : 'Communication Scenario', + values: { + text : 'Planning Calendar API Integration', + format: 'plain' + } + } + } service A { @OpenAPI.Extensions: { ![dpp-is-potentially-sensitive]: 'true' From 70e9a90058b9abf7368041dc08c04549a11820ba Mon Sep 17 00:00:00 2001 From: Roshni Naveena S Date: Thu, 28 Nov 2024 13:04:56 +0530 Subject: [PATCH 09/10] Update openapi.test.js --- test/lib/compile/openapi.test.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/lib/compile/openapi.test.js b/test/lib/compile/openapi.test.js index d260ba2..d0f7409 100644 --- a/test/lib/compile/openapi.test.js +++ b/test/lib/compile/openapi.test.js @@ -461,6 +461,9 @@ service CatalogService { const openAPI = toOpenApi(csn); 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'); expect(openAPI.paths["/F1"].get["x-sap-deprecated-operation"].deprecationDate).toBe('2022-12-31'); From 1e7102cdcc99062944a3ff7380059e862da7059f Mon Sep 17 00:00:00 2001 From: Roshni Naveena S Date: Thu, 28 Nov 2024 13:09:29 +0530 Subject: [PATCH 10/10] Update openapi.test.js --- test/lib/compile/openapi.test.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/lib/compile/openapi.test.js b/test/lib/compile/openapi.test.js index d0f7409..c79e939 100644 --- a/test/lib/compile/openapi.test.js +++ b/test/lib/compile/openapi.test.js @@ -448,7 +448,7 @@ service CatalogService { } @OpenAPI.Extensions: { - ![x-sap-operation-intent]: 'read-collection', + ![x-sap-operation-intent]: 'read-collection for function', ![sap-deprecated-operation] : { deprecationDate: '2022-12-31', successorOperationId: 'successorOperation', @@ -456,6 +456,11 @@ service CatalogService { } } function F1(param: String) returns String; + + @OpenAPI.Extensions: { + ![x-sap-operation-intent]: 'read-collection for action' + } + action A1(param: String) returns String; }`); const openAPI = toOpenApi(csn); @@ -465,7 +470,8 @@ service CatalogService { 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'); + 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();