From 4bcbd7d157b60ca00b672738b2bd18d293272fb9 Mon Sep 17 00:00:00 2001 From: Daniel O'Grady Date: Thu, 29 Jan 2026 13:32:00 +0100 Subject: [PATCH 1/2] Fix max-len warnings --- lib/compile/csdl.js | 26 ++- lib/compile/csdl2openapi.js | 226 ++++++++++++++++++-------- lib/compile/diagram.js | 39 ++++- scripts/generate-call-graph-svg.js | 11 +- test/lib/compile/csdl2openapi.test.js | 78 ++++++--- test/lib/compile/openapi.test.js | 61 +++++-- 6 files changed, 322 insertions(+), 119 deletions(-) diff --git a/lib/compile/csdl.js b/lib/compile/csdl.js index b1a7896..130077b 100644 --- a/lib/compile/csdl.js +++ b/lib/compile/csdl.js @@ -8,11 +8,19 @@ const DEBUG = cds.debug('openapi'); const CDS_TERMS = Object.freeze({ Authorization: ['Authorizations', 'SecuritySchemes'], - Capabilities: ['BatchSupport', 'BatchSupported', 'ChangeTracking', 'CountRestrictions', 'DeleteRestrictions', 'DeepUpdateSupport', 'ExpandRestrictions', - 'FilterRestrictions', 'IndexableByKey', 'InsertRestrictions', 'KeyAsSegmentSupported', 'NavigationRestrictions', 'OperationRestrictions', - 'ReadRestrictions', 'SearchRestrictions', 'SelectSupport', 'SkipSupported', 'SortRestrictions', 'TopSupported', 'UpdateRestrictions'], - Core: ['AcceptableMediaTypes', 'Computed', 'ComputedDefaultValue', 'DefaultNamespace', 'Description', 'Example', 'Immutable', 'LongDescription', - 'OptionalParameter', 'Permissions', 'SchemaVersion'], + Capabilities: [ + 'BatchSupport', 'BatchSupported', 'ChangeTracking', 'CountRestrictions', + 'DeleteRestrictions', 'DeepUpdateSupport', 'ExpandRestrictions', 'FilterRestrictions', + 'IndexableByKey', 'InsertRestrictions', 'KeyAsSegmentSupported', + 'NavigationRestrictions', 'OperationRestrictions', 'ReadRestrictions', + 'SearchRestrictions', 'SelectSupport', 'SkipSupported', 'SortRestrictions', + 'TopSupported', 'UpdateRestrictions' + ], + Core: [ + 'AcceptableMediaTypes', 'Computed', 'ComputedDefaultValue', 'DefaultNamespace', + 'Description', 'Example', 'Immutable', 'LongDescription', 'OptionalParameter', + 'Permissions', 'SchemaVersion' + ], JSON: ['Schema'], Validation: ['AllowedValues', 'Exclusive', 'Maximum', 'Minimum', 'Pattern'] }) @@ -91,9 +99,13 @@ class CSDLMeta { const element = schema[iName2]; if (Array.isArray(element)) { element.filter(overload => overload.$IsBound).forEach(overload => { - const type = overload.$Parameter[0].$Type + (overload.$Parameter[0].$Collection ? '-c' : ''); + const type = overload.$Parameter[0].$Type + + (overload.$Parameter[0].$Collection ? '-c' : ''); if (!this.boundOverloads[type]) this.boundOverloads[type] = []; - this.boundOverloads[type].push({ name: (isDefaultNamespace ? iName2 : qualifiedName), overload }); + this.boundOverloads[type].push({ + name: (isDefaultNamespace ? iName2 : qualifiedName), + overload + }); }); } else if (element.$BaseType) { const base = this.namespaceQualifiedName(element.$BaseType); diff --git a/lib/compile/csdl2openapi.js b/lib/compile/csdl2openapi.js index 7ed02a7..41bf2a2 100644 --- a/lib/compile/csdl2openapi.js +++ b/lib/compile/csdl2openapi.js @@ -19,7 +19,8 @@ const DEBUG = cds.debug('openapi'); // Initialize cds.debug with the 'openapi' // - system query options for actions/functions/imports depending on $Collection // - 200 response for PATCH // - ETag for GET / If-Match for PATCH and DELETE depending on @Core.OptimisticConcurrency -// - CountRestrictions for GET collection-valued (containment) navigation - https://issues.oasis-open.org/browse/ODATA-1300 +// - CountRestrictions for GET collection-valued (containment) navigation +// https://issues.oasis-open.org/browse/ODATA-1300 // - InsertRestrictions/NonInsertableProperties // - InsertRestrictions/NonInsertableNavigationProperties // - see //TODO comments below @@ -87,7 +88,8 @@ const ER_ANNOTATIONS = Object.freeze( /** * 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 }} options * @return {object} OpenAPI description */ module.exports.csdl2openapi = function ( @@ -120,8 +122,8 @@ module.exports.csdl2openapi = function ( Object.keys(entityContainer).forEach(element => { if (entityContainer[element].$Type) { const fullTypeName = entityContainer[element].$Type; - const type = fullTypeName.startsWith(serviceName + '.') - ? fullTypeName.substring(serviceName.length + 1) + const type = fullTypeName.startsWith(serviceName + '.') + ? fullTypeName.substring(serviceName.length + 1) : nameParts(fullTypeName).name; if ((csdl[serviceName]?.[type]?.['@cds.autoexpose'] || csdl[serviceName]?.[type]?.['@cds.autoexposed']) && (!entityContainer[type] || type.endsWith('_texts'))) { @@ -181,11 +183,27 @@ module.exports.csdl2openapi = function ( } } const 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"] }, + "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" + ] + }, }; checkForExtensionEnums(extensionObj, extensionEnums); @@ -288,7 +306,8 @@ module.exports.csdl2openapi = function ( description = containerSchema[meta.voc.Core.Description]; } else { - description = "Use @Core.LongDescription: '...' or @Core.Description: '...' on your CDS service to provide a meaningful description."; + description = "Use @Core.LongDescription: '...' or @Core.Description: '...' " + + "on your CDS service to provide a meaningful description."; } description += (diagram ? new Diagram(meta).getResourceDiagram(entityContainer) : ''); let title; @@ -458,7 +477,10 @@ module.exports.csdl2openapi = function ( * @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) { + function pathItems( + paths, prefix, prefixParameters, element, root, sourceName, targetName, target, level, + navigationPath + ) { const name = prefix.substring(prefix.lastIndexOf('/') + 1); const type = meta.modelElement(element.$Type); const pathItem = {}; @@ -468,8 +490,12 @@ module.exports.csdl2openapi = function ( paths[prefix] = pathItem; if (prefixParameters.length > 0) pathItem.parameters = prefixParameters; - operationRead(pathItem, element, name, sourceName, targetName, target, level, restrictions, false, nonExpandable); - if (!root['$cds.autoexpose'] && element.$Collection && (element.$ContainsTarget || level < 2 && target)) { + operationRead( + pathItem, element, name, sourceName, targetName, target, level, restrictions, false, + nonExpandable + ); + if (!root['$cds.autoexpose'] && element.$Collection && + (element.$ContainsTarget || level < 2 && target)) { operationCreate(pathItem, element, name, sourceName, targetName, target, level, restrictions); } pathItemsForBoundOperations(paths, prefix, prefixParameters, element, sourceName); @@ -540,7 +566,10 @@ module.exports.csdl2openapi = function ( * @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) { + function pathItemsWithKey( + paths, prefix, prefixParameters, element, root, sourceName, targetName, target, level, + navigationPath, restrictions, nonExpandable + ) { 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); @@ -552,7 +581,10 @@ module.exports.csdl2openapi = function ( const pathItem = { parameters }; paths[path] = pathItem; - operationRead(pathItem, element, name, sourceName, targetName, target, level, restrictions, true, nonExpandable); + operationRead( + pathItem, element, name, sourceName, targetName, target, level, restrictions, + true, nonExpandable + ); if (!root['$cds.autoexpose']) { operationUpdate(pathItem, element, name, sourceName, target, level, restrictions, true); operationDelete(pathItem, element, name, sourceName, target, level, restrictions, true); @@ -577,14 +609,19 @@ module.exports.csdl2openapi = function ( * @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) { - 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 + function operationCreate( + pathItem, element, name, sourceName, targetName, target, level, restrictions + ) { + const insertRestrictions = restrictions.InsertRestrictions || + target?.[meta.voc.Capabilities.InsertRestrictions] || {}; + const countRestrictions = + target?.[meta.voc.Capabilities.CountRestrictions]?.Countable === false if (insertRestrictions.Insertable !== false) { const lname = pluralize.singular(splitName(name)); const type = meta.modelElement(element.$Type); pathItem.post = { - summary: insertRestrictions.Description || operationSummary('Creates', name, sourceName, level, true, true), + summary: insertRestrictions.Description || + operationSummary('Creates', name, sourceName, level, true, true), tags: [normaliseTag(sourceName)], requestBody: { description: type && type[meta.voc.Core.Description] || `New ${lname}`, @@ -595,7 +632,10 @@ module.exports.csdl2openapi = function ( } } }, - responses: response(201, `Created ${lname}`, { $Type: element.$Type }, insertRestrictions.ErrorResponses, !countRestrictions), + responses: response( + 201, `Created ${lname}`, { $Type: element.$Type }, + insertRestrictions.ErrorResponses, !countRestrictions + ), }; if (insertRestrictions.LongDescription) pathItem.post.description = insertRestrictions.LongDescription; if (targetName && sourceName != targetName) pathItem.post.tags.push(normaliseTag(targetName)); @@ -617,12 +657,12 @@ module.exports.csdl2openapi = function ( const lname = splitName(name); const sname = splitName(sourceName); - return `${operation} ${ - byKey ? 'a single ' : (collection ? 'a list of ' : '') - }${byKey ? pluralize.singular(lname) : lname - //TODO: suppress "a" for all singletons - }${level == 0 ? '' : (level == 1 && sname == 'me' ? ' of me' : ` of a ${pluralize.singular(sname)}`) - }.` + const prefix = byKey ? 'a single ' : (collection ? 'a list of ' : ''); + const itemName = byKey ? pluralize.singular(lname) : lname; + //TODO: suppress "a" for all singletons + const suffix = level == 0 ? '' : + (level == 1 && sname == 'me' ? ' of me' : ` of a ${pluralize.singular(sname)}`); + return `${operation} ${prefix}${itemName}${suffix}.`; } /** @@ -638,12 +678,16 @@ module.exports.csdl2openapi = function ( * @param {boolean} byKey Read by key * @param {array} nonExpandable Non-expandable navigation properties */ - function operationRead(pathItem, element, name, sourceName, targetName, target, level, restrictions, byKey, nonExpandable) { + function operationRead( + pathItem, element, name, sourceName, targetName, target, level, restrictions, byKey, + nonExpandable + ) { const targetRestrictions = target?.[meta.voc.Capabilities.ReadRestrictions]; const readRestrictions = restrictions.ReadRestrictions || targetRestrictions || {}; const readByKeyRestrictions = readRestrictions.ReadByKeyRestrictions; let readable = true; - const countRestrictions = target && (target[meta.voc.Capabilities.CountRestrictions]?.Countable === false); + const countRestrictions = target && + (target[meta.voc.Capabilities.CountRestrictions]?.Countable === false); if (byKey && readByKeyRestrictions && readByKeyRestrictions.Readable !== undefined) readable = readByKeyRestrictions.Readable; else if (readRestrictions.Readable !== undefined) @@ -655,18 +699,25 @@ module.exports.csdl2openapi = function ( const lname = splitName(name); const collection = !byKey && element.$Collection; const operation = { - summary: descriptions.Description || operationSummary('Retrieves', name, sourceName, level, element.$Collection, byKey), + summary: descriptions.Description || + operationSummary('Retrieves', name, sourceName, level, element.$Collection, byKey), tags: [normaliseTag(sourceName)], parameters: [], - responses: response(200, `Retrieved ${byKey ? pluralize.singular(lname) : lname}`, { $Type: element.$Type, $Collection: collection }, - byKey ? readByKeyRestrictions?.ErrorResponses : readRestrictions?.ErrorResponses, !countRestrictions) + responses: response( + 200, `Retrieved ${byKey ? pluralize.singular(lname) : lname}`, + { $Type: element.$Type, $Collection: collection }, + byKey ? readByKeyRestrictions?.ErrorResponses : readRestrictions?.ErrorResponses, + !countRestrictions + ) }; - const deltaSupported = element[meta.voc.Capabilities.ChangeTracking] && element[meta.voc.Capabilities.ChangeTracking].Supported; + const deltaSupported = element[meta.voc.Capabilities.ChangeTracking] && + element[meta.voc.Capabilities.ChangeTracking].Supported; if (!byKey && deltaSupported) { // @ts-expect-error - set above operation.responses[200].content['application/json'].schema.properties['@odata.deltaLink'] = { type: 'string', - example: `${basePath}/${name}?$deltatoken=opaque server-generated token for fetching the delta` + example: `${basePath}/${name}?$deltatoken=opaque ` + + 'server-generated token for fetching the delta' } } if (descriptions.LongDescription) operation.description = descriptions.LongDescription; @@ -815,13 +866,16 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot * @param {object} restrictions Navigation property restrictions of navigation segment */ function optionFilter(parameters, target, restrictions) { - const filterRestrictions = restrictions.FilterRestrictions || target?.[meta.voc.Capabilities.FilterRestrictions] || {}; + const filterRestrictions = restrictions.FilterRestrictions || + target?.[meta.voc.Capabilities.FilterRestrictions] || {}; if (filterRestrictions.Filterable !== false) { const filter = { name: `${queryOptionPrefix}filter`, - description: filterRestrictions[meta.voc.Core.Description] - || 'Filter items by property values, see [Filtering](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionfilter)', + description: filterRestrictions[meta.voc.Core.Description] || + 'Filter items by property values, see [Filtering]' + + '(http://docs.oasis-open.org/odata/odata/v4.01/' + + 'odata-v4.01-part1-protocol.html#sec_SystemQueryOptionfilter)', in: 'query', schema: { type: 'string' @@ -999,7 +1053,8 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot * @param {object} restrictions Navigation property restrictions of navigation segment */ function optionSearch(parameters, target, restrictions) { - const searchRestrictions = restrictions.SearchRestrictions ?? target?.[meta.voc.Capabilities.SearchRestrictions] ?? {}; + const searchRestrictions = restrictions.SearchRestrictions ?? + target?.[meta.voc.Capabilities.SearchRestrictions] ?? {}; if (searchRestrictions.Searchable !== false) { if (searchRestrictions[meta.voc.Core.Description]) { @@ -1098,14 +1153,19 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot * @param {object} restrictions Navigation property restrictions of navigation segment * @param {boolean} byKey Update by key */ - function operationUpdate(pathItem, element, name, sourceName, target, level, restrictions, byKey = false) { - const updateRestrictions = restrictions.UpdateRestrictions || target?.[meta.voc.Capabilities.UpdateRestrictions] || {}; - const countRestrictions = target?.[meta.voc.Capabilities.CountRestrictions]?.Countable === false; + function operationUpdate( + pathItem, element, name, sourceName, target, level, restrictions, byKey = false + ) { + 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]) { const type = meta.modelElement(element.$Type); const operation = { - summary: updateRestrictions.Description || operationSummary('Changes', name, sourceName, level, element.$Collection, byKey), + summary: updateRestrictions.Description || + operationSummary('Changes', name, sourceName, level, element.$Collection, byKey), tags: [normaliseTag(sourceName)], requestBody: { description: type && type[meta.voc.Core.Description] || 'New property values', @@ -1136,14 +1196,21 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot * @param {object} restrictions Navigation property restrictions of navigation segment * @param {boolean} byKey Delete by key */ - function operationDelete(pathItem, element, name, sourceName, target, level, restrictions, byKey = false) { - const deleteRestrictions = restrictions.DeleteRestrictions || target?.[meta.voc.Capabilities.DeleteRestrictions] || {}; - const countRestrictions = target?.[meta.voc.Capabilities.CountRestrictions]?.Countable === false + function operationDelete( + pathItem, element, name, sourceName, target, level, restrictions, byKey = false + ) { + const deleteRestrictions = restrictions.DeleteRestrictions || + target?.[meta.voc.Capabilities.DeleteRestrictions] || {}; + const countRestrictions = + target?.[meta.voc.Capabilities.CountRestrictions]?.Countable === false; if (deleteRestrictions.Deletable !== false) { pathItem.delete = { - summary: deleteRestrictions.Description || operationSummary('Deletes', name, sourceName, level, element.$Collection, byKey), + summary: deleteRestrictions.Description || + operationSummary('Deletes', name, sourceName, level, element.$Collection, byKey), tags: [normaliseTag(sourceName)], - responses: response(204, "Success", undefined, deleteRestrictions.ErrorResponses, !countRestrictions), + responses: response( + 204, "Success", undefined, deleteRestrictions.ErrorResponses, !countRestrictions + ), }; if (deleteRestrictions.LongDescription) pathItem.delete.description = deleteRestrictions.LongDescription; customParameters(pathItem.delete, deleteRestrictions); @@ -1160,7 +1227,9 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot * @param {number} level Number of navigation segments so far * @param {string} navigationPrefix Path for finding navigation restrictions */ - function pathItemsWithNavigation(paths, prefix, prefixParameters, type, root, sourceName, level, navigationPrefix) { + function pathItemsWithNavigation( + paths, prefix, prefixParameters, type, root, sourceName, level, navigationPrefix + ) { const navigationRestrictions = root[meta.voc.Capabilities.NavigationRestrictions] ?? {}; const rootNavigable = level == 0 && enumMember(navigationRestrictions.Navigability) != 'None' || level == 1 && enumMember(navigationRestrictions.Navigability) != 'Single' @@ -1321,7 +1390,8 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot function pathValueSuffix(typename) { //TODO: handle other Edm types, enumeration types, and type definitions if (['Edm.Int64', 'Edm.Int32', 'Edm.Int16', 'Edm.SByte', 'Edm.Byte', - 'Edm.Double', 'Edm.Single', 'Edm.Date', 'Edm.DateTimeOffset', 'Edm.Guid'].includes(typename)) return ''; + 'Edm.Double', 'Edm.Single', 'Edm.Date', 'Edm.DateTimeOffset', 'Edm.Guid' + ].includes(typename)) return ''; if (keyAsSegment) return ''; return `'`; } @@ -1370,14 +1440,26 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot * @param {string} sourceName Name of path source * @param {object} actionImport Action import */ - function pathItemAction(paths, prefix, prefixParameters, actionName, overload, sourceName, actionImport = {}) { + function pathItemAction( + paths, prefix, prefixParameters, actionName, overload, sourceName, actionImport = {} + ) { const name = actionName.indexOf('.') === -1 ? actionName : nameParts(actionName).name; const pathItem = { post: { - summary: actionImport[meta.voc.Core.Description] || overload[meta.voc.Core.Description] || `Invokes action ${name}`, - tags: [normaliseTag(overload[meta.voc.Common.Label] || sourceName || 'Service Operations')], - responses: overload.$ReturnType ? response(200, "Success", overload.$ReturnType, overload[meta.voc.Capabilities.OperationRestrictions]?.ErrorResponses) - : response(204, "Success", undefined, overload[meta.voc.Capabilities.OperationRestrictions]?.ErrorResponses), + summary: actionImport[meta.voc.Core.Description] || + overload[meta.voc.Core.Description] || `Invokes action ${name}`, + tags: [normaliseTag( + overload[meta.voc.Common.Label] || sourceName || 'Service Operations' + )], + responses: overload.$ReturnType ? + response( + 200, "Success", overload.$ReturnType, + overload[meta.voc.Capabilities.OperationRestrictions]?.ErrorResponses + ) : + response( + 204, "Success", undefined, + overload[meta.voc.Capabilities.OperationRestrictions]?.ErrorResponses + ), } }; const actionExtension = getExtensions(overload, 'operation'); @@ -1429,7 +1511,9 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot function pathItemFunctionImport(paths, name, child) { const overloads = meta.modelElement(child.$Function); console.assert(overloads, `Unknown function "${child.$Function}" in function import "${name}"`); - overloads && overloads.filter(overload => !overload.$IsBound).forEach(overload => pathItemFunction(paths, `/${name}`, [], child.$Function, overload, child.$EntitySet, child)); + overloads && overloads.filter(overload => !overload.$IsBound).forEach(overload => + pathItemFunction(paths, `/${name}`, [], child.$Function, overload, child.$EntitySet, child) + ); } /** @@ -1442,7 +1526,9 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot * @param {string} sourceName Name of path source * @param {object} functionImport Function Import */ - function pathItemFunction(paths, prefix, prefixParameters, functionName, overload, sourceName, functionImport = {}) { + function pathItemFunction( + paths, prefix, prefixParameters, functionName, overload, sourceName, functionImport = {} + ) { const name = functionName.indexOf('.') === -1 ? functionName : nameParts(functionName).name; let parameters = overload.$Parameter || []; if (overload.$IsBound) parameters = parameters.slice(1); @@ -1478,9 +1564,8 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot } param.schema = { type: 'string' }; if (param.description) param.description += ' \n'; else param.description = ''; - param.description += `This is ${ - p.$Collection ? 'a ' : '' - }URL-encoded JSON ${ + param.description += `This is ${p.$Collection ? 'a ' : '' + }URL-encoded JSON ${ p.$Collection ? 'array with items ' : '' }of type ${ meta.namespaceQualifiedName(p.$Type ?? 'Edm.String') @@ -1501,7 +1586,8 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot param.name = `@${p.$Name}`; else param.name = p.$Name; - if (!p.$Type || p.$Type === "Edm.String" || (type && (!type.$Type || type.$Type === "Edm.String"))) { + if (!p.$Type || p.$Type === "Edm.String" || + (type && (!type.$Type || type.$Type === "Edm.String"))) { if (param.description) param.description += ' \n'; else param.description = ''; param.description += "String value needs to be enclosed in single quotes"; @@ -1929,7 +2015,9 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot * @return {object} Map of properties */ function propertiesOfStructuredType(type) { - const properties = (type && type.$BaseType) ? propertiesOfStructuredType(meta.modelElement(type.$BaseType)) : {}; + const properties = (type && type.$BaseType) + ? propertiesOfStructuredType(meta.modelElement(type.$BaseType)) + : {}; if (type) { Object.keys(type).filter(name => isIdentifier(name)).forEach(name => { properties[name] = type[name]; @@ -1947,6 +2035,7 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot top: { name: `${queryOptionPrefix}top`, in: 'query', + // eslint-disable-next-line max-len description: 'Show only the first n items, see [Paging - Top](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptiontop)', schema: { type: 'integer', @@ -2047,6 +2136,7 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot { type: 'number' }, { type: 'string' } ], + // eslint-disable-next-line max-len description: 'The number of entities in the collection. Available when using the [$count](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptioncount) query option.', }; } @@ -2095,7 +2185,9 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot s = { type: 'string', format: 'date-time', - example: `2017-04-13T15:51:04${isNaN(element.$Precision) || element.$Precision === 0 ? '' : `.${'0'.repeat(element.$Precision)}`}Z` + example: `2017-04-13T15:51:04${isNaN(element.$Precision) || element.$Precision === 0 + ? '' + : `.${'0'.repeat(element.$Precision)}`}Z` }; break; case 'Edm.Decimal': { @@ -2304,7 +2396,9 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot }; } - const description = forParameter ? undefined : (element[meta.voc.Core.LongDescription] || element[meta.voc.Core.Description]); + const description = forParameter + ? undefined + : (element[meta.voc.Core.LongDescription] || element[meta.voc.Core.Description]); if (description) { if (s.$ref) s = { allOf: [s] }; s.description = description; @@ -2365,7 +2459,9 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot * @param {object} entityContainer Entity Container object */ function getSecuritySchemes(components, entityContainer) { - const authorizations = entityContainer && entityContainer[meta.voc.Authorization.Authorizations] ? entityContainer[meta.voc.Authorization.Authorizations] : []; + const authorizations = entityContainer && entityContainer[meta.voc.Authorization.Authorizations] + ? entityContainer[meta.voc.Authorization.Authorizations] + : []; const schemes = {}; const location = { Header: 'header', QueryOption: 'query', Cookie: 'cookie' }; authorizations.forEach(auth => { @@ -2441,7 +2537,9 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot * @param {object} entityContainer Entity Container object */ function security(openapi, entityContainer) { - const securitySchemes = entityContainer && entityContainer[meta.voc.Authorization.SecuritySchemes] ? entityContainer[meta.voc.Authorization.SecuritySchemes] : []; + const securitySchemes = entityContainer && entityContainer[meta.voc.Authorization.SecuritySchemes] + ? entityContainer[meta.voc.Authorization.SecuritySchemes] + : []; // check if securitySchemas exist if it does not exist then throw a warning if (securitySchemes.length === 0) { DEBUG?.('No security schemes defined in the entity container'); diff --git a/lib/compile/diagram.js b/lib/compile/diagram.js index 98497ab..aff44b9 100644 --- a/lib/compile/diagram.js +++ b/lib/compile/diagram.js @@ -18,7 +18,12 @@ class Diagram { #meta #comma = '' // TODO: make colors configurable - #color = { resource: '{bg:lawngreen}', entityType: '{bg:lightslategray}', complexType: '', external: '{bg:whitesmoke}' } + #color = { + resource: '{bg:lawngreen}', + entityType: '{bg:lightslategray}', + complexType: '', + external: '{bg:whitesmoke}' + } constructor(meta) { this.#meta = meta; @@ -39,16 +44,26 @@ class Diagram { .filter(name => isIdentifier(name) && ['EntityType', 'ComplexType'].includes(schema[name].$Kind)) .forEach(typeName => { const type = schema[typeName]; - diagram += `${this.#comma + (type.$BaseType ? `[${nameParts(type.$BaseType).name}]^` : '')}[${typeName}${type.$Kind == 'EntityType' ? this.#color.entityType : this.#color.complexType}]`; + diagram += `${this.#comma + + (type.$BaseType ? `[${nameParts(type.$BaseType).name}]^` : '') + }[${typeName}${type.$Kind == 'EntityType' ? + this.#color.entityType : this.#color.complexType}]`; Object.keys(type).filter(name => isIdentifier(name)).forEach(propertyName => { const property = type[propertyName]; const targetNP = nameParts(property.$Type || 'Edm.String'); if (property.$Kind == 'NavigationProperty' || targetNP.qualifier != 'Edm') { const target = this.#meta.modelElement(property.$Type); - const bidirectional = property.$Partner && target && target[property.$Partner] && target[property.$Partner].$Partner == propertyName; + const bidirectional = property.$Partner && target && + target[property.$Partner] && + target[property.$Partner].$Partner == propertyName; // Note: if the partner has the same name then it will also be depicted if (!bidirectional || propertyName <= property.$Partner) { - diagram += `,[${typeName}]${(property.$Kind != 'NavigationProperty' || property.$ContainsTarget) ? '++' : (bidirectional ? cardinality(target[property.$Partner]) : '')}-${cardinality(property)}${(property.$Kind != 'NavigationProperty' || bidirectional) ? '' : '>'}[${target ? targetNP.name : property.$Type + this.#color.external}]`; + diagram += `,[${typeName}]${ + (property.$Kind != 'NavigationProperty' || property.$ContainsTarget) ? + '++' : (bidirectional ? cardinality(target[property.$Partner]) : '') + }-${cardinality(property)}${ + (property.$Kind != 'NavigationProperty' || bidirectional) ? '' : '>' + }[${target ? targetNP.name : property.$Type + this.#color.external}]`; } } }); @@ -59,8 +74,9 @@ class Diagram { Object.keys(entityContainer).filter(name => isIdentifier(name)).reverse().forEach(name => { const resource = entityContainer[name]; if (resource.$Type) { - diagram += `${this.#comma}[${name}%20${this.#color.resource}]` // additional space in case entity set and type have same name - + `++-${cardinality(resource)}>[${nameParts(resource.$Type).name}]`; + // additional space in case entity set and type have same name + diagram += `${this.#comma}[${name}%20${this.#color.resource}]` + + `++-${cardinality(resource)}>[${nameParts(resource.$Type).name}]`; } else if (resource.$Action) { diagram += `${this.#comma}[${name}${this.#color.resource}]`; const overload = this.#meta.modelElement(resource.$Action).find(pOverload => !pOverload.$IsBound); @@ -77,7 +93,13 @@ class Diagram { }); if (diagram != '') { - diagram = `\n\n## Entity Data Model\n![ER Diagram](https://yuml.me/diagram/class/${diagram})\n\n### Legend\n![Legend](https://yuml.me/diagram/plain;dir:TB;scale:60/class/[External.Type${this.#color.external}],[ComplexType${this.#color.complexType}],[EntityType${this.#color.entityType}],[EntitySet/Singleton/Operation${this.#color.resource}])`; + diagram = `\n\n## Entity Data Model\n` + + `![ER Diagram](https://yuml.me/diagram/class/${diagram})\n\n### Legend\n` + + `![Legend](https://yuml.me/diagram/plain;dir:TB;scale:60/class/` + + `[External.Type${this.#color.external}],` + + `[ComplexType${this.#color.complexType}],` + + `[EntityType${this.#color.entityType}],` + + `[EntitySet/Singleton/Operation${this.#color.resource}])`; } return diagram; @@ -100,7 +122,8 @@ class Diagram { for (const param of overload.$Parameter || []) { const type = this.#meta.modelElement(param.$Type || "Edm.String"); if (type) { - diag += `${this.#comma}[${name}${this.#color.resource}]in-${cardinality(param.$Type)}>[${nameParts(param.$Type).name}]`; + diag += `${this.#comma}[${name}${this.#color.resource}]in-` + + `${cardinality(param.$Type)}>[${nameParts(param.$Type).name}]`; } } return diag; diff --git a/scripts/generate-call-graph-svg.js b/scripts/generate-call-graph-svg.js index 125c6f5..35e1cc5 100644 --- a/scripts/generate-call-graph-svg.js +++ b/scripts/generate-call-graph-svg.js @@ -100,7 +100,8 @@ function generateCallGraph(program) { function findContainingFunction(node) { let current = node.parent; while (current) { - if (ts.isFunctionDeclaration(current) || ts.isMethodDeclaration(current) || ts.isFunctionExpression(current) || ts.isArrowFunction(current)) { + if (ts.isFunctionDeclaration(current) || ts.isMethodDeclaration(current) || + ts.isFunctionExpression(current) || ts.isArrowFunction(current)) { return current; } current = current.parent; @@ -113,7 +114,7 @@ function getFunctionName(declaration, checker) { if (symbol) { return symbol.getName(); } - + if (declaration.name && ts.isIdentifier(declaration.name)) { return declaration.name.text; } @@ -132,7 +133,7 @@ function getFunctionName(declaration, checker) { function convertCallGraphToMermaid(callGraph, filterPrefix) { let mermaidString = 'graph TD\n'; const nodes = new Set(); - + const createNodeId = (fileName, functionName) => { const safeFileName = path.basename(fileName).replace(/[.-]/g, '_'); const safeFunctionName = functionName.replace(/[^a-zA-Z0-9_]/g, '_'); @@ -156,7 +157,7 @@ function convertCallGraphToMermaid(callGraph, filterPrefix) { mermaidString += ` ${calleeId}["${call.callee}"]\n`; nodes.add(calleeId); } - + const link = ` ${callerId} --> ${calleeId}\n`; if (!mermaidString.includes(link)) { mermaidString += link; @@ -181,7 +182,7 @@ function generateSvgFromMermaid(mermaidDiagram, outputPath) { } resolve(stdout); }); - + if (child.stdin) { child.stdin.write(mermaidDiagram); child.stdin.end(); diff --git a/test/lib/compile/csdl2openapi.test.js b/test/lib/compile/csdl2openapi.test.js index 010098f..7ff7fc4 100644 --- a/test/lib/compile/csdl2openapi.test.js +++ b/test/lib/compile/csdl2openapi.test.js @@ -454,7 +454,9 @@ describe("Edge cases", () => { const expected_sources_get_param = { description: - "Order items by property values, see [Sorting](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionorderby)", + "Order items by property values, see [Sorting]" + + "(http://docs.oasis-open.org/odata/odata/v4.01/" + + "odata-v4.01-part1-protocol.html#sec_SystemQueryOptionorderby)", explode: false, in: "query", name: "$orderby", @@ -518,7 +520,8 @@ describe("Edge cases", () => { $Kind: "TypeDefinition", $UnderlyingType: "Edm.Stream", "@JSON.Schema": - '{"type":"object","additionalProperties":false,"patternProperties":{"^[\\\\w\\\\.\\\\-\\\\/]+$":{"type":"string"}}}', + '{"type":"object","additionalProperties":false,' + + '"patternProperties":{"^[\\\\w\\\\.\\\\-\\\\/]+$":{"type":"string"}}}', }, typeDefinitionNew: { $Kind: "TypeDefinition", @@ -592,7 +595,8 @@ describe("Edge cases", () => { $Kind: "TypeDefinition", $UnderlyingType: "Edm.Stream", "@Org.OData.JSON.V1.Schema": - '{"type":"object","additionalProperties":false,"patternProperties":{"^[\\\\w\\\\.\\\\-\\\\/]+$":{"type":"string"}}}', + '{"type":"object","additionalProperties":false,' + + '"patternProperties":{"^[\\\\w\\\\.\\\\-\\\\/]+$":{"type":"string"}}}', }, typeDefinitionNew: { $Kind: "TypeDefinition", @@ -819,9 +823,12 @@ describe("Edge cases", () => { const actual = lib.csdl2openapi(csdl, {}); assert.ok(actual.paths["/act"], "Path /act should exist"); assert.ok(actual.paths["/act"].post, "POST operation should exist"); - assert.ok(actual.paths["/act"].post.requestBody, "requestBody should exist for action without parameters"); - assert.strictEqual(actual.paths["/act"].post.requestBody.required, false, "requestBody should not be required"); - assert.ok(actual.paths["/act"].post.requestBody.content["application/json"], "application/json content-type should be present"); + assert.ok(actual.paths["/act"].post.requestBody, + "requestBody should exist for action without parameters"); + assert.strictEqual(actual.paths["/act"].post.requestBody.required, false, + "requestBody should not be required"); + assert.ok(actual.paths["/act"].post.requestBody.content["application/json"], + "application/json content-type should be present"); assert.deepStrictEqual( actual.paths["/act"].post.requestBody.content["application/json"].schema, { type: "object" }, @@ -888,6 +895,7 @@ describe("Edge cases", () => { schema: { type: "string" }, example: "{}", description: + // eslint-disable-next-line max-len "This is URL-encoded JSON of type this.Complex, see [Complex and Collection Literals](https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#sec_ComplexandCollectionLiterals)", }, ], @@ -908,6 +916,7 @@ describe("Edge cases", () => { schema: { type: "string" }, example: "{}", description: + // eslint-disable-next-line max-len "param description \nThis is URL-encoded JSON of type this.Complex, see [Complex and Collection Literals](https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#sec_ComplexandCollectionLiterals)", }, { @@ -917,6 +926,7 @@ describe("Edge cases", () => { schema: { type: "string" }, example: "[]", description: + // eslint-disable-next-line max-len "This is a URL-encoded JSON array with items of type Edm.String, see [Complex and Collection Literals](https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#sec_ComplexandCollectionLiterals)", }, ], @@ -990,6 +1000,7 @@ describe("Edge cases", () => { in: "query", required: true, description: + // eslint-disable-next-line max-len "Dates to be skipped \nThis is a URL-encoded JSON array with items of type Edm.Date, see [Complex and Collection Literals](https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#sec_ComplexandCollectionLiterals)", schema: { type: "string" }, example: "[]", @@ -1217,6 +1228,7 @@ describe("Edge cases", () => { name: "$filter", schema: { type: "string" }, description: + // eslint-disable-next-line max-len "Filter items by property values, see [Filtering](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionfilter)", }, { $ref: "#/components/parameters/count" }, @@ -1399,6 +1411,7 @@ describe("Edge cases", () => { name: "$filter", schema: { type: "string" }, description: + // eslint-disable-next-line max-len "Filter items by property values, see [Filtering](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionfilter)", }, { $ref: "#/components/parameters/count" }, @@ -1406,7 +1419,9 @@ describe("Edge cases", () => { in: "query", name: "$orderby", description: - "Order items by property values, see [Sorting](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionorderby)", + "Order items by property values, see [Sorting]" + + "(http://docs.oasis-open.org/odata/odata/v4.01/" + + "odata-v4.01-part1-protocol.html#sec_SystemQueryOptionorderby)", explode: false, schema: { type: "array", @@ -1428,7 +1443,9 @@ describe("Edge cases", () => { in: "query", name: "$select", description: - "Select properties to be returned, see [Select](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionselect)", + "Select properties to be returned, see [Select]" + + "(http://docs.oasis-open.org/odata/odata/v4.01/" + + "odata-v4.01-part1-protocol.html#sec_SystemQueryOptionselect)", explode: false, schema: { type: "array", @@ -1592,6 +1609,7 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot name: "$filter", schema: { type: "string" }, description: + // eslint-disable-next-line max-len "Filter items by property values, see [Filtering](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionfilter)", }, { $ref: "#/components/parameters/count" }, @@ -1599,7 +1617,9 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot in: "query", name: "$orderby", description: - "Order items by property values, see [Sorting](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionorderby)", + "Order items by property values, see [Sorting]" + + "(http://docs.oasis-open.org/odata/odata/v4.01/" + + "odata-v4.01-part1-protocol.html#sec_SystemQueryOptionorderby)", explode: false, schema: { type: "array", @@ -1619,7 +1639,9 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot in: "query", name: "$select", description: - "Select properties to be returned, see [Select](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionselect)", + "Select properties to be returned, see [Select]" + + "(http://docs.oasis-open.org/odata/odata/v4.01/" + + "odata-v4.01-part1-protocol.html#sec_SystemQueryOptionselect)", explode: false, schema: { type: "array", @@ -1775,6 +1797,7 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot name: "$filter", schema: { type: "string" }, description: + // eslint-disable-next-line max-len "Filter items by property values, see [Filtering](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionfilter)", }, { $ref: "#/components/parameters/count" }, @@ -1782,7 +1805,9 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot in: "query", name: "$orderby", description: - "Order items by property values, see [Sorting](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionorderby)", + "Order items by property values, see [Sorting]" + + "(http://docs.oasis-open.org/odata/odata/v4.01/" + + "odata-v4.01-part1-protocol.html#sec_SystemQueryOptionorderby)", explode: false, schema: { type: "array", @@ -1804,7 +1829,9 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot in: "query", name: "$select", description: - "Select properties to be returned, see [Select](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionselect)", + "Select properties to be returned, see [Select]" + + "(http://docs.oasis-open.org/odata/odata/v4.01/" + + "odata-v4.01-part1-protocol.html#sec_SystemQueryOptionselect)", explode: false, schema: { type: "array", @@ -1991,6 +2018,7 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot in: "query", name: "$select", description: + // eslint-disable-next-line max-len "Select properties to be returned, see [Select](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionselect)", explode: false, schema: { @@ -2182,10 +2210,13 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot const csdl = require('./data/non-expandable.json'); const actual = lib.csdl2openapi(csdl, {}); const pagesSchema = actual.components.schemas['CatalogService.Pages']; - - assert.strictEqual(Object.hasOwn(pagesSchema.properties, 'parent'), false, 'parent navigation property should be excluded from schema'); - assert.strictEqual(Object.hasOwn(pagesSchema.properties, 'parent_ID'), true, 'parent_ID foreign key should be included in schema'); - assert.strictEqual(Object.hasOwn(pagesSchema.properties, 'number'), true, 'number property should be included in schema'); + + assert.strictEqual(Object.hasOwn(pagesSchema.properties, 'parent'), false, + 'parent navigation property should be excluded from schema'); + assert.strictEqual(Object.hasOwn(pagesSchema.properties, 'parent_ID'), true, + 'parent_ID foreign key should be included in schema'); + assert.strictEqual(Object.hasOwn(pagesSchema.properties, 'number'), true, + 'number property should be included in schema'); }); test("Default Namespace", () => { @@ -2337,12 +2368,15 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot assert.deepStrictEqual( actual.info.description.split("\n"), [ - "Use @Core.LongDescription: '...' or @Core.Description: '...' on your CDS service to provide a meaningful description.", + "Use @Core.LongDescription: '...' or @Core.Description: '...' " + + "on your CDS service to provide a meaningful description.", "", "## Entity Data Model", + // eslint-disable-next-line max-len "![ER Diagram](https://yuml.me/diagram/class/[root{bg:lightslategray}],[root]->[other],[other{bg:lightslategray}],[act{bg:lawngreen}]->[root],[act{bg:lawngreen}]in->[root],[others%20{bg:lawngreen}]++-*>[other],[roots%20{bg:lawngreen}]++-*>[root])", "", "### Legend", + // eslint-disable-next-line max-len "![Legend](https://yuml.me/diagram/plain;dir:TB;scale:60/class/[External.Type{bg:whitesmoke}],[ComplexType],[EntityType{bg:lightslategray}],[EntitySet/Singleton/Operation{bg:lawngreen}])", ], "diagram" @@ -2550,7 +2584,8 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot }, { "@odata.type": - "https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Authorization.V1.xml#Auth.ApiKey", + "https://oasis-tcs.github.io/odata-vocabularies/vocabularies/" + + "Org.OData.Authorization.V1.xml#Auth.ApiKey", Name: "api_key", Description: "Authentication via API key", KeyName: "x-api-key", @@ -2807,13 +2842,17 @@ describe("CAP / CS01", () => { name: "$filter", schema: { type: "string" }, description: + // eslint-disable-next-line max-len "Filter items by property values, see [Filtering](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionfilter)\n\nRequired filter properties:\n- two", }, { $ref: "#/components/parameters/count" }, { in: "query", name: "$orderby", - description: "Order items by property values, see [Sorting](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionorderby)", + description: + "Order items by property values, see [Sorting]" + + "(http://docs.oasis-open.org/odata/odata/v4.01/" + + "odata-v4.01-part1-protocol.html#sec_SystemQueryOptionorderby)", explode: false, schema: { type: "array", @@ -2828,6 +2867,7 @@ describe("CAP / CS01", () => { in: "query", name: "$select", description: + // eslint-disable-next-line max-len "Select properties to be returned, see [Select](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionselect)", explode: false, schema: { diff --git a/test/lib/compile/openapi.test.js b/test/lib/compile/openapi.test.js index 27d60c8..2b3a9f9 100644 --- a/test/lib/compile/openapi.test.js +++ b/test/lib/compile/openapi.test.js @@ -70,7 +70,13 @@ function checkAnnotations(csn, annotations, scenario = SCENARIO.positive, proper } // all other components of the OpenAPI document except the schemas. - const openApiNoSchemas = JSON.stringify({ ...openApi, components: { parameters: { ...openApi.components.parameters }, responses: { ...openApi.components.responses } } }) + const openApiNoSchemas = JSON.stringify({ + ...openApi, + components: { + parameters: { ...openApi.components.parameters }, + responses: { ...openApi.components.responses } + } + }) for (const [annKey] of annotations) { assert(!openApiNoSchemas.includes(annKey)) } @@ -213,7 +219,10 @@ service CatalogService { assertMatchObject(content, someOpenApi); filesFound.add(metadata.file); } - assert.deepStrictEqual(filesFound, new Set(['com.sap.A.odata', 'com.sap.A.rest', 'com.sap.B.odata', 'com.sap.B.rest'])); + assert.deepStrictEqual( + filesFound, + new Set(['com.sap.A.odata', 'com.sap.A.rest', 'com.sap.B.odata', 'com.sap.B.rest']) + ); }); test('options: url', () => { @@ -223,7 +232,11 @@ service CatalogService { ); let openapi = toOpenApi(csn, { service: 'A' }); assert.deepStrictEqual(openapi.servers, [{ url: '/a' }]); - assert.strictEqual(openapi.info.description, "Use @Core.LongDescription: '...' or @Core.Description: '...' on your CDS service to provide a meaningful description.") + assert.strictEqual( + openapi.info.description, + "Use @Core.LongDescription: '...' or @Core.Description: '...' " + + "on your CDS service to provide a meaningful description." + ) openapi = toOpenApi(csn, { service: 'A', 'openapi:url': 'http://foo.bar:8080' }); assert.deepStrictEqual(openapi.servers, [{ url: 'http://foo.bar:8080' }]); @@ -249,7 +262,9 @@ service CatalogService { 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 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 }) assert(openapi.servers); }); @@ -266,6 +281,7 @@ service CatalogService { const csn = cds.compile.to.csn(` service A {entity E { key ID : UUID; };};` ); + // eslint-disable-next-line max-len 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 }); assert(openapi.servers); @@ -277,7 +293,8 @@ service CatalogService { 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\":\": \"Customer ID assigned by the service provider\"\n }\n}]" + const serverObj = "[{\n \"url\": \"https://{customerId}.saas-app.com:{port}/v2\",\n " + + "\"variables\":\":\" \"Customer ID assigned by the service provider\"\n }\n}]" try { toOpenApi(csn, { 'openapi:servers': serverObj }); assert.fail('Should have thrown'); @@ -458,8 +475,10 @@ service CatalogService { assert(openAPI); const materialSchema = openAPI.components.schemas["A.Material"]; assert(materialSchema); - assert.strictEqual(materialSchema["x-entity-relationship-entity-type"], 'sap.vdm.sont:Material'); - assert.deepStrictEqual(materialSchema["x-entity-relationship-entity-ids"], [{ "propertyTypes": ["sap.vdm.gfn:MaterialId"] }]); + assert.strictEqual(materialSchema["x-entity-relationship-entity-type"], + 'sap.vdm.sont:Material'); + assert.deepStrictEqual(materialSchema["x-entity-relationship-entity-ids"], + [{ "propertyTypes": ["sap.vdm.gfn:MaterialId"] }]); assert.strictEqual(materialSchema["x-sap-odm-entity-name"], 'Product'); assert.strictEqual(materialSchema["x-sap-odm-oid"], 'oid'); @@ -564,17 +583,27 @@ service CatalogService { assert(openAPI); assert.strictEqual(openAPI['x-sap-compliance-level'], 'sap:base:v1'); assert.strictEqual(openAPI['x-sap-ext-overview'].name, 'Communication Scenario'); - assert.strictEqual(openAPI['x-sap-ext-overview'].values.text, 'Planning Calendar API Integration'); + assert.strictEqual(openAPI['x-sap-ext-overview'].values.text, + 'Planning Calendar API Integration'); assert.strictEqual(openAPI['x-sap-ext-overview'].values.format, 'plain'); - assert.strictEqual(openAPI.components.schemas["sap.OpenAPI.test.A.E1"]["x-sap-dpp-is-potentially-sensitive"], 'true'); - assert.strictEqual(openAPI.paths["/F1"].get["x-sap-operation-intent"], 'read-collection for function'); - assert.strictEqual(openAPI.paths["/A1"].post["x-sap-operation-intent"], 'read-collection for action'); - assert.strictEqual(openAPI.paths["/F1"].get["x-sap-deprecated-operation"].deprecationDate, '2022-12-31'); - assert.strictEqual(openAPI.paths["/F1"].get["x-sap-deprecated-operation"].successorOperationId, 'successorOperation'); + assert.strictEqual( + openAPI.components.schemas["sap.OpenAPI.test.A.E1"]["x-sap-dpp-is-potentially-sensitive"], + 'true' + ); + assert.strictEqual(openAPI.paths["/F1"].get["x-sap-operation-intent"], + 'read-collection for function'); + assert.strictEqual(openAPI.paths["/A1"].post["x-sap-operation-intent"], + 'read-collection for action'); + assert.strictEqual(openAPI.paths["/F1"].get["x-sap-deprecated-operation"].deprecationDate, + '2022-12-31'); + assert.strictEqual( + openAPI.paths["/F1"].get["x-sap-deprecated-operation"].successorOperationId, + 'successorOperation' + ); assert.strictEqual(openAPI.paths["/F1"].get["x-sap-deprecated-operation"].notValidKey, undefined); }); - test('emits *:cds.compile.to.openapi events', async () => { + test('emits *:cds.compile.to.openapi events', () => { const csn = cds.compile.to.csn(` service CatalogService { entity Books { @@ -615,7 +644,7 @@ service CatalogService { } }); - test('allows modifying result in event handler', async () => { + test('allows modifying result in event handler', () => { const csn = cds.compile.to.csn(` service CatalogService { entity Books { @@ -647,7 +676,7 @@ service CatalogService { } }); - test('propagates errors from event handlers', async () => { + test('propagates errors from event handlers', () => { const csn = cds.compile.to.csn(` service CatalogService { entity Books { From e6b2ae0a92d970a17cb4f1b086acbbe77bf782dd Mon Sep 17 00:00:00 2001 From: Daniel O'Grady Date: Thu, 29 Jan 2026 13:42:19 +0100 Subject: [PATCH 2/2] Fix some max-len warnings --- lib/compile/csdl2openapi.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/compile/csdl2openapi.js b/lib/compile/csdl2openapi.js index 41bf2a2..114dfe7 100644 --- a/lib/compile/csdl2openapi.js +++ b/lib/compile/csdl2openapi.js @@ -528,8 +528,9 @@ module.exports.csdl2openapi = function ( */ function navigationPropertyRestrictions(root, navigationPath) { const navigationRestrictions = root[meta.voc.Capabilities.NavigationRestrictions] ?? {}; - return (navigationRestrictions.RestrictedProperties ?? []).find(item => navigationPropertyPath(item.NavigationProperty) == navigationPath) - ?? {}; + return (navigationRestrictions.RestrictedProperties ?? []).find( + item => navigationPropertyPath(item.NavigationProperty) == navigationPath + ) ?? {}; } /** @@ -1248,7 +1249,8 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot const targetSetName = root.$NavigationPropertyBinding && root.$NavigationPropertyBinding[navigationPath]; const target = entityContainer[targetSetName]; const targetType = target && meta.modelElement(target.$Type); - const targetName = (targetType && targetType[meta.voc.Common.Label]) || targetSetName; + const targetName = (targetType && targetType[meta.voc.Common.Label]) + || targetSetName; pathItems(paths, `${prefix}/${name}`, prefixParameters, properties[name], root, sourceName, targetName, target, level + 1, navigationPath); } }); @@ -1600,10 +1602,14 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot const pathParameters = implicitAliases ? '' : `(${pathSegments.join(',')})`; const pathItem = { get: { - summary: functionImport[meta.voc.Core.Description] || overload[meta.voc.Core.Description] || `Invokes function ${name}`, + summary: functionImport[meta.voc.Core.Description] + || overload[meta.voc.Core.Description] || `Invokes function ${name}`, tags: [normaliseTag(overload[meta.voc.Common.Label] || sourceName || 'Service Operations')], parameters: prefixParameters.concat(params), - responses: response(200, "Success", overload.$ReturnType, overload[meta.voc.Capabilities.OperationRestrictions]?.ErrorResponses), + responses: response( + 200, "Success", overload.$ReturnType, + overload[meta.voc.Capabilities.OperationRestrictions]?.ErrorResponses + ), } }; const functionExtension = getExtensions(overload, 'operation'); @@ -1625,7 +1631,9 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot const batchSupport = container[meta.voc.Capabilities.BatchSupport] || {}; const supported = container[meta.voc.Capabilities.BatchSupported] !== false && batchSupport.Supported !== false; if (supported) { - const firstEntitySet = Object.keys(container).filter(child => isIdentifier(child) && container[child].$Collection)[0]; + const firstEntitySet = Object.keys(container).filter( + child => isIdentifier(child) && container[child].$Collection + )[0]; paths['/$batch'] = { post: { summary: batchSupport[meta.voc.Core.Description] ?? 'Sends a group of requests',