@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
JavaScript
/* 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, """).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