UNPKG

niwe-odata-openapi

Version:
1,583 lines (1,469 loc) 96.8 kB
/** * Converts OData CSDL JSON to OpenAPI 3.0.2 * * Latest version: https://github.com/oasis-tcs/odata-openapi/blob/main/lib/csdl2openapi.js */ const { EDM, nameParts } = require("./edm"); const { resourceDiagram } = require("./diagram"); const pluralize = require("pluralize"); const a = require("indefinite"); //TODO // - Core.Example for complex types // - reduce number of loops over schemas // - inject $$name into each model element to make parameter passing easier? // - allow passing additional files for referenced documents // - delta: headers Prefer and Preference-Applied // - inline definitions for Edm.* to make OpenAPI documents self-contained // - $Extends for entity container: include /paths from referenced container // - both "clickable" and freestyle $expand, $select, $orderby - does not work yet, open issue for Swagger UI // - 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 // - InsertRestrictions/NonInsertableProperties // - InsertRestrictions/NonInsertableNavigationProperties // - UpdateRestrictions/NonUpdatableProperties // - UpdateRestrictions/NonUpdatableNavigationProperties // - see //TODO comments below const SUFFIX = { read: "", create: "-create", update: "-update", }; const TITLE_SUFFIX = { "": "", "-create": " (for create)", "-update": " (for update)", }; const SYSTEM_QUERY_OPTIONS = [ "compute", "expand", "select", "filter", "search", "count", "orderby", "skip", "top", "format", "index", "schemaversion", "skiptoken", "apply", ]; /** * Construct an OpenAPI description from a CSDL document * @param {object} csdl CSDL document * @param {object} options Optional parameters * @return {object} OpenAPI description */ module.exports.csdl2openapi = function ( csdl, { scheme = "https", host = "localhost", basePath = "/service-root", diagram = false, openapiVersion = "3.0.2", maxLevels = 4, messages = [], skipBatchPath = false, defaultTitle = null, defaultDescription = null, rootResourcesToKeep = null, } = {}, ) { const serviceRoot = scheme + "://" + host + basePath; const queryOptionPrefix = csdl.$Version <= "4.0" ? "$" : ""; const typesToInline = {}; // filled in schema() and used in inlineTypes() const oas31 = openapiVersion >= "3.1.0"; const requiredSchemas = { list: [], used: {}, entityReferenceNeeded: false, }; const model = new EDM(); model.addDocument(csdl, messages); const entityTypesToKeep = rootResourcesToKeep ? model.referencedEntityTypes(rootResourcesToKeep) : null; const voc = model.voc; const entityContainer = model.entityContainer ?? {}; const keyAsSegment = entityContainer[voc.Capabilities.KeyAsSegmentSupported]; const applySupported = entityContainer[voc.Aggregation.ApplySupported]; const deepUpdate = entityContainer[voc.Capabilities.DeepUpdateSupport] && entityContainer[voc.Capabilities.DeepUpdateSupport].Supported; const openapi = { openapi: openapiVersion, info: info(csdl, entityContainer), servers: servers(serviceRoot), tags: tags(), paths: paths(model.entityContainer), components: components(!!model.entityContainer), }; if (!model.entityContainer) { delete openapi.servers; delete openapi.tags; } securitySchemes(openapi.components, entityContainer); security(openapi, model.entityContainer); return openapi; /** * Construct the Info Object * @param {object} csdl CSDL document * @param {object} entityContainer Entity Container object * @return {object} Info Object */ function info(csdl, entityContainer) { const containerNamespace = csdl.$EntityContainer && nameParts(csdl.$EntityContainer).qualifier; const containerSchema = csdl[containerNamespace] ?? {}; const description = (entityContainer[voc.Core.LongDescription] || containerSchema[voc.Core.LongDescription] || defaultDescription || "This service is located at [" + serviceRoot + "/](" + serviceRoot.replace(/\(/g, "%28").replace(/\)/g, "%29") + "/)") + (diagram ? resourceDiagram(model, rootResourcesToKeep, entityTypesToKeep) : ""); return { title: entityContainer[voc.Core.Description] || containerSchema[voc.Core.Description] || defaultTitle || (csdl.$EntityContainer ? "Service for namespace " + containerNamespace : "OData CSDL document"), description: csdl.$EntityContainer ? description : "", version: containerSchema[voc.Core.SchemaVersion] ?? "", }; } /** * Construct an array of Server Objects * @param {string} serviceRoot Service root URL * @return {Array} The list of servers */ function servers(serviceRoot) { return [{ url: serviceRoot }]; } /** * Construct an array of Tag Objects from the entity container * @param {object} container The entity container * @return {Array} The list of tags */ function tags() { const tags = []; for (const [name, resource] of model.resources) { // all entity sets and singletons if (!resource.$Type) continue; const tag = { name }; const type = model.element(resource.$Type); const description = resource[voc.Core.Description] || (type && type[voc.Core.Description]); if (description) tag.description = description; tags.push(tag); } return tags; } /** * Construct the Paths Object from the entity container * @param {object} container Entity container * @return {object} Paths Object */ function paths(container) { const paths = {}; for (const [name, child] of model.resources) { if (rootResourcesToKeep && !rootResourcesToKeep.includes(name)) continue; if (child.$Type) { pathItems( paths, "/" + name, [], // entity sets and singletons are almost containment navigation properties { ...child, $ContainsTarget: true }, child, name, name, child, 0, "", ); } else if (child.$Action) { pathItemActionImport(paths, name, child); } else if (child.$Function) { pathItemFunctionImport(paths, name, child); } else { messages.push("Unrecognized entity container child: " + name); } } if (model.resources.length > 0) pathItemBatch(paths, container); return paths; } /** * Add path and Path Item Object for a navigation segment * @param {object} paths Paths Object to augment * @param {string} prefix Prefix for path * @param {Array} prefixParameters Parameter Objects for prefix * @param {object} element Model element of navigation segment * @param {object} root Root model element * @param {string} sourceName Name of path source * @param {string} targetName Name of path target * @param {string} target Target container child of path * @param {integer} 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, ) { const name = prefix.substring(prefix.lastIndexOf("/") + 1); const type = model.element(element.$Type); const pathItem = {}; const restrictions = navigationPropertyRestrictions(root, navigationPath); const nonExpandable = nonExpandableProperties(root, navigationPath); paths[prefix] = pathItem; if (prefixParameters.length > 0) pathItem.parameters = prefixParameters; operationRead( pathItem, element, name, sourceName, targetName, target, level, restrictions, false, nonExpandable, ); if ( element.$Collection && (element.$ContainsTarget || (level < 2 && target)) ) { operationCreate( pathItem, element, name, sourceName, targetName, target, level, restrictions, ); } if (element.$ContainsTarget) { pathItemsForBoundOperations( paths, prefix, prefixParameters, element, sourceName, ); if (element.$Collection) { if (level <= maxLevels) pathItemsWithKey( paths, prefix, prefixParameters, element, root, sourceName, targetName, target, level, navigationPath, restrictions, nonExpandable, ); } else { operationUpdate( pathItem, element, name, sourceName, target, level, restrictions, ); if (element.$Nullable) { operationDelete( pathItem, element, name, sourceName, target, level, restrictions, ); } pathItemsForBoundOperations( paths, prefix, prefixParameters, element, sourceName, ); pathItemsWithNavigation( paths, prefix, prefixParameters, type, root, sourceName, level, navigationPath, ); } } if (Object.keys(pathItem).filter((i) => i !== "parameters").length === 0) delete paths[prefix]; if (element.$Collection) { pathItemForFilterSegment( paths, prefix, prefixParameters, element, sourceName, target, level, restrictions, ); } } /** * Find navigation restrictions for a navigation path * @param {object} root Root model element * @param {string} navigationPath Path for finding navigation restrictions * @return Navigation property restrictions of navigation segment */ function navigationPropertyRestrictions(root, navigationPath) { const navigationRestrictions = root[voc.Capabilities.NavigationRestrictions] || {}; return ( (navigationRestrictions.RestrictedProperties || []).find( (item) => item.NavigationProperty == navigationPath, ) || {} ); } /** * Find non-expandable properties for a navigation path * @param {object} root Root model element * @param {string} navigationPath Path for finding navigation restrictions * @return Navigation property restrictions of navigation segment */ function nonExpandableProperties(root, navigationPath) { const expandRestrictions = root[voc.Capabilities.ExpandRestrictions] || {}; const prefix = navigationPath.length === 0 ? "" : navigationPath + "/"; const from = prefix.length; const nonExpandable = []; for (const path of expandRestrictions.NonExpandableProperties || []) { if (path.startsWith(prefix)) { nonExpandable.push(path.substring(from)); } } return nonExpandable; } /** * Add path and Path Item Object for a navigation segment with key * @param {object} paths Paths Object to augment * @param {string} prefix Prefix for path * @param {Array} prefixParameters Parameter Objects for prefix * @param {object} element Model element of navigation segment * @param {object} root Root model element * @param {string} sourceName Name of path source * @param {string} targetName Name of path target * @param {string} target Target container child of path * @param {integer} level Number of navigation segments so far * @param {string} navigationPath Path for finding navigation restrictions * @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, ) { const targetIndexable = target == null || target[voc.Capabilities.IndexableByKey] != false; if ( restrictions.IndexableByKey == true || (restrictions.IndexableByKey != false && targetIndexable) ) { const name = prefix.substring(prefix.lastIndexOf("/") + 1); const type = model.element(element.$Type); const key = entityKey(type, level); if (key.parameters.length > 0) { const path = prefix + key.segment; const parameters = prefixParameters.concat(key.parameters); const pathItem = { parameters: parameters }; paths[path] = pathItem; operationRead( pathItem, element, name, sourceName, targetName, target, level, restrictions, true, nonExpandable, ); operationUpdate( pathItem, element, name, sourceName, target, level, restrictions, ); operationDelete( pathItem, element, name, sourceName, target, level, restrictions, ); if ( Object.keys(pathItem).filter((i) => i !== "parameters").length === 0 ) delete paths[path]; pathItemForMediaResource( paths, path, parameters, type, name, sourceName, ); pathItemsForBoundOperations( paths, path, parameters, element, sourceName, true, ); pathItemsWithNavigation( paths, path, parameters, type, root, sourceName, level, navigationPath, ); } } } /** * Construct Operation Object for create * @param {object} pathItem Path Item Object to augment * @param {object} element Model element of navigation segment * @param {string} name Name of navigation segment * @param {string} sourceName Name of path source * @param {string} targetName Name of path target * @param {string} target Target container child of path * @param {integer} 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 && target[voc.Capabilities.InsertRestrictions]) || {}; let label = element[voc.Common.Label] || name; const headInfo = element[voc.UI.HeaderInfo]; label = headInfo?.TypeName || label; let summary = insertRestrictions.Description || (level > 0 ? `Create related ${label}` : `Create ${label}`); if (insertRestrictions.Insertable !== false) { const type = model.element(element.$Type); const hasStream = type && type.$HasStream; pathItem.post = { summary: summary, ...(insertRestrictions.LongDescription && { description: insertRestrictions.LongDescription, }), tags: [sourceName], requestBody: { description: (type && type[voc.Core.Description]) || (hasStream ? "New media resource" : "New entity"), required: true, content: hasStream ? { "*/*": { schema: { type: "string", format: "binary" } } } : { "application/json": { schema: ref(element.$Type, SUFFIX.create), }, }, }, responses: response( 201, "Created entity", { $Type: element.$Type }, insertRestrictions.ErrorResponses, ), }; if (targetName && sourceName != targetName) pathItem.post.tags.push(targetName); customParameters(pathItem.post, insertRestrictions); } } /** * Construct Operation Object for read * @param {object} pathItem Path Item Object to augment * @param {object} element Model element of navigation segment * @param {string} name Name of navigation segment * @param {string} sourceName Name of path source * @param {string} targetName Name of path target * @param {string} target Target container child of path * @param {integer} level Number of navigation segments so far * @param {object} restrictions Navigation property restrictions of navigation segment * @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, ) { const targetRestrictions = target?.[voc.Capabilities.ReadRestrictions]; const readRestrictions = restrictions.ReadRestrictions || targetRestrictions || {}; const readByKeyRestrictions = readRestrictions.ReadByKeyRestrictions; let readable = true; if ( byKey && readByKeyRestrictions && readByKeyRestrictions.Readable !== undefined ) readable = readByKeyRestrictions.Readable; else if (readRestrictions.Readable !== undefined) readable = readRestrictions.Readable; if (readable) { let descriptions = (level == 0 ? targetRestrictions : restrictions.ReadRestrictions) || {}; if (byKey) descriptions = descriptions.ReadByKeyRestrictions || {}; let label = element[voc.Common.Label] || name; const headInfo = element[voc.UI.HeaderInfo]; label = byKey ? headInfo?.TypeName || label : headInfo?.TypeNamePlural || pluralize(label); let summary = descriptions.Description || (level > 0 ? `Get ${label}` : `Get ${byKey ? a(label) : label}`); const collection = !byKey && element.$Collection; const operation = { summary: summary, ...(descriptions.LongDescription && { description: descriptions.LongDescription, }), tags: [sourceName], parameters: [], responses: response( 200, "Retrieved entit" + (collection ? "ies" : "y"), { $Type: element.$Type, $Collection: collection }, byKey ? readByKeyRestrictions?.ErrorResponses : readRestrictions?.ErrorResponses, ), }; const deltaSupported = element[voc.Capabilities.ChangeTracking] && element[voc.Capabilities.ChangeTracking].Supported; if (collection && deltaSupported) { const deltaLinkSchema = { type: "string", example: basePath + "/" + name + "?$deltatoken=opaque server-generated token for fetching the delta", }; if (oas31) { deltaLinkSchema.examples = [deltaLinkSchema.example]; delete deltaLinkSchema.example; } operation.responses[200].content["application/json"].schema.properties[ "@odata.deltaLink" ] = deltaLinkSchema; } if (target && sourceName != targetName) operation.tags.push(targetName); customParameters( operation, byKey ? readByKeyRestrictions || readRestrictions : readRestrictions, ); if (collection) { optionTop(operation.parameters, target, restrictions); optionSkip(operation.parameters, target, restrictions); if (csdl.$Version >= "4.0") optionSearch(operation.parameters, target, restrictions); optionFilter(operation.parameters, target, restrictions); optionCount(operation.parameters, target); optionOrderBy(operation.parameters, element, target, restrictions); } optionSelect(operation.parameters, element, target, restrictions); optionExpand(operation.parameters, element, target, nonExpandable); // if (collection) { // optionApply(operation.parameters); // } pathItem.get = operation; } } /** * Add custom headers and query options * @param {object} operation Operation object to augment * @param {object} restrictions Restrictions for operation */ function customParameters(operation, restrictions) { if ( !operation.parameters && (restrictions.CustomHeaders || restrictions.CustomQueryOptions) ) operation.parameters = []; for (const custom of restrictions.CustomHeaders || []) { operation.parameters.push(customParameter(custom, "header")); } for (const custom of restrictions.CustomQueryOptions || []) { operation.parameters.push(customParameter(custom, "query")); } } /** * Construct custom parameter * @param {object} custom custom parameter in OData format * @param {string} location "header" or "query" */ function customParameter(custom, location) { let schema = jsonSchema(custom); if (!schema) schema = { type: "string" }; if (custom.DocumentationURL) schema.externalDocs = { url: custom.DocumentationURL }; //TODO: Examples in schema return { name: custom.Name, in: location, required: custom.Required || false, ...(custom.Description && { description: custom.Description }), schema, }; } /** * Add parameter for query option $apply * @param {Array} parameters Array of parameters to augment * @param {string} target Target container child of path * @param {object} restrictions Navigation property restrictions of navigation segment */ function optionApply(parameters) { if (applySupported) { parameters.push({ $ref: "#/components/parameters/apply", }); } } /** * Add parameter for query option $count * @param {Array} parameters Array of parameters to augment * @param {string} target Target container child of path */ function optionCount(parameters, target) { const targetRestrictions = target && target[voc.Capabilities.CountRestrictions]; const targetCountable = target == null || targetRestrictions == null || targetRestrictions.Countable !== false; if (targetCountable) { parameters.push({ $ref: "#/components/parameters/count", }); } } /** * Add parameter for query option $expand * @param {Array} parameters Array of parameters to augment * @param {object} element Model element of navigation segment * @param {string} target Target container child of path * @param {array} nonExpandable Non-expandable navigation properties */ function optionExpand(parameters, element, target, nonExpandable) { const targetRestrictions = target && target[voc.Capabilities.ExpandRestrictions]; const supported = targetRestrictions == null || targetRestrictions.Expandable != false; if (supported) { const expandItems = ["*"].concat( navigationPaths(element).filter( (path) => !nonExpandable.includes(path), ), ); if (expandItems.length > 1) { if (csdl.$Version === "2.0") expandItems.shift(1); parameters.push({ name: queryOptionPrefix + "expand", description: (targetRestrictions && targetRestrictions[voc.Core.Description]) || `Expand related entities, see ${ csdl.$Version === "2.0" ? "[URI Conventions (OData Version 2.0)](https://www.odata.org/documentation/odata-version-2-0/uri-conventions/)" : "[Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionexpand)" }`, in: "query", explode: false, schema: { type: "array", uniqueItems: true, items: { type: "string", enum: expandItems, }, }, example: "*", }); } } } /** * Collect navigation paths of a navigation segment and its potentially structured components * @param {object} element Model element of navigation segment * @param {string} prefix Navigation prefix * @param {integer} level Number of navigation segments so far * @return {Array} Array of navigation property paths */ function navigationPaths(element, prefix = "", level = 0) { const paths = []; const type = model.element(element.$Type); for (const [key, property] of model.propertiesOfStructuredType(type)) { if (property.$Kind == "NavigationProperty") { paths.push(prefix + key); } else if (property.$Type && level < maxLevels) { paths.push(...navigationPaths(property, prefix + key + "/", level + 1)); } } return paths; } /** * Add parameter for query option $filter * @param {Array} parameters Array of parameters to augment * @param {string} target Target container child of path * @param {object} restrictions Navigation property restrictions of navigation segment */ function optionFilter(parameters, target, restrictions) { const filterRestrictions = restrictions.FilterRestrictions || (target && target[voc.Capabilities.FilterRestrictions]) || {}; if (filterRestrictions.Filterable !== false) { let example = ""; if (target?.$Type) { const entityElement = model.element(target?.$Type); example = entityElement?.["$Key"]?.length > 0 ? `${entityElement?.["$Key"][0]} ne null` : ""; } const filter = { name: queryOptionPrefix + "filter", description: filterRestrictions[voc.Core.Description] || `Filter items by property values, see ${ csdl.$Version === "2.0" ? "[URI Conventions (OData Version 2.0)](https://www.odata.org/documentation/odata-version-2-0/uri-conventions/)" : "[Filtering](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionfilter)" }`, in: "query", schema: { type: "string", }, example, }; if (filterRestrictions.RequiresFilter) filter.required = true; if (filterRestrictions.RequiredProperties) { filter.description += "\n\nRequired filter properties:"; for (const item of filterRestrictions.RequiredProperties) { filter.description += "\n- " + item; } } parameters.push(filter); } } /** * Add parameter for query option $orderby * @param {Array} parameters Array of parameters to augment * @param {object} element Model element of navigation segment * @param {string} target Target container child of path * @param {object} restrictions Navigation property restrictions of navigation segment */ function optionOrderBy(parameters, element, target, restrictions) { const sortRestrictions = restrictions.SortRestrictions || (target && target[voc.Capabilities.SortRestrictions]) || {}; if (sortRestrictions.Sortable !== false) { const nonSortable = {}; for (const name of sortRestrictions.NonSortableProperties || []) { nonSortable[name] = true; } const orderbyItems = []; for (const property of primitivePaths(element)) { if (nonSortable[property]) continue; orderbyItems.push(property); orderbyItems.push(property + " desc"); } let example = ""; if (target?.$Type) { const entityElement = model.element(target?.$Type); example = entityElement?.["$Key"] ? entityElement?.["$Key"].toString() : ""; } if (orderbyItems.length > 0) { parameters.push({ name: queryOptionPrefix + "orderby", description: sortRestrictions[voc.Core.Description] || `Order items by property values, see ${ csdl.$Version === "2.0" ? "[URI Conventions (OData Version 2.0)](https://www.odata.org/documentation/odata-version-2-0/uri-conventions/)" : "[Sorting](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionorderby)" }`, in: "query", explode: false, schema: { type: "array", uniqueItems: true, items: { type: "string", enum: orderbyItems, }, }, example, }); } } } /** * Collect primitive paths of a navigation segment and its potentially structured components * @param {object} element Model element of navigation segment * @param {string} prefix Navigation prefix * @return {Array} Array of primitive property paths */ function primitivePaths(element, prefix = "") { const paths = []; const elementType = model.element(element.$Type); if (!elementType) { messages.push(`Unknown type for element: ${JSON.stringify(element)}`); return paths; } const propsOfType = model.propertiesOfStructuredType(elementType); const ignore = propsOfType.filter( ([, type]) => type.$Kind !== "NavigationProperty" && type.$Type && !type.$Type.startsWith("Edm.") && !model.element(type.$Type), ); // Keep old logging for (const entry of ignore) { messages.push(`Unknown type for element: ${JSON.stringify(entry)}`); } const properties = propsOfType .filter((entry) => entry[1].$Kind !== "NavigationProperty") .filter((entry) => !ignore.includes(entry)) .map(entryToProperty({ path: prefix, typeRefChain: [] })); for (let i = 0; i < properties.length; i++) { const property = properties[i]; if (!property.isComplex) { paths.push(property.path); continue; } const typeRefChainTail = property.typeRefChain[property.typeRefChain.length - 1]; // Allow full cycle to be shown (0) times if ( property.typeRefChain.filter((_type) => _type === typeRefChainTail) .length > 1 ) { // messages.push(`Cycle detected ${property.typeRefChain.join("->")}`); continue; } const expanded = Object.entries(property.properties) .filter((property) => property[1].$Kind !== "NavigationProperty") .map(entryToProperty(property)); properties.splice(i + 1, 0, ...expanded); } return paths; } function entryToProperty(parent) { return function (entry) { const key = entry[0]; const property = entry[1]; const propertyType = property.$Type && model.element(property.$Type); if ( propertyType && propertyType.$Kind && propertyType.$Kind === "ComplexType" ) { return { properties: model.propertiesMapOfStructuredType(propertyType), path: `${parent.path}${key}/`, typeRefChain: parent.typeRefChain.concat(property.$Type), isComplex: true, }; } return { properties: {}, path: `${parent.path}${key}`, typeRefChain: [], isComplex: false, }; }; } /** * Add parameter for query option $search * @param {Array} parameters Array of parameters to augment * @param {string} target Target container child of path * @param {object} restrictions Navigation property restrictions of navigation segment */ function optionSearch(parameters, target, restrictions) { const searchRestrictions = restrictions.SearchRestrictions || (target && target[voc.Capabilities.SearchRestrictions]) || {}; if (searchRestrictions.Searchable !== false) { if (searchRestrictions[voc.Core.Description]) { parameters.push({ name: queryOptionPrefix + "search", description: searchRestrictions[voc.Core.Description], in: "query", schema: { type: "string" }, }); } else { parameters.push({ $ref: "#/components/parameters/search" }); } } } /** * Add parameter for query option $select * @param {Array} parameters Array of parameters to augment * @param {object} element Model element of navigation segment * @param {string} target Target container child of path * @param {object} restrictions Navigation property restrictions of navigation segment */ function optionSelect(parameters, element, target, restrictions) { const selectSupport = restrictions.SelectSupport || (target && target[voc.Capabilities.SelectSupport]) || {}; if (selectSupport.Supported !== false) { const type = model.element(element.$Type) || {}; const selectItems = []; for (const [name, property] of model.propertiesOfStructuredType(type)) { if (property.$Kind === "NavigationProperty" && csdl.$Version !== "2.0") continue; selectItems.push(name); } if (selectItems.length > 0) { parameters.push({ name: queryOptionPrefix + "select", description: `Select properties to be returned, see ${ csdl.$Version === "2.0" ? "[URI Conventions (OData Version 2.0)](https://www.odata.org/documentation/odata-version-2-0/uri-conventions/)" : "[Select](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionselect)" }`, in: "query", explode: false, schema: { type: "array", uniqueItems: true, items: { type: "string", enum: selectItems, }, }, example: "*", }); } } } /** * Add parameter for query option $skip * @param {Array} parameters Array of parameters to augment * @param {string} target Target container child of path * @param {object} restrictions Navigation property restrictions of navigation segment */ function optionSkip(parameters, target, restrictions) { const supported = Object.keys(restrictions).length > 0 ? restrictions.SkipSupported !== false : target == null || target[voc.Capabilities.SkipSupported] !== false; if (supported) { parameters.push({ $ref: "#/components/parameters/skip", }); } } /** * Add parameter for query option $top * @param {Array} parameters Array of parameters to augment * @param {string} target Target container child of path * @param {object} restrictions Navigation property restrictions of navigation segment */ function optionTop(parameters, target, restrictions) { const supported = Object.keys(restrictions).length > 0 ? restrictions.TopSupported !== false : target == null || target[voc.Capabilities.TopSupported] !== false; if (supported) { parameters.push({ $ref: "#/components/parameters/top", }); } } /** * Construct Operation Object for update * @param {object} pathItem Path Item Object to augment * @param {object} element Model element of navigation segment * @param {string} name Name of navigation segment * @param {string} sourceName Name of path source * @param {string} target Target container child of path * @param {integer} level Number of navigation segments so far * @param {object} restrictions Navigation property restrictions of navigation segment */ function operationUpdate( pathItem, element, name, sourceName, target, level, restrictions, ) { const updateRestrictions = restrictions.UpdateRestrictions || (target && target[voc.Capabilities.UpdateRestrictions]) || {}; let label = element[voc.Common.Label] || name; const headInfo = element[voc.UI.HeaderInfo]; label = headInfo?.TypeName || label; let summary = updateRestrictions.Description || (level > 0 ? `Update related ${label}` : `Update ${label}`); if (updateRestrictions.Updatable !== false) { const type = model.element(element.$Type); const operation = { summary: summary, ...(updateRestrictions.LongDescription && { description: updateRestrictions.LongDescription, }), tags: [sourceName], requestBody: { description: (type && type[voc.Core.Description]) || "New property values", required: true, content: { "application/json": { schema: csdl.$Version === "2.0" ? { type: "object", title: "Modified " + nameParts(element.$Type).name, properties: { d: ref(element.$Type, SUFFIX.update) }, } : ref(element.$Type, SUFFIX.update), }, }, }, responses: response( 204, "Success", undefined, updateRestrictions.ErrorResponses, ), }; customParameters(operation, updateRestrictions); pathItem[updateRestrictions.UpdateMethod?.toLowerCase() || "patch"] = operation; } } /** * Construct Operation Object for delete * @param {object} pathItem Path Item Object to augment * @param {object} element Model element of navigation segment * @param {string} name Name of navigation segment * @param {string} sourceName Name of path source * @param {string} target Target container child of path * @param {integer} level Number of navigation segments so far * @param {object} restrictions Navigation property restrictions of navigation segment */ function operationDelete( pathItem, element, name, sourceName, target, level, restrictions, ) { const deleteRestrictions = restrictions.DeleteRestrictions || (target && target[voc.Capabilities.DeleteRestrictions]) || {}; let label = element[voc.Common.Label] || name; const headInfo = element[voc.UI.HeaderInfo]; label = headInfo?.TypeName || label; let summary = deleteRestrictions.Description || (level > 0 ? `Delete related ${label}` : `Delete ${label}`); if (deleteRestrictions.Deletable !== false) { pathItem.delete = { summary: summary, ...(deleteRestrictions.LongDescription && { description: deleteRestrictions.LongDescription, }), tags: [sourceName], responses: response( 204, "Success", undefined, deleteRestrictions.ErrorResponses, ), }; customParameters(pathItem.delete, deleteRestrictions); } } /** * Add path and Path Item Object for media resource * @param {object} paths The Paths Object to augment * @param {string} prefix Prefix for path * @param {Array} prefixParameters Parameter Objects for prefix * @param {object} type Entity type object of navigation segment * @param {string} name Name of navigation segment * @param {string} sourceName Name of path source */ function pathItemForMediaResource( paths, prefix, prefixParameters, type, name, sourceName, ) { if (!type.$HasStream) return; const pathItem = { parameters: prefixParameters, get: { summary: `Get media resource from ${name} by key`, tags: [sourceName], responses: { 200: { description: "Retrieved media resource", content: { "*/*": { schema: { type: "string", format: "binary" } }, }, }, "4XX": { $ref: "#/components/responses/error", }, }, }, }; paths[`${prefix}/$value`] = pathItem; } /** * Add paths and Path Item Objects for navigation segments * @param {object} paths The Paths Object to augment * @param {string} prefix Prefix for path * @param {Array} prefixParameters Parameter Objects for prefix * @param {object} type Entity type object of navigation segment * @param {string} sourceName Name of path source * @param {integer} 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, ) { if ( type["@SAP__common.ResultContext"] !== true && (!type || level >= maxLevels) ) { return; } const parentRestrictions = navigationPropertyRestrictions( root, navigationPrefix, ); if (parentRestrictions.Navigability == "Single") return; const navigationRestrictions = root[voc.Capabilities.NavigationRestrictions] || {}; const rootNavigable = (level == 0 && navigationRestrictions.Navigability != "None") || (level == 1 && navigationRestrictions.Navigability != "Single") || level > 1; const properties = navigationPathMap(type); for (const [name, property] of Object.entries(properties)) { if ( !property.$ContainsTarget && entityTypesToKeep && !entityTypesToKeep.includes(property.$Type) ) continue; const navigationPath = navigationPrefix + (navigationPrefix.length > 0 ? "/" : "") + name; const restrictions = navigationPropertyRestrictions(root, navigationPath); if ( ["Recursive", "Single"].includes(restrictions.Navigability) || (restrictions.Navigability == null && rootNavigable) ) { const targetSetName = root.$NavigationPropertyBinding && root.$NavigationPropertyBinding[navigationPath]; const target = entityContainer[targetSetName]; pathItems( paths, prefix + "/" + name, prefixParameters, property, root, sourceName, targetSetName, target, level + 1, navigationPath, ); } } } /** * Collect navigation paths of a navigation segment and its potentially structured components * @param {object} type Structured type * @param {object} map Map of navigation property paths and their types * @param {string} prefix Navigation prefix * @param {integer} level Number of navigation segments so far * @return {object} Map of navigation property paths and their types */ function navigationPathMap(type, map = {}, prefix = "", level = 0) { for (const [name, property] of model.propertiesOfStructuredType(type)) { if (property.$Kind == "NavigationProperty") { map[prefix + name] = property; } else if (property.$Type && !property.$Collection && level < maxLevels) { navigationPathMap( model.element(property.$Type), map, prefix + name + "/", level + 1, ); } } return map; } /** * Construct map of key names for an entity type * @param {object} type Entity type object * @return {object} Map of key names */ function keyMap(type) { const map = {}; if (type.$Kind == "EntityType") { const keys = model.key(type); for (const key of keys) { if (typeof key == "string") map[key] = true; } } return map; } /** * Key for path item * @param {object} entityType Entity Type object * @param {integer} level Number of navigation segments so far * @return {object} key: Key segment, parameters: key parameters */ function entityKey(entityType, level) { let segment = ""; const params = []; const keys = model.key(entityType); const properties = model.propertiesMapOfStructuredType(entityType); keys.forEach((key, index) => { const suffix = level > 0 ? "_" + level : ""; if (keyAsSegment) segment += "/"; else { if (index > 0) segment += ","; if (keys.length != 1) segment += key + "="; } let parameter; let property = {}; if (typeof key == "string") { parameter = key; property = properties[key]; } else { parameter = Object.keys(key)[0]; const segments = key[parameter].split("/"); property = properties[segments[0]]; for (let i = 1; i < segments.length; i++) { const complexType = model.element(property.$Type); const properties = model.propertiesMapOfStructuredType(complexType); property = properties[segments[i]]; } } const propertyType = property.$Type; segment += pathValuePrefix(propertyType) + "{" + parameter + suffix + "}" + pathValueSuffix(propertyType); const param = { description: [property[voc.Core.Description], property[voc.Core.LongDescription]] .filter((t) => t) .join(" \n") || "key: " + parameter, in: "path", name: parameter + suffix, required: true, schema: schema(property, "", true), }; params.push(param); }); return { segment: (keyAsSegment ? "" : "(") + segment + (keyAsSegment ? "" : ")"), parameters: params, }; } /** * Prefix for key value in key segment * @param {typename} Qualified name of key property type * @return {string} value prefix */ function pathValuePrefix(typename) { //TODO: handle other Edm types, enumeration types, and type definitions if ( [ "Edm.Int64", "Edm.Int32", "Edm.Int16", "Edm.SByte", "Edm.Byte", "Edm.Decimal", "Edm.Double", "Edm.Single", "Edm.Boolean", "Edm.Date", "Edm.DateTimeOffset", "Edm.TimeOfDay", "Edm.Guid", ].includes(typename) ) return ""; if (keyAsSegment) return ""; return "'"; } /** * Suffix for key value in key segment * @param {typename} Qualified name of key property type * @return {string} value prefix */ 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.Decimal", "Edm.Double", "Edm.Single", "Edm.Boolean", "Edm.Date", "Edm.DateTimeOffset", "Edm.TimeOfDay", "Edm.Guid", ].includes(typename) ) return ""; if (keyAsSegment) return ""; return "'"; } /** * Add path and Path Item Object for actions and functions bound to the element * @param {object} paths Paths Object to augment * @param {string} prefix Prefix for path * @param {Array} prefixParameters Parameter Objects for prefix * @param {object} element Model element the operations are bound to * @param {string} sourceName Name of path source * @param {boolean} byKey read by key */ function pathItemsForBoundOperations( paths, prefix, prefixParameters, element, sourceName, byKey = false, ) { const overloads = //TODO: make method model.boundOverloads[ element.$Type + (!byKey && element.$Collection ? "-c" : "") ] || []; for (const item of overloads) { if (item.overload.$Kind == "Action") pathItemAction( paths, prefix + "/" + item.name, prefixParameters, item.name, item.overload,