diff --git a/.github/workflows/type-check.yml b/.github/workflows/type-check.yml new file mode 100644 index 0000000..9e3b21e --- /dev/null +++ b/.github/workflows/type-check.yml @@ -0,0 +1,23 @@ +name: Type Check + +on: + pull_request: + branches: + - main +jobs: + typecheck: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '24' + + - name: Install dependencies + run: npm i + + - name: Run type check + run: npm run check:types \ No newline at end of file diff --git a/lib/compile/csdl2openapi.js b/lib/compile/csdl2openapi.js index d09d545..50518f0 100644 --- a/lib/compile/csdl2openapi.js +++ b/lib/compile/csdl2openapi.js @@ -3,7 +3,7 @@ */ const cds = require('@sap/cds'); var pluralize = require('pluralize') -const DEBUG = cds.debug('openapi'); // Initialize cds.debug with the 'openapi' +const DEBUG = cds.debug('openapi'); // Initialize cds.debug with the 'openapi' //TODO @@ -104,7 +104,8 @@ module.exports.csdl2openapi = function ( const namespace = { 'Edm': 'Edm' }; const namespaceUrl = {}; const voc = {}; - const requiredSchemas = { list: [], used: {} }; + /** @type {{ list: { namespace:string, name: string, suffix: string }[], used: Record }} */ + const requiredSchemas = { list: [], used: {} }; preProcess(csdl, boundOverloads, derivedTypes, alias, namespace, namespaceUrl, voc); @@ -157,7 +158,7 @@ module.exports.csdl2openapi = function ( for (const [key, value] of Object.entries(containerSchema)) { if (key.startsWith('@OpenAPI.Extensions')) { - const annotationProperties = key.split('@OpenAPI.Extensions.')[1]; + const annotationProperties = key.split('@OpenAPI.Extensions.')[1] ?? '' const keys = annotationProperties.split('.'); if (!keys[0].startsWith("x-sap-")) { keys[0] = (keys[0].startsWith("sap-") ? "x-" : "x-sap-") + keys[0]; @@ -171,25 +172,25 @@ module.exports.csdl2openapi = function ( } let extensionEnums = { "x-sap-compliance-level": {allowedValues: ["sap:base:v1", "sap:core:v1", "sap:core:v2" ] } , - "x-sap-api-type": {allowedValues: [ "ODATA", "ODATAV4", "REST" , "SOAP"] }, + "x-sap-api-type": {allowedValues: [ "ODATA", "ODATAV4", "REST" , "SOAP"] }, "x-sap-direction": {allowedValues: ["inbound", "outbound", "mixed"] , default : "inbound" }, "x-sap-dpp-entity-semantics": {allowedValues: ["sap:DataSubject", "sap:DataSubjectDetails", "sap:Other"] }, "x-sap-dpp-field-semantics": {allowedValues: ["sap:DataSubjectID", "sap:ConsentID", "sap:PurposeID", "sap:ContractRelatedID", "sap:LegalEntityID", "sap:DataControllerID", "sap:UserID", "sap:EndOfBusinessDate", "sap:BlockingDate", "sap:EndOfRetentionDate"] }, }; - checkForExtentionEnums(extensionObj, extensionEnums); + checkForExtensionEnums(extensionObj, extensionEnums); - let extenstionSchema = { + let extensionSchema = { "x-sap-stateInfo": ['state', 'deprecationDate', 'decomissionedDate', 'link'], "x-sap-ext-overview": ['name', 'values'], "x-sap-deprecated-operation" : ['deprecationDate', 'successorOperationRef', "successorOperationId"], "x-sap-odm-semantic-key" : ['name', 'values'], }; - checkForExtentionSchema(extensionObj, extenstionSchema); + checkForExtentionSchema(extensionObj, extensionSchema); return extensionObj; } - function checkForExtentionEnums(extensionObj, extensionEnums){ + function checkForExtensionEnums(extensionObj, extensionEnums){ for (const [key, value] of Object.entries(extensionObj)) { if(extensionEnums[key] && extensionEnums[key].allowedValues && !extensionEnums[key].allowedValues.includes(value)){ if(extensionEnums[key].default){ @@ -202,14 +203,14 @@ module.exports.csdl2openapi = function ( } } - function checkForExtentionSchema(extensionObj, extenstionSchema) { + function checkForExtentionSchema(extensionObj, extensionSchema) { for (const [key, value] of Object.entries(extensionObj)) { - if (extenstionSchema[key]) { + if (extensionSchema[key]) { if (Array.isArray(value)) { - extensionObj[key] = value.filter((v) => extenstionSchema[key].includes(v)); + extensionObj[key] = value.filter((v) => extensionSchema[key].includes(v)); } else if (typeof value === "object" && value !== null) { for (const field in value) { - if (!extenstionSchema[key].includes(field)) { + if (!extensionSchema[key].includes(field)) { delete extensionObj[key][field]; } } @@ -223,9 +224,9 @@ module.exports.csdl2openapi = function ( if (resObj[openapiProperty] === undefined) { resObj[openapiProperty] = {}; } - + let node = resObj[openapiProperty]; - + // traverse the annotation property and define the objects if they're not defined for (let nestedIndex = 1; nestedIndex < keys.length - 1; nestedIndex++) { const nestedElement = keys[nestedIndex]; @@ -234,14 +235,15 @@ module.exports.csdl2openapi = function ( } node = node[nestedElement]; } - + // set value annotation property node[keys[keys.length - 1]] = value; } - + if (!csdl.$EntityContainer) { - delete openapi.servers; - delete openapi.tags; + // explicit cast required as .servers and .tags are not declared as optional + delete /**@type{any}*/(openapi).servers; + delete /**@type{any}*/(openapi).tags; } security(openapi, entityContainer); @@ -301,13 +303,14 @@ module.exports.csdl2openapi = function ( Object.keys(schema.$Annotations || {}).forEach(target => { const annotations = schema.$Annotations[target]; const segments = target.split('/'); - const open = segments[0].indexOf('('); + const firstSegment = segments[0]; + const open = firstSegment.indexOf('('); let element; if (open == -1) { - element = modelElement(segments[0]); + element = modelElement(firstSegment); } else { - element = modelElement(segments[0].substring(0, open)); - let args = segments[0].substring(open + 1, segments[0].length - 1); + element = modelElement(firstSegment.substring(0, open)); + let args = firstSegment.substring(open + 1, firstSegment.length - 1); element = element.find( (overload) => (overload.$Kind == "Action" && @@ -337,23 +340,21 @@ module.exports.csdl2openapi = function ( case 1: Object.assign(element, annotations); break; - case 2: + case 2: { + const secondSegment = /**@type{string}*/(segments[1]) if (['Action', 'Function'].includes(element.$Kind)) { - if (segments[1] == '$ReturnType') { + if (secondSegment === '$ReturnType') { if (element.$ReturnType) Object.assign(element.$ReturnType, annotations); } else { - const parameter = element.$Parameter.find(p => p.$Name == segments[1]); + const parameter = element.$Parameter.find(p => p.$Name == secondSegment); Object.assign(parameter, annotations); } - } else { - if (element[segments[1]]) { - Object.assign(element[segments[1]], annotations); - } else { - // DEBUG?.(`Invalid annotation target '${target}'`) - } + } else if (element[secondSegment]) { + Object.assign(element[secondSegment], annotations); } break; + } default: DEBUG?.('More than two annotation target path segments'); } @@ -673,8 +674,8 @@ module.exports.csdl2openapi = function ( * @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 {null | import('./types').TargetRestrictions[]} target Target container child of path + * @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) { @@ -753,8 +754,8 @@ module.exports.csdl2openapi = function ( * @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 {null | object} target Target container child of path + * @param {number} 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 @@ -792,13 +793,13 @@ module.exports.csdl2openapi = function ( * @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 {null | import('./types').TargetRestrictions[]} target Target container child of path + * @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 && target[voc.Capabilities.InsertRestrictions] || {}; - let countRestrictions = target && (target[voc.Capabilities.CountRestrictions]?.Countable === false); // count property will be added if CountRestrictions is false + const insertRestrictions = restrictions.InsertRestrictions || target?.[voc.Capabilities.InsertRestrictions] || {}; + let countRestrictions = target?.[voc.Capabilities.CountRestrictions]?.Countable === false // count property will be added if CountRestrictions is false if (insertRestrictions.Insertable !== false) { const lname = pluralize.singular(splitName(name)); const type = modelElement(element.$Type); @@ -836,7 +837,7 @@ module.exports.csdl2openapi = function ( * @param {string} operation Operation (verb) * @param {string} name Name of navigation segment * @param {string} sourceName Name of path source - * @param {integer} level Number of navigation segments so far + * @param {number} level Number of navigation segments so far * @param {boolean} collection Access a collection * @param {boolean} byKey Access by key * @return {string} Operation Text @@ -860,8 +861,8 @@ module.exports.csdl2openapi = function ( * @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 {null |import('./types').TargetRestrictions[]} target Target container child of path + * @param {number} 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 @@ -901,9 +902,12 @@ module.exports.csdl2openapi = function ( customParameters(operation, byKey ? readByKeyRestrictions || readRestrictions : readRestrictions); if (collection) { + // @ts-expect-error - see FIXME in optionTop and optionSkip optionTop(operation.parameters, target, restrictions); + // @ts-expect-error optionSkip(operation.parameters, target, restrictions); if (csdl.$Version >= '4.0') optionSearch(operation.parameters, target, restrictions); + // @ts-expect-error optionFilter(operation.parameters, target, restrictions); optionCount(operation.parameters, target); optionOrderBy(operation.parameters, element, target, restrictions); @@ -960,10 +964,10 @@ module.exports.csdl2openapi = function ( /** * Add parameter for query option $count * @param {Array} parameters Array of parameters to augment - * @param {string} target Target container child of path + * @param {null | import('./types').TargetRestrictions[]} target Target container child of path */ function optionCount(parameters, target) { - const targetRestrictions = target && target[voc.Capabilities.CountRestrictions]; + const targetRestrictions = target?.[voc.Capabilities.CountRestrictions]; const targetCountable = target == null || targetRestrictions == null || targetRestrictions.Countable !== false; @@ -980,11 +984,11 @@ module.exports.csdl2openapi = function ( * 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 {null | import('./types').TargetRestrictions[]} 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 targetRestrictions = target?.[voc.Capabilities.ExpandRestrictions]; const supported = targetRestrictions == null || targetRestrictions.Expandable != false; if (supported) { const expandItems = ['*'].concat(navigationPaths(element).filter(path => !nonExpandable.includes(path))); @@ -1015,7 +1019,7 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot * 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 + * @param {number} level Number of navigation segments so far * @return {Array} Array of navigation property paths */ function navigationPaths(element, prefix = '', level = 0) { @@ -1035,11 +1039,11 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot /** * Add parameter for query option $filter * @param {Array} parameters Array of parameters to augment - * @param {string} target Target container child of path + * @param {null | import('./types').TargetRestrictions} 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] || {}; + const filterRestrictions = restrictions.FilterRestrictions || target?.[voc.Capabilities.FilterRestrictions] || {}; if (filterRestrictions.Filterable !== false) { const filter = { @@ -1067,11 +1071,11 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot * 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 {null | import('./types').TargetRestrictions[]} 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] || {}; + const sortRestrictions = restrictions.SortRestrictions || target?.[voc.Capabilities.SortRestrictions] || {}; if (sortRestrictions.Sortable !== false) { const nonSortable = {}; @@ -1105,7 +1109,7 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot /** * Unpack EnumMember value if it uses CSDL JSON CS01 style, like CAP does - * @param {string or object} path Qualified name of referenced type + * @param {string | object} member Qualified name of referenced type * @return {object} Reference Object */ function enumMember(member) { @@ -1117,7 +1121,7 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot /** * Unpack NavigationPropertyPath value if it uses CSDL JSON CS01 style, like CAP does - * @param {string or object} path Qualified name of referenced type + * @param {string | object} path Qualified name of referenced type * @return {object} Reference Object */ function navigationPropertyPath(path) { @@ -1129,7 +1133,7 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot /** * Unpack PropertyPath value if it uses CSDL JSON CS01 style, like CAP does - * @param {string or object} path Qualified name of referenced type + * @param {string | object} path Qualified name of referenced type * @return {object} Reference Object */ function propertyPath(path) { @@ -1221,11 +1225,11 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot /** * Add parameter for query option $search * @param {Array} parameters Array of parameters to augment - * @param {string} target Target container child of path + * @param {null | import('./types').TargetRestrictions[]} 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] || {}; + const searchRestrictions = restrictions.SearchRestrictions ?? target?.[voc.Capabilities.SearchRestrictions] ?? {}; if (searchRestrictions.Searchable !== false) { if (searchRestrictions[voc.Core.Description]) { @@ -1245,11 +1249,11 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot * 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 {null | import('./types').TargetRestrictions[]} 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] || {}; + const selectSupport = restrictions.SelectSupport ?? target?.[voc.Capabilities.SelectSupport] ?? {}; if (selectSupport.Supported !== false) { const type = modelElement(element.$Type) || {}; @@ -1280,7 +1284,7 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot /** * Add parameter for query option $skip * @param {Array} parameters Array of parameters to augment - * @param {string} target Target container child of path + * @param {Record} target Target container child of path FIXME: this seems to be an incorrect use of TargetRestrictions * @param {object} restrictions Navigation property restrictions of navigation segment */ function optionSkip(parameters, target, restrictions) { @@ -1298,13 +1302,13 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot /** * Add parameter for query option $top * @param {Array} parameters Array of parameters to augment - * @param {string} target Target container child of path + * @param {Record} target Target container child of path FIXME: this seems to be an incorrect use of TargetRestrictions * @param {object} restrictions Navigation property restrictions of navigation segment */ function optionTop(parameters, target, restrictions) { const supported = restrictions.TopSupported !== undefined ? restrictions.TopSupported - : target == null || target[voc.Capabilities.TopSupported] !== false; + : target == null || target?.[voc.Capabilities.TopSupported] !== false; if (supported) { parameters.push({ @@ -1319,14 +1323,14 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot * @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 {null | import('./types').TargetRestrictions[]} target Target container child of path + * @param {number} level Number of navigation segments so far * @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) { - const updateRestrictions = restrictions.UpdateRestrictions || target && target[voc.Capabilities.UpdateRestrictions] || {}; - let countRestrictions = target && (target[voc.Capabilities.CountRestrictions]?.Countable === false); + function operationUpdate(pathItem, element, name, sourceName, target, level, restrictions, byKey = false) { + const updateRestrictions = restrictions.UpdateRestrictions || target?.[voc.Capabilities.UpdateRestrictions] || {}; + let countRestrictions = target?.[voc.Capabilities.CountRestrictions]?.Countable === false; if (updateRestrictions.Updatable !== false) { const type = modelElement(element.$Type); const operation = { @@ -1356,14 +1360,14 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot * @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 {null | import('./types').TargetRestrictions[]} target Target container child of path + * @param {number} level Number of navigation segments so far * @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) { - const deleteRestrictions = restrictions.DeleteRestrictions || target && target[voc.Capabilities.DeleteRestrictions] || {}; - let countRestrictions = target && (target[voc.Capabilities.CountRestrictions]?.Countable === false); + function operationDelete(pathItem, element, name, sourceName, target, level, restrictions, byKey = false) { + const deleteRestrictions = restrictions.DeleteRestrictions || target?.[voc.Capabilities.DeleteRestrictions] || {}; + let countRestrictions = target?.[voc.Capabilities.CountRestrictions]?.Countable === false if (deleteRestrictions.Deletable !== false) { pathItem.delete = { summary: deleteRestrictions.Description || operationSummary('Deletes', name, sourceName, level, element.$Collection, byKey), @@ -1382,11 +1386,11 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot * @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 {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) { - const navigationRestrictions = root[voc.Capabilities.NavigationRestrictions] || {}; + const navigationRestrictions = root[voc.Capabilities.NavigationRestrictions] ?? {}; const rootNavigable = level == 0 && enumMember(navigationRestrictions.Navigability) != 'None' || level == 1 && enumMember(navigationRestrictions.Navigability) != 'Single' || level > 1; @@ -1416,7 +1420,7 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot * @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 + * @param {number} level Number of navigation segments so far * @return {object} Map of navigation property paths and their types */ function navigationPathMap(type, map = {}, prefix = '', level = 0) { @@ -1467,7 +1471,7 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot /** * Key for path item * @param {object} entityType Entity Type object - * @param {integer} level Number of navigation segments so far + * @param {number} level Number of navigation segments so far * @return {object} key: Key segment, parameters: key parameters */ function entityKey(entityType, level) { @@ -1516,7 +1520,7 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot /** * Prefix for key value in key segment - * @param {typename} Qualified name of key property type + * @param {string} typename Qualified name of key property type * @return {string} value prefix */ function pathValuePrefix(typename) { @@ -1529,7 +1533,7 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot /** * Suffix for key value in key segment - * @param {typename} Qualified name of key property type + * @param {string} typename Qualified name of key property type * @return {string} value prefix */ function pathValueSuffix(typename) { @@ -1582,7 +1586,7 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot * @param {string} actionName Qualified name of function * @param {object} overload Function overload * @param {string} sourceName Name of path source - * @param {string} actionImport Action import + * @param {object} actionImport Action import */ function pathItemAction(paths, prefix, prefixParameters, actionName, overload, sourceName, actionImport = {}) { const name = actionName.indexOf('.') === -1 ? actionName : nameParts(actionName).name; @@ -1793,7 +1797,7 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot /** * Construct Responses Object - * @param {string} code HTTP response code + * @param {string | number} code HTTP response code * @param {string} description Description * @param {object} type Response type object * @param {array} errors Array of operation-specific status codes with descriptions @@ -2174,7 +2178,6 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot /** * Construct OData error response - * @return {object} Error response schema */ function error() { const err = { @@ -2218,8 +2221,9 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot }, required: ['lang', 'value'] }; - delete err.properties.error.properties.details; - delete err.properties.error.properties.target; + // explicit cast required, as .details and .target are no inferred as optional above + delete /**@type{any}*/(err.properties.error.properties).details; + delete /**@type{any}*/(err.properties.error.properties).target; } return err; @@ -2227,7 +2231,6 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot /** * Construct OData count response - * @return {object} Count response schema */ function count() { return { @@ -2241,17 +2244,19 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot /** * Construct Schema Object for model object referencing a type - * @param {object} modelElement referencing a type + * @param {object} element referencing a type * @return {object} Schema Object */ function getSchema(element, suffix = '', forParameter = false, forFunction = false) { + + /** @type {import('./types').Schema} */ let s = {}; switch (element.$Type) { case 'Edm.AnnotationPath': case 'Edm.ModelElementPath': case 'Edm.NavigationPropertyPath': case 'Edm.PropertyPath': - s.type = 'string'; + s = { type: 'string' }; break; case 'Edm.Binary': s = { @@ -2261,7 +2266,7 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot if (element.$MaxLength) s.maxLength = Math.ceil(4 * element.$MaxLength / 3); break; case 'Edm.Boolean': - s.type = 'boolean'; + s = { type: 'boolean' }; break; case 'Edm.Byte': s = { @@ -2284,9 +2289,10 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot example: '2017-04-13T15:51:04' + (isNaN(element.$Precision) || element.$Precision === 0 ? '' : '.' + '0'.repeat(element.$Precision)) + 'Z' }; break; - case 'Edm.Decimal': + case 'Edm.Decimal': { + const preDecimal = /** @type {const}*/({ type: 'number', format: 'decimal' }) s = { - anyOf: [{ type: 'number', format: 'decimal' }, { type: 'string' }], + anyOf: [preDecimal, { type: 'string' }], example: 0 }; if (!isNaN(element.$Precision)) s['x-sap-precision'] = element.$Precision; @@ -2294,19 +2300,20 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot // eslint-disable-next-line no-case-declarations let scale = !isNaN(element.$Scale) ? element.$Scale : null; if (scale !== null) { - // Node.js 12.13.0 has problems with negative exponents, 10 ** -5 --> 0.000009999999999999999 + // Node.js 20 has problems with negative exponents, 10 ** -5 --> 0.000009999999999999999 if (scale <= 0) - s.anyOf[0].multipleOf = 10 ** -scale; + preDecimal.multipleOf = 10 ** -scale; else - s.anyOf[0].multipleOf = 1 / 10 ** scale; + preDecimal.multipleOf = 1 / 10 ** scale; } if (element.$Precision < 16) { let limit = 10 ** (element.$Precision - scale); let delta = 10 ** -scale; - s.anyOf[0].maximum = limit - delta; - s.anyOf[0].minimum = -s.anyOf[0].maximum; + preDecimal.maximum = limit - delta; + preDecimal.minimum = -preDecimal.maximum; } break; + } case 'Edm.Double': s = { anyOf: [{ type: 'number', format: 'double' }, { type: 'string' }], @@ -2383,11 +2390,13 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot } break; case 'Edm.String': - case undefined: - s.type = 'string'; + case undefined: { + s = { type: 'string' }; if (element.$MaxLength) s.maxLength = element.$MaxLength; - getPattern(s, element); + const pattern = element[voc.Validation.Pattern]; + if (pattern) s.pattern = pattern; break; + } case 'Edm.TimeOfDay': s = { type: 'string', @@ -2428,41 +2437,55 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot s.example = element[voc.Core.Example].Value; } + /** @returns {s is import('./types.d.ts').StringSchema} */ + const isStringSchema = s => s?.type === 'string' + /** @returns {s is import('./types.d.ts').NumberSchema} */ + const isNumberSchema = s => s?.type === 'number' || s?.type === 'integer' + /** @returns {s is import('./types.d.ts').AnyOf} */ + const isAnyOfSchema = s => Boolean(s?.anyOf) + if (forFunction) { if (s.example && typeof s.example === "string") { s.example = `${pathValuePrefix(element.$Type)}${s.example }${pathValueSuffix(element.$Type)} `; } - if (s.pattern) { - const pre = pathValuePrefix(element.$Type); - const suf = pathValueSuffix(element.$Type); - s.pattern = s.pattern.replace(/^\^/, `^ ${pre} (`); - s.pattern = s.pattern.replace(/\$$/, `)${suf} $`); - } else if (!element.$Type || element.$Type === "Edm.String") { - s.pattern = "^'([^']|'')*'$"; + if (isStringSchema(s)) { + if (s.pattern) { + const pre = pathValuePrefix(element.$Type); + const suf = pathValueSuffix(element.$Type); + s.pattern = s.pattern.replace(/^\^/, `^ ${pre} (`); + s.pattern = s.pattern.replace(/\$$/, `)${suf} $`); + } else if (!element.$Type || element.$Type === "Edm.String") { + s.pattern = "^'([^']|'')*'$"; + } } if (element.$Nullable) { s.default = "null"; - if (s.pattern) { - s.pattern = s.pattern.replace(/^\^/, "^(null|"); - s.pattern = s.pattern.replace(/\$$/, ")$"); + if (isStringSchema(s) && s.pattern) { + s.pattern = s.pattern + .replace(/^\^/, "^(null|") + .replace(/\$$/, ")$"); } } } if (element[voc.Validation.Maximum] != undefined) { if (s.$ref) s = { allOf: [s] }; - if (s.anyOf) { + if (isAnyOfSchema(s) && isNumberSchema(s.anyOf[0])) { s.anyOf[0].maximum = element[voc.Validation.Maximum]; } + // TODO: this implies that we could be handling an AnyOfSchema here. So exclusiveMinimum is attach to that? Or to its first element, as above? + // @ts-expect-error if (element[voc.Validation.Maximum + voc.Validation.Exclusive]) s.exclusiveMaximum = true; } if (element[voc.Validation.Minimum] != undefined) { if (s.$ref) s = { allOf: [s] }; - if (s.anyOf) { + if (isAnyOfSchema(s) && isNumberSchema(s.anyOf[0])) { s.anyOf[0].minimum = element[voc.Validation.Minimum]; } + // TODO: see above + // @ts-expect-error if (element[voc.Validation.Minimum + voc.Validation.Exclusive]) s.exclusiveMinimum = true; } @@ -2500,16 +2523,6 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot if (values) schema.enum = values.map(record => record.Value); } - /** - * Add pattern to Schema Object for string-like model element - * @param {object} schema Schema Object to augment - * @param {object} element Model element - */ - function getPattern(schema, element) { - const pattern = element[voc.Validation.Pattern]; - if (pattern) schema.pattern = pattern; - } - /** * Construct Reference Object for a type * @param {string} typename Qualified name of referenced type @@ -2664,5 +2677,4 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot function isIdentifier(name) { return !name.startsWith('$') && !name.includes('@'); } - }; diff --git a/lib/compile/index.js b/lib/compile/index.js index 9c522b3..4f6cdf5 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -184,4 +184,5 @@ function _servicePath(csdl, csn, protocols) { return paths; } + return {} } \ No newline at end of file diff --git a/lib/compile/types.d.ts b/lib/compile/types.d.ts new file mode 100644 index 0000000..1a8f7b9 --- /dev/null +++ b/lib/compile/types.d.ts @@ -0,0 +1,50 @@ +type StringSchema = { + type: 'string' + format?: 'base64url' | 'uuid' | 'time' | 'date' | 'date-time' | 'duration' + maxLength?: number + example?: string + pattern?: string +} + +type NumberSchema = { + type: 'number' | 'integer' + format?: 'float' | 'double' | 'decimal' | 'uint8' | 'int8' | 'int16' | 'int32' | 'int64' + multipleOf?: number + example?: number, + minimum?: number + maximum?: number + exclusiveMinimum?: boolean + exclusiveMaximum?: boolean +} + +type BooleanSchema = { + type: 'boolean' +} + +type ArraySchema = { + type: 'array', + items: Schema +} + +type Meta = { + nullable?: boolean + default?: unknown + example?: string | number, + description?: string + '$ref'?: unknown +} + +type SingleSchema = (StringSchema | NumberSchema | BooleanSchema | ArraySchema) & Meta + +type AnyOf = { anyOf: Schema[] } & Meta +type AllOf = { allOf: Schema[] } & Meta +type MultiSchema = AnyOf | AllOf + +export type Schema = (SingleSchema | MultiSchema) + + + +export type TargetRestrictions = { + Countable?: boolean + Expandable?: boolean +} \ No newline at end of file diff --git a/package.json b/package.json index 327d5be..a7a2bf8 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ ], "scripts": { "test": "npx jest --silent", - "lint": "npx eslint ." + "lint": "npx eslint .", + "check:types": "npx tsc --noEmit" }, "dependencies": { "pluralize": "^8.0.0" @@ -31,7 +32,9 @@ "@sap/cds": ">=7.6" }, "devDependencies": { + "@types/node": "^24.3.0", + "eslint": "^8.56.0", "jest": ">=29", - "eslint": "^8.56.0" + "typescript": "^5.9.2" } } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c14df73 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,31 @@ +{ + // Visit https://aka.ms/tsconfig to read more about this file + "compilerOptions": { + "module": "nodenext", + "target": "esnext", + "lib": ["esnext"], + "types": ["node"], + "allowJs": true, + "checkJs": true, + "noUncheckedIndexedAccess": false, + "exactOptionalPropertyTypes": true, + "noImplicitOverride": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true, + "strict": true, + "jsx": "react-jsx", + "verbatimModuleSyntax": true, + "isolatedModules": true, + "noUncheckedSideEffectImports": true, + "moduleDetection": "force", + "skipLibCheck": true, + // enable after first round + "noImplicitReturns": false, + "noImplicitAny": false, + }, + "exclude": [ + "**/*.test.js" + ] +}