UNPKG

@sap/cds-dk

Version:

Command line client and development toolkit for the SAP Cloud Application Programming Model

1,248 lines (1,101 loc) 72 kB
/* eslint-disable no-prototype-builtins */ /** * OData V4 to CSN parser */ "use strict"; const messages = require("../message").getMessages(); const common = require("../common"); const versionInfo = require("../../../package.json").version; const ParserContext = require("./ParserContext"); const ignoreObjects = ["$Reference"]; // add all the objects that needs to be ignored for parsing. const { warn, info } = require("../../util/term"); const MetadataConverterFactory = require("./metadataconverter/MetadataConverterFactory"); const edmPrimitiveTypes = [ 'Edm.Binary', 'Edm.Boolean', 'Edm.Byte', 'Edm.Date', 'Edm.DateTimeOffset', 'Edm.Decimal', 'Edm.Double', 'Edm.Duration', 'Edm.Guid', 'Edm.Int16', 'Edm.Int32', 'Edm.Int64', 'Edm.SByte', 'Edm.Single', 'Edm.Stream', 'Edm.String', 'Edm.TimeOfDay', 'Edm.Geography', 'Edm.GeographyPoint', 'Edm.GeographyLineString', 'Edm.GeographyPolygon', 'Edm.GeographyMultiPoint', 'Edm.GeographyMultiLineString', 'Edm.GeographyMultiPolygon', 'Edm.GeographyCollection', 'Edm.Geometry', 'Edm.GeometryPoint', 'Edm.GeometryLineString', 'Edm.GeometryPolygon', 'Edm.GeometryMultiPoint', 'Edm.GeometryMultiLineString', 'Edm.GeometryMultiPolygon', 'Edm.GeometryCollection' ]; const edmToCdsTypeMapping = { 'Edm.Binary': 'cds.LargeBinary', 'Edm.Boolean': 'cds.Boolean', 'Edm.Date': 'cds.Date', 'Edm.Decimal': 'cds.Decimal', 'Edm.Double': 'cds.Double', 'Edm.Guid': 'cds.UUID', 'Edm.Int16': 'cds.Integer', 'Edm.Int32': 'cds.Integer', 'Edm.Int64': 'cds.Integer64', 'Edm.String': 'cds.LargeString', 'Edm.TimeOfDay': 'cds.Time', }; const edmToCdsTypeMappingWithMaxLength = { 'Edm.Binary': 'cds.Binary', 'Edm.Byte': 'cds.Binary', // couldn't find any example 'Edm.String': 'cds.String' }; const edmToCdsTypeMappingWithPrecision = { 'Edm.Decimal': 'cds.Decimal' }; const edmToCdsTypeSpecialHandling = { 'Edm.Byte': 'cds.Integer', 'Edm.DateTimeOffset': ['cds.DateTime', 'cds.Timestamp'], 'Edm.Double': 'cds.Double', // couldn't find an example 'Edm.SByte': 'cds.Integer', 'Edm.Single': 'cds.Double', 'Edm.Stream': 'cds.LargeBinary' }; const known_vocabularies = { 'Org.OData.Authorization.V1': 'Authorization', 'Org.OData.Aggregation.V1': 'Aggregation', 'Org.OData.Core.V1': 'Core', 'Org.OData.Capabilities.V1': 'Capabilities', 'Org.OData.Validation.V1': 'Validation', 'Org.OData.Measures.V1': 'Measures', 'Org.OData.JSON.V1': 'JSON', 'Org.OData.Repeatability.V1': 'Repeatability', 'com.sap.vocabularies.Analytics.v1': 'Analytics', 'com.sap.vocabularies.CodeList.v1': 'CodeList', 'com.sap.vocabularies.Common.v1': 'Common', 'com.sap.vocabularies.Communication.v1': 'Communication', 'com.sap.vocabularies.DataIntegration.v1': 'DataIntegration', 'com.sap.vocabularies.Graph.v1': 'Graph', 'com.sap.vocabularies.Hierarchy.v1': 'Hierarchy', 'com.sap.vocabularies.HTML5.v1': 'HTML5', 'com.sap.vocabularies.ODM.v1': 'ODM', 'com.sap.vocabularies.Offline.v1': 'Offline', 'com.sap.vocabularies.PDF.v1': 'PDF', 'com.sap.vocabularies.PersonalData.v1': 'PersonalData', 'com.sap.vocabularies.Session.v1': 'Session', 'com.sap.vocabularies.UI.v1': 'UI', }; function _isJson(edmx) { let isJson = false; try { JSON.parse(edmx); isJson = true; } catch (err) { isJson = false; } return isJson; } function _extractSchemaNameAndElementName(name) { let lastIndex = name.lastIndexOf('.'); let schemaName = name.substring(0, lastIndex); let elementName = name.substring(lastIndex + 1); return [schemaName, elementName]; } /** * @param {String} fqName * @param {String} schemaName * @param {Object} parserContext * @returns index of schemaName in the schemaDataList */ function _getIndexAndNamespaceOfSchema(fqName, schemaName, parserContext) { let aliasNameMapping = parserContext.schemaAliasToNamespace; let schemaIndexMap = parserContext.schemaToSchemaDataIndex; // if prefix is schema `alias` value if (Object.keys(aliasNameMapping).includes(schemaName)) { schemaName = aliasNameMapping[schemaName]; return [schemaIndexMap[schemaName], schemaName]; } // if the prefix is schema `namespace` value else if (Object.keys(schemaIndexMap).includes(schemaName)) { return [schemaIndexMap[schemaName], schemaName]; } // the schema namespace is not resolvable, throw error else { throw new Error(messages.UNRESOLVED_SCHEMA_NAMESPACE + fqName ); } } function _resolveAliasedElementName(fqName, parserContext) { let [schemaName, elementName] = _extractSchemaNameAndElementName(fqName); [, schemaName] = _getIndexAndNamespaceOfSchema(fqName, schemaName, parserContext); return schemaName + '.' + elementName; } function _collectPrimarySchemaContext(jsonObj, parserContext, importerContext) { let jsonObjEntityContainer = jsonObj.$EntityContainer; if (jsonObjEntityContainer) { // scenario: Schema Namespace="A.B.C.D" & jsonObjEntityContainer is "A.B.C.D.EntityContainerName" [parserContext.primarySchema, parserContext.entityContainerName] = _extractSchemaNameAndElementName(jsonObjEntityContainer); // required for `--keep-namespace` scenario importerContext.schemaNamespace = parserContext.primarySchema; } else { parserContext.mockServerUc = false; } } function _updateElementObjectForCollection(dataTypeObj, isPrimaryKey, propertyDataType) { if (isPrimaryKey) { throw new Error(messages.COLLECTION_IN_KEY); } if (propertyDataType === 'Edm.Stream') { throw new Error(messages.STREAM_NO_COLLECTION); } dataTypeObj.items = {}; Object.keys(dataTypeObj).forEach(objKey => { if (!objKey.startsWith('@') && !(dataTypeObj[objKey] instanceof Object)) { dataTypeObj.items[objKey] = dataTypeObj[objKey]; delete dataTypeObj[objKey]; } }); } function _updateElementObjectForDefaultValue(dataTypeObj, defaultvalue) { // for collection, default value isin't expected if (dataTypeObj.items === undefined) { dataTypeObj.default = {}; // defaultvalue could be of type string (eg., '0') or number (eg., 0 or -1.5) dataTypeObj.default.val = (defaultvalue !== undefined) ? defaultvalue : null; } else { console.log(warn(messages.NO_DEFAULT_FOR_COLLECTIONS)); } } function _updateDataTypeObjectForMaxLength(dataTypeObj, propertyDataType, maxLength) { const mappedValue = edmToCdsTypeMappingWithMaxLength[propertyDataType]; if (mappedValue) { dataTypeObj.type = mappedValue dataTypeObj.length = maxLength; } if (propertyDataType === 'Edm.Binary' && maxLength > 5000) { console.log(warn(messages.BINARY_MAXLENGTH)); } } function _updateDataTypeObjectForPrecision(dataTypeObj, propertyDataType, precision, scale) { const mappedValue = edmToCdsTypeMappingWithPrecision[propertyDataType]; const numberedScale = Number(scale); if (mappedValue) { dataTypeObj.type = mappedValue; if (precision >= 0) { dataTypeObj.precision = precision; } if (numberedScale >= 0) { dataTypeObj.scale = scale; } if (numberedScale > precision) { console.log(warn(messages.SCALE_GT_PRECISION)); } if (precision >= 0 && (scale === 'floating' || scale === 'variable')) { console.log(warn(messages.FIXED_PRECISION_VARIABLE_SCALE)); } } } function _updateDataTypeObjectForSpecialHandling(dataTypeObj, propertyDataType, maxLength, precision) { if (propertyDataType === 'Edm.Byte' && maxLength) { return; // already taken care with direct mapping with maxlength so returning } else if (['Edm.DateTimeOffset', 'Edm.Double'].includes(propertyDataType)) { if (propertyDataType === 'Edm.Double' && precision) { dataTypeObj['@odata.Precision'] = precision; } if (propertyDataType === 'Edm.DateTimeOffset') { dataTypeObj['@odata.Precision'] = precision; dataTypeObj['@odata.Type'] = propertyDataType; dataTypeObj.type = (precision) ? edmToCdsTypeSpecialHandling[propertyDataType][1] : edmToCdsTypeSpecialHandling[propertyDataType][0]; } return; } else if (propertyDataType === 'Edm.Stream') { dataTypeObj['@Core.MediaType'] = 'application/octet-stream'; } dataTypeObj['@odata.Type'] = propertyDataType; dataTypeObj.type = edmToCdsTypeSpecialHandling[propertyDataType]; } function _captureAnnotationInCSN(targetString, annotationObject, csnDefsTargetObject) { /** * For scenarios like: * <Annotation Term="Core.Computed" /> * Here, 'annotationObject' would be null but in CSN we need to treat it as: * @Core.Computed : true */ if (annotationObject === null) { annotationObject = true; } /** * if anntationObject is empty list or empty object, we won't capture it. * eg., <Annotation Term="Core.OptimisticConcurrency"> <Collection /> </Annotation> * TODO: we might have to revisit it later for nested scenario. */ if ((annotationObject instanceof Array && annotationObject.length === 0) || (annotationObject instanceof Object && Object.keys(annotationObject).length === 0) ) { return; } /** * <Annotation Term="Common.Text" Path="name"/> --> annotationTermObject = { "=": annotationTermObject.$Path }; */ if (annotationObject.$Path) { annotationObject = { "=": annotationObject.$Path }; } csnDefsTargetObject[targetString] = annotationObject; } function _replaceSpecialCharacters(text) { return text.replace(/(?:\\[rn]|[\r\n]+)+/gm, "\n").replace(/\s+/g, ' ').replace(/"/g, "&quot;").trim(); } function _resolveCoreTerm(annotationTerm, annotationTermObject, csnDefsTargetObject) { if (annotationTerm === "@Core.Description" || annotationTerm === "@Core.LongDescription") { // we don't capture `null` value for doc if (annotationTermObject === null) return; annotationTermObject = _replaceSpecialCharacters(annotationTermObject); if (csnDefsTargetObject.doc) { if (annotationTerm === "@Core.Description") { annotationTermObject = annotationTermObject + '\n\n' + csnDefsTargetObject.doc; } else { annotationTermObject = csnDefsTargetObject.doc + '\n\n' + annotationTermObject; } } return _captureAnnotationInCSN('doc', annotationTermObject, csnDefsTargetObject); } /** * <Annotation Term="Core.XYZ"/> * For such scenario, mark the `anntationTermObject` as `true` */ if (annotationTermObject === null) annotationTermObject = true; if (annotationTerm === "@Core.OptionalParameter") { _updateElementObjectForDefaultValue(csnDefsTargetObject, annotationTermObject.DefaultValue); } else { _captureAnnotationInCSN(annotationTerm, annotationTermObject, csnDefsTargetObject); } } function _flattenAnnotationObject(annotationPrefix, annotationObject) { let flattenedResult = []; Object.entries(annotationObject).forEach(annotation => { let currentResult = []; /** * if annotation looks like: * { "#" : <value> } or { "=" : <value> } * don't flatten because it is for the `EnumMember` & `Path` use cases respectively */ if (annotation[0] === '#' || annotation[0] === '=') { currentResult[0] = annotationPrefix; currentResult[1] = annotationObject; } else { currentResult[0] = annotationPrefix + '.' + annotation[0]; currentResult[1] = annotation[1]; } flattenedResult.push(currentResult); }); return flattenedResult; } function _removeTypeInAnnotation(annotationObject) { Object.keys(annotationObject).forEach(key => { if (key === "$Type") delete annotationObject[key]; else if (annotationObject[key] instanceof Object) { _removeTypeInAnnotation(annotationObject[key]); } }); } function _parseCapabilitiesTerm(annotationTerm, annotationTermObject, csnDefsTargetObject) { let targetString; /** * <Annotation Term="Capabilities.KeyAsSegmentSupported" /> * For such scenario, mark the `anntationTermObject` as `true` */ if (annotationTermObject === null) annotationTermObject = true; _removeTypeInAnnotation(annotationTermObject); /** * For entries like: * <Annotation Term="Capabilities.FilterFunctions"> <Collection> <String>eq</String> <String>ne</String> </Collection> </Annotation> * <Annotation Term="Capabilities.DeleteRestrictions.Deletable" Bool="true"/> * * simply preserve the value as it is */ if (annotationTermObject instanceof Array || !(annotationTermObject instanceof Object)) { _captureAnnotationInCSN(annotationTerm, annotationTermObject, csnDefsTargetObject); } else { Object.keys(annotationTermObject).forEach(annoKey => { let currentAnnoValue = annotationTermObject[annoKey]; // if the currentAnnoValue is an Object, we need to flatten it if (currentAnnoValue instanceof Object && !(currentAnnoValue instanceof Array)) { targetString = annotationTerm + '.' + annoKey; let flattenedResultList = _flattenAnnotationObject(targetString, currentAnnoValue); for (let idx = 0; idx < flattenedResultList.length; idx++) { _captureAnnotationInCSN(flattenedResultList[idx][0], flattenedResultList[idx][1], csnDefsTargetObject); } } // if the currentAnnoValue is a list or direct value, we preserve as it is else { targetString = annotationTerm + '.' + annoKey; _captureAnnotationInCSN(targetString, currentAnnoValue, csnDefsTargetObject); } }); } } function _parseValidationTerm(annotationTerm, annotationTermObject, csnDefsTargetObject) { const resolveCastFor = ["Minimum", "Maximum", "MaxItems"]; const term = annotationTerm.split('.')[1]; if (resolveCastFor.includes(term) && annotationTermObject.$Cast) { annotationTermObject = Number(annotationTermObject.$Cast); } _captureAnnotationInCSN(annotationTerm, annotationTermObject, csnDefsTargetObject); } function _parseAnnotationTerms(annotationTerm, annotationTermObject, csnDefsTargetObject) { const term = annotationTerm.split('.'); const vocabAlias = term[0]; switch (vocabAlias) { case "@Core": _resolveCoreTerm(annotationTerm, annotationTermObject, csnDefsTargetObject); break; case "@Capabilities": _parseCapabilitiesTerm(annotationTerm, annotationTermObject, csnDefsTargetObject); break; case "@Validation": _parseValidationTerm(annotationTerm, annotationTermObject, csnDefsTargetObject); break; default: _captureAnnotationInCSN(annotationTerm, annotationTermObject, csnDefsTargetObject); break; } } function _validateAnnotationNamespace(annotationTerm, vocabNamespaceToAlias) { let aliasOrNamespace = annotationTerm.substring(1, annotationTerm.lastIndexOf('.')); // This check is for ignoring the @cds annotations given for special cases, e.g. @cds.validate = false for collided action/function if (aliasOrNamespace === "cds") { return } for (let alias of Object.values(known_vocabularies)) { if (alias === aliasOrNamespace) { return; } } for (let [namespace, alias] of Object.entries(vocabNamespaceToAlias)) { if (alias === aliasOrNamespace || namespace === aliasOrNamespace) { console.log(warn(annotationTerm + " AnnotationTerm belongs to an unknown namespace: " + namespace)); return; } } console.log(warn(annotationTerm + ": Reference of this AnnotationTerm is not present in the edmx file.")); } function _resolveAnnotationTerm(schemaDataAnnotation, parserContext) { for (const [namespace, alias] of Object.entries(parserContext.vocabNamespaceToAlias)) { if (known_vocabularies[namespace]) { // Case 1: Namespace belongs to known vocabularies. Replacing the namespace/alias of the doc with standard alias if (schemaDataAnnotation.includes('@' + alias + '.')) { schemaDataAnnotation = schemaDataAnnotation.split('@' + alias + '.').join('@' + known_vocabularies[namespace] + '.'); } if (schemaDataAnnotation.includes('@' + namespace + '.')) { schemaDataAnnotation = schemaDataAnnotation.split('@' + namespace + '.').join('@' + known_vocabularies[namespace] + '.'); } } else { // Case 2: Namespace doesn't belong to known vocabularies. Replacing the namespace with alias(if provided) in the doc. if (schemaDataAnnotation.includes('@' + namespace + '.') && alias) { schemaDataAnnotation = schemaDataAnnotation.split('@' + namespace + '.').join('@' + alias + '.'); } } } /** * Case 3: Namespace doesn't have edmx:Reference. But it is present in known_vocabularies. * Replacing it with standard alias. */ for (const [namespace, alias] of Object.entries(known_vocabularies)) { if (schemaDataAnnotation.includes(namespace)) { schemaDataAnnotation = schemaDataAnnotation.split(namespace).join(alias); } } let index = schemaDataAnnotation.indexOf('"@'); while (index != -1) { let i = schemaDataAnnotation.indexOf('"', index + 1); let annotationTerm = schemaDataAnnotation.substring(index+1, i); _validateAnnotationNamespace(annotationTerm, parserContext.vocabNamespaceToAlias); index = schemaDataAnnotation.indexOf('"@', i+1); } return JSON.parse(schemaDataAnnotation); } /** * Capture all the annotation cases withtin an Element * * UnKnown term: <Annotation Term="Org.OData.ASDF.V1.qwe"/> * Known term: <Annotation Term="Org.OData.Measures.V1.Unit" String="Centimeters"/> * * if "--include-namespaces *" then we also capture for ABS and QWE * <TypeDefinition Name="TD_2" UnderlyingType="Edm.Int32" ABS:asdf="testing" QWE:qwe="testing"/> */ function _captureAnnotationWithinElement(sourceObject, targetObject, parserContext) { let element; Object.keys(sourceObject).forEach(key => { if (key.startsWith('@')) { element = _resolveAnnotationTerm(JSON.stringify(key), parserContext); _parseAnnotationTerms(element, sourceObject[key], targetObject); } }); } function _generatePrimitiveDataTypeCsn(propertyObject, isPrimaryKey, parserContext) { const dataTypeObj = {}; const propertyDataType = propertyObject.$Type; const maxLength = Number(propertyObject.$MaxLength); const precision = Number(propertyObject.$Precision); const scale = propertyObject.$Scale; const underlyingType = propertyObject.$UnderlyingType; const isCollection = propertyObject.$Collection; const nullable = propertyObject.$Nullable; const defaultvalue = propertyObject.$DefaultValue; if (isPrimaryKey) { dataTypeObj.key = true; if (nullable) { console.log(warn(messages.NULLABLE_KEY)); } } _captureAnnotationWithinElement(propertyObject, dataTypeObj, parserContext); dataTypeObj.type = edmToCdsTypeMapping[propertyDataType]; if (maxLength) { _updateDataTypeObjectForMaxLength(dataTypeObj, propertyDataType, maxLength); } /** * Precision: non-negative integers * Scale: non-negative integers, variable, floating */ if (precision >= 0 || scale !== undefined) { _updateDataTypeObjectForPrecision(dataTypeObj, propertyDataType, precision, scale); } if (edmToCdsTypeSpecialHandling[propertyDataType]) { _updateDataTypeObjectForSpecialHandling(dataTypeObj, propertyDataType, maxLength, precision); } if (!edmToCdsTypeMapping[propertyDataType] && !edmToCdsTypeSpecialHandling[propertyDataType]) { dataTypeObj['@odata.Type'] = propertyDataType; dataTypeObj.type = 'cds.String'; } /** * When called from TypeDefinition or EnumType we, * don't want to process Nullable, Collection, DefaultValue. */ if (underlyingType) { return dataTypeObj; } if (!nullable) { dataTypeObj['notNull'] = true; } if (isCollection) { _updateElementObjectForCollection(dataTypeObj, isPrimaryKey, propertyDataType); } // defaultvalue could be of type string (eg., '0') or number (eg., 0 or -1.5) if (defaultvalue !== undefined) { _updateElementObjectForDefaultValue(dataTypeObj, defaultvalue); } return dataTypeObj; } function _getEntityTypeMappedName(entityType, parserContext) { const mappedResult = parserContext.entityToEntitySetMap.get(entityType); // if one entity type has mapping to multiple entity sets, use the first entity set if (mappedResult.length > 1) { return parserContext.primarySchema + '.' + mappedResult[0]; } else { return parserContext.primarySchema + '.' + mappedResult; } } function _generateStructuredDataTypeCsn(dataType, propertyObject, isPrimaryKey, dataTypeKind, elementKind, parserContext) { const dataTypeObj = {}; const isCollection = propertyObject.$Collection; const nullable = propertyObject.$Nullable; const defaultvalue = propertyObject.$DefaultValue; let propertyDataType = propertyObject.$Type; // const underlyingType = propertyObject.$UnderlyingType; // might have to use later /** * for a typeDefintion to be a Primary key, it has to be * based out of one of the entries in the edmPrimitiveTypes list * We might have to decide if we want to throw an error if we get * an entry not following this rule. */ if (isPrimaryKey) { dataTypeObj.key = true; if (nullable) { console.log(warn(messages.NULLABLE_KEY)); } } // if dataTypeKind is EntityType, also check for mapped entity name if (parserContext.mockServerUc && dataTypeKind === 'EntityType') { // propertyDataType is resolved fully qualified name if (parserContext.entityToEntitySetMap?.get(propertyDataType)) { propertyDataType = _getEntityTypeMappedName(propertyDataType, parserContext); } // dataType can be aliased name, so check that too. else if (parserContext.entityToEntitySetMap?.get(dataType)) { propertyDataType = _getEntityTypeMappedName(dataType, parserContext); } } dataTypeObj.type = propertyDataType; if (!nullable) { dataTypeObj['notNull'] = true; } if (isCollection) { _updateElementObjectForCollection(dataTypeObj, isPrimaryKey, propertyDataType); } /** * only Primitive type and EnumType can have DefaultValue facet as per documentation * but compiler accepts it for TypeDefintion and ComplexType as well, * do we also need to handle it for TypeDefintion and ComplexType? * * For parameters, defaultvalue is added via OptionalParamter Annotation * defaultvalue could be of type string (eg., '0') or number (eg., 0 or -1.5) */ if (defaultvalue !== undefined && dataTypeKind === 'EnumType' && elementKind === 'property') { _updateElementObjectForDefaultValue(dataTypeObj, defaultvalue); } else { // throw error, because only primitive or enumeration type can have DefaultValue. } _captureAnnotationWithinElement(propertyObject, dataTypeObj, parserContext); return dataTypeObj; } function _parseAssociationProperty(foreignReference, associationNode, parserContext) { const associationProperty = {}; associationProperty.type = (associationNode.$OnDelete === 'Cascade') ? 'cds.Composition' : 'cds.Association'; let schemaName = associationNode.$Type.substring(0, associationNode.$Type.lastIndexOf('.')); if (parserContext.schemaAliasToNamespace[schemaName]) { schemaName = parserContext.schemaAliasToNamespace[schemaName]; } let resolvedEntityName; let entityTypeMapping = ( parserContext.entityToEntitySetMap?.get(associationNode.$Type) || parserContext.entityToSingletonMap?.get(associationNode.$Type) ); // if entityTypeMapping is `undefined`, means the associationNode.$Type could be aliased if (!entityTypeMapping) { resolvedEntityName = _resolveAliasedElementName(associationNode.$Type, parserContext); entityTypeMapping = ( parserContext.entityToEntitySetMap?.get(resolvedEntityName) || parserContext.entityToSingletonMap?.get(resolvedEntityName) ); } if (parserContext.mockServerUc && entityTypeMapping) { if (entityTypeMapping.length > 1) { for (let j = 0; j < entityTypeMapping.length; j++) { if (entityTypeMapping[j] === foreignReference) { associationProperty.target = parserContext.schemaToPrimarySchema[schemaName] + '.' + entityTypeMapping[j]; } } } else { associationProperty.target = parserContext.schemaToPrimarySchema[schemaName] + '.' + entityTypeMapping; } } else { associationProperty.target = resolvedEntityName; } // cardinality associationProperty.cardinality = {}; if (associationNode.$Collection) { associationProperty.cardinality.max = '*'; } else { associationProperty.cardinality.max = 1; } // Convert managed associations and compositions in unmanaged with empty key to avoid // "generation" of keys, that do not exist in the external service. if (common.checkForEmptyKeys(associationProperty, "V4")) { associationProperty.keys = []; } _captureAnnotationWithinElement(associationNode, associationProperty, parserContext); return associationProperty; } function _checkIsStructureTypeAndReturnDataKind(dataType, contentObject, elementKind, parserContext) { let fqName = dataType; let [schemaName, elementName] = _extractSchemaNameAndElementName(dataType); if (parserContext.schemaAliasToNamespace[schemaName]) { fqName = parserContext.schemaAliasToNamespace[schemaName] + "." + elementName; contentObject.$Type = fqName; } if (parserContext.complexTypes.includes(fqName)) { return 'ComplexType'; } else if (parserContext.typeDefinitions.includes(fqName)) { return 'TypeDefinition'; } else if (parserContext.enumTypes.includes(fqName)) { return 'EnumType'; } else if (elementKind === 'property' && parserContext.entityTypes.includes(fqName)) { throw new Error(messages.UNSUPPORTED_PROPERTY_TYPE + dataType); } else if (elementKind === 'parameter' && parserContext.entityTypes.includes(fqName)) { return 'EntityType'; } else { throw new Error(messages.UNRESOLVED_TYPE + dataType); } } /** * @param {Object} csnObj * @param {Object} elementObj * @param {Object} parserContext */ function _parsePropertyContent(csnObj, elementObj, parserContext) { let contentObj = {}; const isPrimaryKey = true; let dataType, isPrimitiveType, keySet = []; Object.keys(elementObj).forEach(objKey => { const contentKind = elementObj[objKey].$Kind; contentObj = elementObj[objKey]; if ((objKey.indexOf('@') === -1) && (objKey.indexOf('.') === -1)) { if (contentObj instanceof Object) { if (contentObj.$Type) { dataType = contentObj.$Type; isPrimitiveType = dataType.startsWith('Edm') ? true : false; } if (objKey === '$Key') { keySet.push(...elementObj.$Key); } // primitive datatypes else if (isPrimitiveType) { if (edmPrimitiveTypes.includes(dataType)) { csnObj.elements[objKey] = _generatePrimitiveDataTypeCsn(contentObj, (keySet.includes(objKey) ? isPrimaryKey : !isPrimaryKey), parserContext ); } else { throw new Error(messages.INVALID_DATATYPE + `'${dataType}' in element: '${objKey}'`) } } // structured datatypes else if (!isPrimitiveType && !(contentKind === 'NavigationProperty')) { const dataKind = _checkIsStructureTypeAndReturnDataKind(dataType, contentObj, 'property', parserContext); if (['EnumType', 'TypeDefinition'].includes(dataKind)) { csnObj.elements[objKey] = _generateStructuredDataTypeCsn(dataType, contentObj, (keySet.includes(dataType) ? isPrimaryKey : !isPrimaryKey), dataKind, 'property', parserContext ); } else { csnObj.elements[objKey] = _generateStructuredDataTypeCsn(dataType, contentObj, !isPrimaryKey, dataKind, 'property', parserContext ); } } else if (contentKind === 'NavigationProperty') { csnObj.elements[objKey] = _parseAssociationProperty(objKey, elementObj[objKey], parserContext); } } } }); } function _addBlobElement() { return { "blob": { "@Core.MediaType": 'application/octet-stream', "type": "cds.LargeBinary" } } } /** * to handle the Abstract, Open & Base Type scenarios * @param {String} fqName, element name * @param {Object} csnObj, * @param {Object} elementObj, * @param {Object} parserContext */ function _captureOpenTypes(fqName, csnObj, elementObj, isEntity, mockServerUc, parserContext) { let baseTypeName = elementObj.$BaseType; let isAbstract = elementObj.$Abstract; let isOpenType = elementObj.$OpenType; if (baseTypeName) { let resolvedBaseTypeName = _resolveAliasedElementName(baseTypeName, parserContext); if (isEntity) { if (parserContext.entityTypes.includes(resolvedBaseTypeName)) { if (!parserContext.entityTypeOpenEntries.includes(resolvedBaseTypeName)) { parserContext.entityTypeOpenEntries.push(resolvedBaseTypeName); } if (mockServerUc) { let entityMapping = ( parserContext.entityToEntitySetMap?.get(baseTypeName) || parserContext.entityToSingletonMap?.get(baseTypeName) ); if (entityMapping) { resolvedBaseTypeName = _getEntityTypeMappedName(baseTypeName, parserContext); } else { entityMapping = ( parserContext.entityToEntitySetMap?.get(resolvedBaseTypeName) || parserContext.entityToSingletonMap?.get(resolvedBaseTypeName) ); if (entityMapping) { resolvedBaseTypeName = parserContext.primarySchema + '.' + entityMapping[0]; } } } } else { throw new Error(messages.UNRESOLVED_TYPE + `'${baseTypeName}'`); } } else { if (parserContext.complexTypes.includes(resolvedBaseTypeName) && !parserContext.complexTypeOpenEntries.includes(resolvedBaseTypeName) ) { parserContext.complexTypeOpenEntries.push(resolvedBaseTypeName); } else if (!parserContext.complexTypes.includes(resolvedBaseTypeName)) { throw new Error(messages.UNRESOLVED_TYPE + `'${baseTypeName}'`); } } csnObj.includes = [resolvedBaseTypeName]; } if (isAbstract || isOpenType) { if (isEntity && !parserContext.entityTypeOpenEntries.includes(fqName)) { parserContext.entityTypeOpenEntries.push(fqName); } else if (!parserContext.complexTypeOpenEntries.includes(fqName)) { parserContext.complexTypeOpenEntries.push(fqName); } } } function _generateCSNForEntityType(ignorePersistenceSkip, entityList, parserContext) { const csnEntity = {}; let isEntity = true; entityList.forEach(entityElement => { let entityName = Object.keys(entityElement)[0]; csnEntity[entityName] = {}; csnEntity[entityName].kind = 'entity'; csnEntity[entityName]['@cds.external'] = true; if (!ignorePersistenceSkip) { csnEntity[entityName]['@cds.persistence.skip'] = true; } _captureOpenTypes(entityName, csnEntity[entityName], entityElement[entityName], isEntity, parserContext.mockServerUc, parserContext); _captureAnnotationWithinElement(entityElement[entityName], csnEntity[entityName], parserContext); csnEntity[entityName].elements = {}; _parsePropertyContent(csnEntity[entityName], entityElement[entityName], parserContext); // needs to be checked if this is necessary if (entityElement[entityName].$HasStream) { csnEntity[entityName].elements = { ...csnEntity[entityName].elements, ..._addBlobElement() }; } }); // add @open annotations parserContext.entityTypeOpenEntries.forEach(entityName => { if (csnEntity[entityName]) csnEntity[entityName]['@open'] = true; }); return csnEntity; } function _parseCSNForEntityType(schemaData, ignorePersistenceSkip, parserContext) { let entitiesList = []; for (let fqName of parserContext.entityTypes) { let entity = {}; let [schemaName, elementName] = _extractSchemaNameAndElementName(fqName); let entityObj = schemaData[elementName]; if (entityObj?.$Kind === 'EntityType' && schemaName === schemaData.$SchemaNamespace) { entity[fqName] = entityObj; entitiesList.push(entity); } } return _generateCSNForEntityType(ignorePersistenceSkip, entitiesList, parserContext); } function _parseParameterAndReturnTypeContent(elementObject, parserContext) { let dataType = elementObject.$Type; const isPrimitiveType = dataType.startsWith('Edm') ? true : false; const isPrimaryKey = true; // primitive type if (isPrimitiveType) { if (edmPrimitiveTypes.includes(dataType)) { return _generatePrimitiveDataTypeCsn(elementObject, !isPrimaryKey, parserContext); } else { throw new Error(messages.INVALID_DATATYPE + `'${dataType}' in element: '${elementObject.$Name}'`) } } // structured type else { const dataKind = _checkIsStructureTypeAndReturnDataKind(dataType, elementObject, 'parameter', parserContext); return _generateStructuredDataTypeCsn(dataType, elementObject, !isPrimaryKey, dataKind, 'parameter', parserContext); } } function _generateCSNForBoundActionFunction(actionFunctionObject, kind, parserContext) { const csnObj = {}; const isBound = actionFunctionObject.$IsBound; const parameters = actionFunctionObject.$Parameter; const returnType = actionFunctionObject.$ReturnType; csnObj.kind = kind; _captureAnnotationWithinElement(actionFunctionObject, csnObj, parserContext); if (parameters) { csnObj.params = {}; for (let index = 0; index < parameters.length; index++) { let isCollection = parameters[index].$Collection; csnObj.params[parameters[index].$Name] = _parseParameterAndReturnTypeContent(parameters[index], parserContext); /** * binding parameter handling - only first parameter will contain the binding * parameter information, so only make change to the first parameter information. * NOTE: Edm.Stream can act as the binding parameter for action/function * We don't have any example for it, so not handled as of now */ if (isBound && index === 0) { if (isCollection) { csnObj.params[parameters[index].$Name].items.type = '$self'; } else { csnObj.params[parameters[index].$Name].type = '$self'; } } } } if (returnType) { csnObj.returns = _parseParameterAndReturnTypeContent(returnType, parserContext); } return csnObj; } function _parseBoundActionFunction(schemaData, csnObj, kind, parserContext) { const boundObject = (kind === 'action') ? parserContext.entityToBoundActions : parserContext.entityToBoundFunctions; Object.keys(boundObject).forEach(fqEntityName => { const [schemaName, ] = _extractSchemaNameAndElementName(fqEntityName); if (schemaName === schemaData.$SchemaNamespace) { const actionFunctionObjectList = boundObject[fqEntityName]; if (!csnObj[fqEntityName].actions) { csnObj[fqEntityName].actions = {}; } Object.keys(actionFunctionObjectList).forEach(actionFunctionName => { csnObj[fqEntityName].actions[actionFunctionName] = _generateCSNForBoundActionFunction( actionFunctionObjectList[actionFunctionName], kind, parserContext ); }); } }); } function _parseUnboundActionFunction(schemaData, kind, parserContext) { const unboundObject = (kind === 'action') ? parserContext.unboundActions : parserContext.unboundFunctions; const csnObj = {}; Object.keys(unboundObject).forEach(fqName => { const [schemaName, ] = _extractSchemaNameAndElementName(fqName); if (schemaName === schemaData.$SchemaNamespace) { const actionFunctionObject = unboundObject[fqName]; const parameters = actionFunctionObject.$Parameter; const returnType = actionFunctionObject.$ReturnType; csnObj[fqName] = {}; csnObj[fqName].kind = kind; csnObj[fqName]['@cds.external'] = true; _captureAnnotationWithinElement(actionFunctionObject, csnObj[fqName], parserContext); if (parameters) { csnObj[fqName].params = {}; for (let paramObj of parameters) { csnObj[fqName].params[paramObj.$Name] = _parseParameterAndReturnTypeContent(paramObj, parserContext); } } if (returnType) { csnObj[fqName].returns = _parseParameterAndReturnTypeContent(returnType, parserContext); } } }); return csnObj; } function _parserCSNForComplexType(schemaData, parserContext) { const complexCsn = {}; const isEntity = true; for (let fqName of parserContext.complexTypes) { let [schemaName, elementName] = _extractSchemaNameAndElementName(fqName); let complexObj = schemaData[elementName]; if (complexObj?.$Kind === 'ComplexType' && schemaName === schemaData.$SchemaNamespace) { complexCsn[fqName] = {}; complexCsn[fqName].kind = 'type'; complexCsn[fqName]['@cds.external'] = true; complexCsn[fqName].elements = {}; // inheritance handling _captureOpenTypes(fqName, complexCsn[fqName], complexObj, !isEntity, !parserContext.mockServerUc, parserContext); _captureAnnotationWithinElement(complexObj, complexCsn[fqName], parserContext); _parsePropertyContent(complexCsn[fqName], complexObj, parserContext); } } // add @open annotations parserContext.complexTypeOpenEntries.forEach(key => { if (complexCsn[key]) complexCsn[key]['@open'] = true; }); return complexCsn; } function _parseCSNForEnumType(schemaData, parserContext) { const nonMemberList = ["$Kind", "$Type", "$UnderlyingType", "$IsFlags"]; const possibleCdsTypes = ['cds.Integer', 'cds.Integer64']; const enumCsn = {}; const isKey = true; for (let fqName of parserContext.enumTypes) { let [schemaName, elementName] = _extractSchemaNameAndElementName(fqName); let enumObj = schemaData[elementName]; if (enumObj?.$Kind === 'EnumType' && schemaName === schemaData.$SchemaNamespace) { enumCsn[fqName] = {}; enumCsn[fqName].kind = 'type'; enumCsn[fqName]['@cds.external'] = true; enumCsn[fqName].enum = {}; /** * If UnderlyingType not specified, default is Edm.Int32 --> cds.Integer */ let underlyingType = enumObj.$UnderlyingType; enumObj.$Type = underlyingType; let cdsType = underlyingType ? (_generatePrimitiveDataTypeCsn(enumObj, !isKey, parserContext)) : { type : 'cds.Integer' }; enumCsn[fqName].type = cdsType.type; if (cdsType['@odata.Type']) { enumCsn[fqName]['@odata.Type'] = cdsType['@odata.Type']; } if (!possibleCdsTypes.includes(enumCsn[fqName].type)) { throw new Error(messages.INVALID_ENUM_TYPE + `'${underlyingType}'`); } Object.keys(enumObj).forEach(member => { if (!nonMemberList.includes(member)) { enumCsn[fqName].enum[member] = {}; enumCsn[fqName].enum[member].val = enumObj[member]; } }); } } return enumCsn; } function _parseCSNForTypeDefinition(schemaData, parserContext) { const typeDef = {}; const isKey = true; for (let fqName of parserContext.typeDefinitions) { let [schemaName, elementName] = _extractSchemaNameAndElementName(fqName); let typeDefObj = schemaData[elementName]; if (typeDefObj?.$Kind === 'TypeDefinition' && schemaName === schemaData.$SchemaNamespace) { typeDef[fqName] = {}; typeDef[fqName].kind = 'type'; typeDef[fqName]['@cds.external'] = true; let underlyingType = typeDefObj.$UnderlyingType; typeDefObj.$Type = underlyingType; if (edmPrimitiveTypes.includes(underlyingType)) { typeDef[fqName] = Object.assign(typeDef[fqName], _generatePrimitiveDataTypeCsn(typeDefObj, !isKey, parserContext)); } else { throw new Error(messages.UNRESOLVED_TYPE + `'${underlyingType}'`); } } } return typeDef; } function _resolveEntityContainerAnnotationTarget(targetPath, csnDefs, hasProperty, parserContext) { /** * format: <schemaName_or_Alias><EntityContainerName>/<ContainerElement> * <schemaName_or_Alias><EntityContainerName>/<ContainerElement>/... * * needs to be handled without 'EntityContainerName' */ if (hasProperty) { const eleNameList = targetPath.split('/'); // based on the length of eleName list, do the processing if (eleNameList.length === 2) { return csnDefs[parserContext.primarySchema + '.' + eleNameList[1]]; } else { // TODO: We might have to take care of such senarios in the future. } } // direct path to service object else { return csnDefs[parserContext.primarySchema]; } } function _resolveActionFunctionAnnotationTarget(targetPath, csnDefs, hasParameter, parserContext) { let elemNameList = targetPath.split('('); const fqActFunName = _resolveAliasedElementName(elemNameList[0], parserContext); const paramName = hasParameter ? targetPath.split('/')[1] : null; // unbound action or function if (csnDefs[fqActFunName]) { // with parameter if (hasParameter && csnDefs[fqActFunName].params[paramName]) { return csnDefs[fqActFunName]?.params[paramName]; } // without parameter else if (!hasParameter) { return csnDefs[fqActFunName]; } } // bound action or function else { const [, elementName] = _extractSchemaNameAndElementName(fqActFunName); const isCollection = elemNameList[1] === 'Collection' ? true : false; let parameterList = isCollection ? elemNameList[2].split(')') : elemNameList[1].split(')'); let bindingParameter = parameterList[0]; if (bindingParameter.includes(',')) { bindingParameter = bindingParameter.split(',')[0]; } const entityType = _resolveAliasedElementName(bindingParameter, parserContext); if (csnDefs[entityType]?.actions[elementName]) { // with parameter if (hasParameter && csnDefs[entityType].actions[elementName].params[paramName]) { return csnDefs[entityType].actions[elementName].params[paramName]; } // without parameter else if (!hasParameter) { return csnDefs[entityType].actions[elementName]; } } } } function _resolveAnnotationTarget(targetPath, csnDefs, parserContext) { const indexOfSlash = targetPath.indexOf('/'); const indexOfOpenBrace = targetPath.indexOf('('); const hasPropOrParam = (indexOfSlash > 0); let elemNameList, fqName; // Case 1: Direct path if (indexOfOpenBrace === -1 && indexOfSlash === -1) { fqName = _resolveAliasedElementName(targetPath, parserContext); if (csnDefs[fqName]) { return csnDefs[fqName]; } else if (parserContext.entityContainerAnnotations[targetPath]) { return _resolveEntityContainerAnnotationTarget(fqName, csnDefs, hasPropOrParam, parserContext); } } // Case 2: Direct path with / else if (indexOfOpenBrace === -1 && indexOfSlash !== -1) { elemNameList = targetPath.split('/'); fqName = _resolveAliasedElementName(elemNameList[0], parserContext); if (csnDefs[fqName]) { if (csnDefs[fqName]?.elements?.[elemNameList[1]]) { return csnDefs[fqName].elements[elemNameList[1]]; } else if (csnDefs[fqName]?.enum?.[elemNameList[1]]) { return csnDefs[fqName].enum[elemNameList[1]]; } } // Entity Container path with / else if (parserContext.entityContainerAnnotations[targetPath]) { return _resolveEntityContainerAnnotationTarget(targetPath, csnDefs, hasPropOrParam, parserContext); } } // Case 3: actions and functions path with or without parameter else { return _resolveActionFunctionAnnotationTarget(targetPath, csnDefs, hasPropOrParam, parserContext); } } function _parseAnnotations(schemaAnnotations, csnDefs, parserContext) { Object.keys(schemaAnnotations).forEach(annotationTargetPath => { let csnDefsTargetObject = _resolveAnnotationTarget(annotationTargetPath, csnDefs, parserContext); // if the target object exists, process the annotation terms if (csnDefsTargetObject) { let annotationTerms = schemaAnnotations[annotationTargetPath]; Object.keys(annotationTerms).forEach(annotationTerm => { let annotationTermObject = schemaAnnotations[annotationTargetPath][annotationTerm]; _parseAnnotationTerms(annotationTerm, annotationTermObject, csnDefsTargetObject); }); } }); } function _addSuffixForCollisionTypes(fqName, suffix) { console.log(info(`INFO: Due to collision in ${fqName} suffix "${suffix}" is added.`)); const suffixedFqElementName = (fqName + suffix); return suffixedFqElementName; } function _parseEntityContainer(csnDefs, entityContainerElements, parserContext) { const entityContainerElementNames = Object.keys(entityContainerElements); const redundantElementNames = []; let possibleAnnotations = {}; // filter entries starting with '@', they can be possible annotation entityContainerElementNames.filter( elementName => elementName.startsWith('@') ).forEach(elementName => { possibleAnnotations[elementName] = entityContainerElements[elementName]; delete entityContainerElements[elementName]; }); entityContainerElementNames.forEach(ecElement => { if (entityContainerElements[ecElement] instanceof Object) { let definitionRefName, kind; if (entityContainerElements[ecElement].$Action) { definitionRefName = entityContainerElements[ecElement].$Action; kind = 'action'; } else if (entityContainerElements[ecElement].$Function) { definitionRefName = entityContainerElements[ecElement].$Function; kind = 'function'; } else { definitionRefName = entityContainerElements[ecElement].$Type; kind = 'entity'; } let [, elementName] = _extractSchemaNameAndElementName(definitionRefName); let fqDefRefName = _resolveAliasedElementName(definitionRefName, parserContext); let fqElementName = parserContext.primarySchema + "." + ecElement; if (csnDefs[fqElementName] && csnDefs[fqE