// eslint-disable-next-line @typescript-eslint/triple-slash-reference /// import { xml2js } from 'xml-js'; import { ensureArray, RawMetadataInstance } from './utils'; import type { RawAction, RawEntityType, ReferentialConstraint, RawProperty, RawEntitySet, RawAssociation, RawAssociationEnd, RawSchema, RawEntityContainer, RawMetadata, Reference, RawAssociationSet, RawAssociationSetEnd, RawComplexType, RawSingleton, RawTypeDefinition, SimpleIdentifier, FullyQualifiedName, PropertyValue, AnnotationList, AnnotationRecord, PropertyPathExpression, NavigationPropertyPathExpression, AnnotationPathExpression, PathExpression, Apply, Expression, RawAnnotation, RawV2NavigationProperty, RawV4NavigationProperty } from '@sap-ux/vocabularies-types'; import { convertV2Annotations } from './v2annotationsSupport'; import type { V2annotationsSupport } from './v2annotationsSupport'; const collectionRegexp = /Collection\(([^)]+)\)/; type PropertyOutput = { entityProperties: RawProperty[]; entityKeys: RawProperty[]; }; // Type guards /** * Check whether the navigation property is a v4 navigation property. * * @param navPropertyAttributes * @returns true if the navigationProperty is a v4 one */ function isV4NavProperty( navPropertyAttributes: EDMX.NavigationPropertyAttributesV2 | EDMX.NavigationPropertyAttributesV4 ): navPropertyAttributes is EDMX.NavigationPropertyAttributesV4 { return ( (navPropertyAttributes as EDMX.NavigationPropertyAttributesV4).Type !== null && (navPropertyAttributes as EDMX.NavigationPropertyAttributesV4).Type !== undefined ); } // Parser Methods /** * Retrieves the name of the keys for that entity type. * * @param propertyRefs {EDMX.PropertyRef} property reference * @returns the entityType keys name */ function getEntityTypeKeys(propertyRefs: EDMX.PropertyRef[]): SimpleIdentifier[] { return propertyRefs.map((propertyRef) => propertyRef._attributes.Name); } /** * Parse the EDMX.Property to retrieve the property. * * @param entityProperties * @param entityKeys * @param entityTypeFQN * @param annotationLists * @returns the properties */ function parseProperties( entityProperties: EDMX.Property[], entityKeys: SimpleIdentifier[], entityTypeFQN: FullyQualifiedName, annotationLists: AnnotationList[] ): PropertyOutput { return entityProperties.reduce( (outObject: PropertyOutput, entityProperty: EDMX.Property) => { const edmProperty: RawProperty = { _type: 'Property', name: entityProperty._attributes.Name, fullyQualifiedName: `${entityTypeFQN}/${entityProperty._attributes.Name}`, type: entityProperty._attributes.Type }; if (entityProperty._attributes.MaxLength) { edmProperty.maxLength = parseInt(entityProperty._attributes.MaxLength, 10); } if (entityProperty._attributes.Precision) { edmProperty.precision = parseInt(entityProperty._attributes.Precision, 10); } if (entityProperty._attributes.Scale) { edmProperty.scale = parseInt(entityProperty._attributes.Scale, 10); } if (entityProperty._attributes.Nullable) { edmProperty.nullable = entityProperty._attributes.Nullable !== 'false'; } if (entityProperty._attributes.DefaultValue) { switch (edmProperty.type) { case 'Edm.Int16': case 'Edm.Byte': case 'Edm.Int32': case 'Edm.Int64': edmProperty.defaultValue = parseInt(entityProperty._attributes.DefaultValue, 10); break; case 'Edm.Decimal': edmProperty.defaultValue = parseFloat(entityProperty._attributes.DefaultValue); break; case 'Edm.Boolean': edmProperty.defaultValue = entityProperty._attributes.DefaultValue === 'true'; break; default: edmProperty.defaultValue = entityProperty._attributes.DefaultValue; break; } } outObject.entityProperties.push(edmProperty); if (entityKeys.indexOf(edmProperty.name) !== -1) { outObject.entityKeys.push(edmProperty); } const v2Annotations = convertV2Annotations( entityProperty._attributes as V2annotationsSupport, 'Property', entityProperty._attributes.Name ); if (v2Annotations.length > 0) { annotationLists.push(createAnnotationList(edmProperty.fullyQualifiedName, v2Annotations)); } return outObject; }, { entityProperties: [], entityKeys: [] } ); } /** * Parse the referential constraint from the EDMX into an object structure. * * @param referentialConstraints the EDMX referential constraints * @param sourceTypeName the name of the source type * @param targetTypeName the name of the target type * @returns the object representation of the referential constraint. */ function parseReferentialConstraint( referentialConstraints: EDMX.ReferentialConstraint[], sourceTypeName: FullyQualifiedName, targetTypeName: FullyQualifiedName ): ReferentialConstraint[] { return referentialConstraints.reduce((outArray: ReferentialConstraint[], refCon: EDMX.ReferentialConstraint) => { outArray.push({ sourceTypeName: sourceTypeName, sourceProperty: refCon._attributes.Property, targetTypeName: targetTypeName, targetProperty: refCon._attributes.ReferencedProperty }); return outArray; }, []); } /** * Parse the v2 referential constraint from the EDMX into an object structure. * * @param referentialConstraints the v2 referential constraint data * @param associationEnds the associations of the service to find the involved EntitySets. * @returns the object representation of the referential constraint. */ function parseV2ReferentialConstraint( referentialConstraints: EDMX.V2ReferentialConstraint[], associationEnds: RawAssociationEnd[] ) { return referentialConstraints.reduce((outArray: ReferentialConstraint[], refCon) => { let sourceEnd = associationEnds.find((assEnd) => assEnd.role === refCon.Principal._attributes.Role); let targetEnd = associationEnds.find((assEnd) => assEnd.role === refCon.Dependent._attributes.Role); if (sourceEnd !== undefined && targetEnd !== undefined) { let sourceProperties = ensureArray(refCon.Principal.PropertyRef); let targetProperties = ensureArray(refCon.Dependent.PropertyRef); if (sourceEnd.multiplicity !== '1') { targetEnd = sourceEnd; sourceEnd = associationEnds.find( (assEnd) => assEnd.role === refCon.Dependent._attributes.Role ) as RawAssociationEnd; // We're reversing them so it will exist for sure targetProperties = sourceProperties; sourceProperties = ensureArray(refCon.Dependent.PropertyRef); } for (const sourceProperty of sourceProperties) { const propertyIndex = sourceProperties.indexOf(sourceProperty); outArray.push({ sourceTypeName: sourceEnd.type, sourceProperty: sourceProperty._attributes.Name, targetTypeName: targetEnd.type, targetProperty: targetProperties[propertyIndex]._attributes.Name }); } } return outArray; }, []); } /** * Parse the EDMX representation of the navigation property in an object structure. * * @param navigationProperties the navigation property definition * @param currentEntityType the current entity type * @param entityTypeFQN the name of the current entity type * @param annotationLists the list of annotations * @returns all the navigation properties from the service */ function parseNavigationProperties( navigationProperties: EDMX.NavigationProperty[], currentEntityType: EDMX.EntityType | EDMX.ComplexType, entityTypeFQN: FullyQualifiedName, annotationLists: AnnotationList[] ): (RawV2NavigationProperty | RawV4NavigationProperty)[] { return navigationProperties.reduce( (outArray: (RawV2NavigationProperty | RawV4NavigationProperty)[], navigationProperty) => { // V4 const attributes: EDMX.NavigationPropertyAttributesV4 | EDMX.NavigationPropertyAttributesV2 = navigationProperty._attributes; if (isV4NavProperty(attributes)) { const matches = attributes.Type.match(collectionRegexp); const isCollection = matches !== null; const typeName = matches ? matches[1] : attributes.Type; outArray.push({ _type: 'NavigationProperty', name: attributes.Name, fullyQualifiedName: `${entityTypeFQN}/${attributes.Name}`, partner: attributes.Partner, containsTarget: attributes.ContainsTarget === 'true', isCollection, targetTypeName: typeName, referentialConstraint: parseReferentialConstraint( ensureArray(navigationProperty.ReferentialConstraint), currentEntityType._attributes.Name, typeName ) }); } else { // V2 const { Relationship, ToRole, FromRole } = attributes; outArray.push({ _type: 'NavigationProperty', name: attributes.Name, fullyQualifiedName: `${entityTypeFQN}/${attributes.Name}`, relationship: Relationship, toRole: ToRole, fromRole: FromRole }); const v2Annotations = convertV2Annotations( attributes as V2annotationsSupport, 'NavigationProperty', attributes.Name ); if (v2Annotations.length > 0) { annotationLists.push(createAnnotationList(`${entityTypeFQN}/${attributes.Name}`, v2Annotations)); } } return outArray; }, [] ); } function parseAssociationSets(associations: EDMX.AssociationSet[], namespace: string): RawAssociationSet[] { return associations.map((association) => { const associationFQN = `${namespace}.${association._attributes.Name}`; const associationEnd: RawAssociationSetEnd[] = ensureArray(association.End).map( (endValue: EDMX.AssociationSetEnd) => { return { entitySet: endValue._attributes.EntitySet, role: endValue._attributes.Role }; } ); return { fullyQualifiedName: associationFQN, name: association._attributes.Name, association: association._attributes.Association, associationEnd: associationEnd }; }); } function parseAssociations(associations: EDMX.Association[], namespace: string): RawAssociation[] { return associations.map((association) => { const associationFQN = `${namespace}.${association._attributes.Name}`; const associationEnd: RawAssociationEnd[] = ensureArray(association.End).map( (endValue: EDMX.AssociationEnd) => { return { type: endValue._attributes.Type, role: endValue._attributes.Role, multiplicity: endValue._attributes.Multiplicity }; } ); return { fullyQualifiedName: associationFQN, name: association._attributes.Name, associationEnd: associationEnd, referentialConstraints: parseV2ReferentialConstraint( ensureArray(association.ReferentialConstraint), associationEnd ) }; }); } function parseEntityTypes( entityTypes: EDMX.EntityType[], annotationLists: AnnotationList[], namespace: string ): RawEntityType[] { return entityTypes.reduce((outArray: RawEntityType[], entityType) => { const entityKeyNames = entityType.Key ? getEntityTypeKeys(ensureArray(entityType.Key.PropertyRef)) : []; const entityTypeFQN = `${namespace}.${entityType._attributes.Name}`; const { entityProperties, entityKeys } = parseProperties( ensureArray(entityType.Property), entityKeyNames, entityTypeFQN, annotationLists ); const navigationProperties = parseNavigationProperties( ensureArray(entityType.NavigationProperty), entityType, entityTypeFQN, annotationLists ); const outEntityType: RawEntityType = { _type: 'EntityType', name: entityType._attributes.Name, fullyQualifiedName: entityTypeFQN, keys: entityKeys, entityProperties, actions: {}, navigationProperties: navigationProperties }; const v2Annotations = convertV2Annotations( entityType._attributes as V2annotationsSupport, 'EntityType', entityType._attributes.Name ); if (v2Annotations.length > 0) { annotationLists.push(createAnnotationList(outEntityType.fullyQualifiedName, v2Annotations)); } outArray.push(outEntityType); return outArray; }, []); } function parseComplexTypes( complexTypes: EDMX.ComplexType[], annotationLists: AnnotationList[], namespace: string ): RawComplexType[] { return complexTypes.reduce((outArray: RawComplexType[], complexType) => { const complexTypeFQN = `${namespace}.${complexType._attributes.Name}`; const { entityProperties } = parseProperties( ensureArray(complexType.Property), [], complexTypeFQN, annotationLists ); const navigationProperties = parseNavigationProperties( ensureArray(complexType.NavigationProperty), complexType, complexTypeFQN, annotationLists ); outArray.push({ _type: 'ComplexType', name: complexType._attributes.Name, fullyQualifiedName: complexTypeFQN, properties: entityProperties, navigationProperties }); return outArray; }, []); } function parseTypeDefinitions(typeDefinitions: EDMX.TypeDefinition[], namespace: string): RawTypeDefinition[] { return typeDefinitions.reduce((outArray: RawTypeDefinition[], typeDefinition) => { const typeDefinitionFQN = `${namespace}.${typeDefinition._attributes.Name}`; outArray.push({ _type: 'TypeDefinition', name: typeDefinition._attributes.Name, fullyQualifiedName: typeDefinitionFQN, underlyingType: typeDefinition._attributes.UnderlyingType }); return outArray; }, []); } function parseEntitySets( entitySets: EDMX.EntitySet[], namespace: string, entityContainerName: string, annotationLists: AnnotationList[] ): RawEntitySet[] { const outEntitySets: RawEntitySet[] = entitySets.map((entitySet) => { const outEntitySet: RawEntitySet = { _type: 'EntitySet', name: entitySet._attributes.Name, entityTypeName: entitySet._attributes.EntityType, navigationPropertyBinding: {}, fullyQualifiedName: `${namespace}.${entityContainerName}/${entitySet._attributes.Name}` }; const v2Annotations = convertV2Annotations( entitySet._attributes as V2annotationsSupport, 'EntitySet', entitySet._attributes.Name ); if (v2Annotations.length > 0) { annotationLists.push(createAnnotationList(outEntitySet.fullyQualifiedName, v2Annotations)); } return outEntitySet; }); entitySets.forEach((entitySet) => { const currentOutEntitySet = outEntitySets.find( (outEntitySet) => outEntitySet.name === entitySet._attributes.Name ); if (currentOutEntitySet) { ensureArray(entitySet.NavigationPropertyBinding).forEach((navPropertyBinding) => { const currentTargetEntitySet = outEntitySets.find( (outEntitySet) => outEntitySet.name === navPropertyBinding._attributes.Target ); if (currentTargetEntitySet) { currentOutEntitySet.navigationPropertyBinding[navPropertyBinding._attributes.Path] = currentTargetEntitySet; } }); } }); return outEntitySets; } function parseSingletons( singletons: EDMX.Singleton[], namespace: string, entityContainerName: string, annotationLists: AnnotationList[] ): RawSingleton[] { const outSingletons: RawSingleton[] = singletons.map((singleton) => { const outSingleton: RawSingleton = { _type: 'Singleton', name: singleton._attributes.Name, entityTypeName: singleton._attributes.Type, nullable: singleton._attributes.Nullable !== 'false', navigationPropertyBinding: {}, fullyQualifiedName: `${namespace}.${entityContainerName}/${singleton._attributes.Name}` }; const v2Annotations = convertV2Annotations( singleton._attributes as V2annotationsSupport, 'Singleton', singleton._attributes.Name ); if (v2Annotations.length > 0) { annotationLists.push(createAnnotationList(outSingleton.fullyQualifiedName, v2Annotations)); } return outSingleton; }); singletons.forEach((singleton) => { const currentOutSingleton = outSingletons.find( (outSingleton) => outSingleton.name === singleton._attributes.Name ); if (currentOutSingleton) { ensureArray(singleton.NavigationPropertyBinding).forEach((navPropertyBinding) => { const currentTargetSingleton = outSingletons.find( (outSingleton) => outSingleton.name === navPropertyBinding._attributes.Target ); if (currentTargetSingleton) { currentOutSingleton.navigationPropertyBinding[navPropertyBinding._attributes.Path] = currentTargetSingleton; } }); } }); return outSingletons; } function parseActions( actions: (EDMX.Action | EDMX.Function)[], namespace: string, isFunction: boolean = false ): RawAction[] { return actions.map((action) => { let actionEntityType: string = `${ensureArray(action.Parameter) .filter((param) => param._attributes.Name === action._attributes.EntitySetPath) .map((param) => param._attributes.Type)}`; const isBound: boolean = action._attributes.IsBound === 'true'; let actionFQN: string = `${action._attributes.Name}()`; if (isBound) { if (!actionEntityType) { actionEntityType = `${ensureArray(action.Parameter)[0]._attributes.Type}`; } actionFQN = `${namespace}.${action._attributes.Name}(${actionEntityType})`; } return { _type: 'Action', name: action._attributes.Name, isBound: isBound, sourceType: actionEntityType, fullyQualifiedName: actionFQN, isFunction: isFunction, parameters: ensureArray(action.Parameter).map((param) => { return { _type: 'ActionParameter', fullyQualifiedName: `${actionFQN}/${param._attributes.Name}`, name: `${param._attributes.Name}`, type: param._attributes.Type, isEntitySet: param._attributes.Name === action._attributes.EntitySetPath }; }), returnType: action.ReturnType ? action.ReturnType._attributes.Type : '' }; }); } function parseFunctionImport( actions: EDMX.FunctionImport[], entitySets: RawEntitySet[], namespace: string ): RawAction[] { return actions.map((action) => { const targetEntitySet = entitySets.find((et) => et.name === action._attributes.EntitySet); const actionFQN: string = `${namespace}/${action._attributes.Name}()`; return { _type: 'Action', name: action._attributes.Name, isBound: false, sourceType: targetEntitySet ? targetEntitySet.entityTypeName : '', fullyQualifiedName: actionFQN, isFunction: false, parameters: ensureArray(action.Parameter).map((param) => { return { _type: 'ActionParameter', name: param._attributes.Name, fullyQualifiedName: `${actionFQN}/${param._attributes.Name}`, type: param._attributes.Type, isEntitySet: false }; }), returnType: action._attributes.ReturnType ? action._attributes.ReturnType : '' }; }); } function parsePropertyValues( propertyValues: EDMX.PropertyValue[], currentTarget: string, annotationsLists: AnnotationList[] ): PropertyValue[] { return propertyValues.map((propertyValue) => { // I don't care about the first part but need the rest and the spread operator // eslint-disable-next-line @typescript-eslint/no-unused-vars const { Annotation, _attributes, ...properties } = propertyValue; const outPropertyValue: Partial = {}; if (_attributes) { const attributeKey: keyof EDMX.InlineExpression | undefined = Object.keys(_attributes).find( (keyName) => keyName !== 'Property' ) as keyof EDMX.InlineExpression | undefined; outPropertyValue.name = _attributes.Property; const currentPropertyTarget = `${currentTarget}/${outPropertyValue.name}`; if (properties && Object.keys(properties).length > 0) { outPropertyValue.value = parseExpression(properties, currentPropertyTarget, annotationsLists); } else if (attributeKey) { outPropertyValue.value = parseInlineExpression( { [attributeKey]: _attributes[attributeKey] }, currentPropertyTarget, annotationsLists ); } if (propertyValue.Annotation) { const propertyAnnotations = parseAnnotations( ensureArray(propertyValue.Annotation), currentPropertyTarget, annotationsLists ); if (propertyAnnotations && propertyAnnotations.length > 0) { annotationsLists.push(createAnnotationList(currentPropertyTarget, propertyAnnotations)); } } } return outPropertyValue as PropertyValue; }); } function parseRecord( record: EDMX.RecordExpression, currentTarget: string, annotationsLists: AnnotationList[] ): AnnotationRecord { const recordAnnotations = parseAnnotations(ensureArray(record.Annotation), currentTarget, annotationsLists); const outRecord: AnnotationRecord = { type: record._attributes ? unalias(record._attributes.Type) : undefined, propertyValues: parsePropertyValues(ensureArray(record.PropertyValue), currentTarget, annotationsLists) }; if (recordAnnotations && recordAnnotations.length > 0) { outRecord.annotations = recordAnnotations; } return outRecord; } /** * Type Guard for the type of the current collection. * * @param annotation * @param propertyNameToCheck * @returns true if the collection if of the right type */ function isExpressionOfType(annotation: any, propertyNameToCheck: string): annotation is K { return annotation[propertyNameToCheck] != null; } function parseModelPath( propertyPath: EDMX.ModelPath, modelPathType: 'AnnotationPath' | 'PropertyPath' | 'NavigationPropertyPath' | 'Path' ): PropertyPathExpression | NavigationPropertyPathExpression | AnnotationPathExpression | PathExpression { switch (modelPathType) { case 'NavigationPropertyPath': return { type: 'NavigationPropertyPath', NavigationPropertyPath: propertyPath._text }; case 'PropertyPath': return { type: 'PropertyPath', PropertyPath: propertyPath._text }; case 'AnnotationPath': return { type: 'AnnotationPath', AnnotationPath: propertyPath._text }; case 'Path': return { type: 'Path', Path: propertyPath._text }; } } function parseCollection( collection: EDMX.CollectionExpression, currentTarget: string, annotationsLists: AnnotationList[] ): | AnnotationRecord[] | string[] | PropertyPathExpression[] | PathExpression[] | AnnotationPathExpression[] | NavigationPropertyPathExpression[] { if (isExpressionOfType(collection, 'Record')) { const recordArray = ensureArray(collection.Record).map((record, recordIndex) => parseRecord(record, currentTarget + '/' + recordIndex, annotationsLists) ); (recordArray as any).type = 'Record'; return recordArray; } else if (isExpressionOfType(collection, 'PropertyPath')) { const propertyPathArray = ensureArray(collection.PropertyPath).map( (propertyPath) => parseModelPath(propertyPath, 'PropertyPath') as PropertyPathExpression ); (propertyPathArray as any).type = 'PropertyPath'; return propertyPathArray; } else if (isExpressionOfType(collection, 'NavigationPropertyPath')) { const navPropertyPathArray = ensureArray(collection.NavigationPropertyPath).map( (navPropertyPath) => parseModelPath(navPropertyPath, 'NavigationPropertyPath') as NavigationPropertyPathExpression ); (navPropertyPathArray as any).type = 'NavigationPropertyPath'; return navPropertyPathArray; } else if (isExpressionOfType(collection, 'String')) { const stringArray = ensureArray(collection.String).map((stringValue) => stringValue._text); (stringArray as any).type = 'String'; return stringArray; } else if (isExpressionOfType(collection, 'AnnotationPath')) { const annotationPathArray = ensureArray(collection.AnnotationPath).map( (annotationPath) => parseModelPath(annotationPath, 'AnnotationPath') as AnnotationPathExpression ); (annotationPathArray as any).type = 'AnnotationPath'; return annotationPathArray; } else if (isExpressionOfType(collection, 'Path')) { const pathArray = ensureArray(collection.Path).map( (pathDefinition) => parseModelPath(pathDefinition, 'Path') as PathExpression ); (pathArray as any).type = 'Path'; return pathArray; } else if (isExpressionOfType(collection, 'If')) { const stringArray = ensureArray(collection.If).map((stringValue) => stringValue._text); (stringArray as any).type = 'String'; return stringArray; } else if (Object.keys(collection).length === 0) { return []; } else { console.error(`Cannot parse ${JSON.stringify(collection)}, collection type is not supported`); } return []; } function parseApply(applyExpression: EDMX.ApplyExpression): Apply { return applyExpression; } function parseInlineExpression( expression: EDMX.InlineExpression, currentTarget: string, annotationsLists: AnnotationList[] ): Expression { const expressionKeys = Object.keys(expression); if (expressionKeys.length > 1) { throw new Error(`Too many expressions defined on a single object ${JSON.stringify(expression)}`); } const expressionKey = expressionKeys[0]; switch (expressionKey) { case 'String': return { type: 'String', String: expression[expressionKey] as string }; case 'Bool': return { type: 'Bool', Bool: expression.Bool === 'true' }; case 'Decimal': return { type: 'Decimal', Decimal: parseFloat(expression.Decimal as string) }; case 'Date': return { type: 'Date', Date: expression.Date as string }; case 'Int': return { type: 'Int', Int: parseInt(expression.Int as string, 10) }; case 'Path': return { type: 'Path', Path: expression.Path as string }; case 'PropertyPath': return { type: 'PropertyPath', PropertyPath: expression.PropertyPath as string }; case 'AnnotationPath': return { type: 'AnnotationPath', AnnotationPath: expression.AnnotationPath as string }; case 'NavigationPropertyPath': return { type: 'NavigationPropertyPath', NavigationPropertyPath: expression.NavigationPropertyPath as string }; case 'EnumMember': return { type: 'EnumMember', EnumMember: expression[expressionKey] as string }; case 'Collection': return { type: 'Collection', Collection: parseCollection( expression.Collection as EDMX.CollectionExpression, currentTarget, annotationsLists ) }; case 'Record': return { type: 'Record', Record: parseRecord(expression.Record as EDMX.RecordExpression, currentTarget, annotationsLists) }; case 'Apply': return { type: 'Apply', Apply: parseApply(expression.Apply) }; case 'Null': return { type: 'Null' }; default: console.error('Unsupported inline expression type ' + expressionKey); return { type: 'Unknown' }; } } function parseExpression( expression: EDMX.Expression<{}>, currentTarget: string, annotationsLists: AnnotationList[] ): Expression { const expressionKeys = Object.keys(expression); if (expressionKeys.length > 1) { throw new Error(`Too many expressions defined on a single object ${JSON.stringify(expression)}`); } const expressionKey = expressionKeys[0]; switch (expressionKey) { case 'String': return { type: 'String', String: (expression[expressionKey] as any)._text as string }; case 'Bool': return { type: 'Bool', Bool: (expression.Bool as any)._text === 'true' }; case 'Int': return { type: 'Int', Int: parseInt((expression.Int as any)._text as string, 10) }; case 'Decimal': return { type: 'Decimal', Decimal: parseFloat((expression.Decimal as any)._text as string) }; case 'Path': return { type: 'Path', Path: (expression.Path as EDMX.InstancePath)._text }; case 'PropertyPath': return { type: 'PropertyPath', PropertyPath: (expression.PropertyPath as EDMX.ModelPath)._text }; case 'NavigationPropertyPath': return { type: 'NavigationPropertyPath', NavigationPropertyPath: (expression.NavigationPropertyPath as EDMX.ModelPath)._text }; case 'AnnotationPath': return { type: 'AnnotationPath', AnnotationPath: (expression.AnnotationPath as EDMX.ModelPath)._text }; case 'EnumMember': return { type: 'EnumMember', EnumMember: (expression[expressionKey] as any)._text as string }; case 'Collection': return { type: 'Collection', Collection: parseCollection( expression.Collection as EDMX.CollectionExpression, currentTarget, annotationsLists ) }; case 'Record': return { type: 'Record', Record: parseRecord(expression.Record as EDMX.RecordExpression, currentTarget, annotationsLists) }; case 'Apply': return { type: 'Apply', Apply: parseApply(expression.Apply) }; case 'Null': return { type: 'Null' }; default: console.error('Unsupported expression type ' + expressionKey); return { type: 'Unknown' }; } } function parseAnnotation( annotation: EDMX.Annotation, currentTarget: string, annotationsLists: AnnotationList[] ): RawAnnotation { const { Term, Qualifier, ...others } = annotation._attributes; const outAnnotation: Partial = { term: unalias(Term), qualifier: Qualifier }; let currentAnnotationTarget = `${currentTarget}@${unalias(Term)}`; if (Qualifier !== '' && Qualifier !== undefined) { currentAnnotationTarget += `#${Qualifier}`; } if (others && Object.keys(others).length > 0) { outAnnotation.value = parseInlineExpression(others, currentAnnotationTarget, annotationsLists); } if (annotation.Annotation) { const annotationAnnotations = parseAnnotations( ensureArray(annotation.Annotation), currentAnnotationTarget, annotationsLists ); if (annotationAnnotations && annotationAnnotations.length > 0) { outAnnotation.annotations = annotationAnnotations; } } const keys = Object.keys(annotation).filter((keyValue) => keyValue !== '_attributes' && keyValue !== 'Annotation'); if (isExpressionOfType(annotation, 'Record')) { outAnnotation.record = parseRecord(annotation.Record, currentAnnotationTarget, annotationsLists); } else if (isExpressionOfType(annotation, 'Collection')) { outAnnotation.collection = parseCollection(annotation.Collection, currentAnnotationTarget, annotationsLists); } else if (keys.length === 1) { outAnnotation.value = parseExpression( { [keys[0]]: (annotation as any)[keys[0]] }, currentAnnotationTarget, annotationsLists ); } else if (keys.length > 1) { console.error(`Cannot parse ${JSON.stringify(annotation)}, expression type is not supported`); } return outAnnotation as RawAnnotation; } function parseAnnotations( annotations: EDMX.Annotation[], currentTarget: string, annotationsLists: AnnotationList[] ): RawAnnotation[] { return annotations.map((annotation) => parseAnnotation(annotation, currentTarget, annotationsLists)); } function createAnnotationList(target: string, annotations: RawAnnotation[]): AnnotationList { return { target: target, annotations: annotations }; } /** * @param annotationLists * @param annotationsLists */ function parseAnnotationLists(annotationLists: EDMX.AnnotationList[], annotationsLists: AnnotationList[]): void { annotationLists .filter((annotationList) => annotationList._attributes !== undefined) .forEach((annotationList) => { annotationsLists.push( createAnnotationList( annotationList._attributes.Target, parseAnnotations( ensureArray(annotationList.Annotation), annotationList._attributes.Target, annotationsLists ) ) ); }); } function parseSchema(edmSchema: EDMX.Schema, identification: string): RawSchema { const namespace = edmSchema._attributes.Namespace; const annotations: AnnotationList[] = []; const entityTypes = parseEntityTypes(ensureArray(edmSchema.EntityType), annotations, namespace); const complexTypes = parseComplexTypes(ensureArray(edmSchema.ComplexType), annotations, namespace); const typeDefinitions = parseTypeDefinitions(ensureArray(edmSchema.TypeDefinition), namespace); let entitySets: RawEntitySet[] = []; let singletons: RawSingleton[] = []; let associationSets: RawAssociationSet[] = []; let entityContainer: RawEntityContainer = { _type: 'EntityContainer', fullyQualifiedName: '' }; let actions: RawAction[] = []; if (edmSchema.EntityContainer) { entitySets = parseEntitySets( ensureArray(edmSchema.EntityContainer.EntitySet), namespace, edmSchema.EntityContainer._attributes.Name, annotations ); singletons = parseSingletons( ensureArray(edmSchema.EntityContainer.Singleton), namespace, edmSchema.EntityContainer._attributes.Name, annotations ); associationSets = parseAssociationSets(ensureArray(edmSchema.EntityContainer.AssociationSet), namespace); entityContainer = { _type: 'EntityContainer', name: edmSchema.EntityContainer._attributes.Name, fullyQualifiedName: `${namespace}.${edmSchema.EntityContainer._attributes.Name}` }; actions = actions.concat( parseFunctionImport( ensureArray(edmSchema.EntityContainer.FunctionImport), entitySets, entityContainer.fullyQualifiedName ) ); } actions = actions.concat(parseActions(ensureArray(edmSchema.Action), namespace)); actions = actions.concat(parseActions(ensureArray(edmSchema.Function), namespace, true)); const associations = parseAssociations(ensureArray(edmSchema.Association), namespace); parseAnnotationLists(ensureArray(edmSchema.Annotations), annotations); const annotationMap: { [id: string]: AnnotationList[] } = {}; annotationMap[identification] = annotations; return { associations, associationSets, annotations: annotationMap, entityContainer, namespace: namespace, entitySets, singletons, complexTypes, typeDefinitions, actions, entityTypes }; } function parseReferences(references: EDMX.Reference[], schemas: EDMX.Schema[]): Reference[] { const outReferences: Reference[] = references.reduce((referencesArray: Reference[], reference: EDMX.Reference) => { const includes = ensureArray(reference['edmx:Include']); includes.forEach((include: EDMX.ReferenceInclude) => { referencesArray.push({ uri: reference._attributes.Uri, alias: include._attributes.Alias, namespace: include._attributes.Namespace }); }); return referencesArray; }, []); schemas.forEach((schema) => { if (schema && schema._attributes.Alias) { outReferences.push({ uri: '', alias: schema._attributes.Alias, namespace: schema._attributes.Namespace }); } }); return outReferences; } let referenceMap: Record = {}; function unalias(aliasedValue: string | undefined): string | undefined { if (!aliasedValue) { return aliasedValue; } const [alias, value] = aliasedValue.split('.'); const reference = referenceMap[alias]; if (reference) { return `${reference.namespace}.${value}`; } else { // Try to see if it's an annotation Path like to_SalesOrder/@UI.LineItem if (aliasedValue.indexOf('@') !== -1) { const [preAlias, postAlias] = aliasedValue.split('@'); return `${preAlias}@${unalias(postAlias)}`; } else { return aliasedValue; } } } function mergeSchemas(schemas: RawSchema[]): RawSchema { if (schemas.length === 1) { return schemas[0]; } const associations = schemas.reduce((associationsToReduce: RawAssociation[], schema) => { return associationsToReduce.concat(schema.associations); }, []); const associationSets = schemas.reduce((associationSetsToReduce: RawAssociationSet[], schema) => { return associationSetsToReduce.concat(schema.associationSets); }, []); const entitySets = schemas.reduce((entitySetsToReduce: RawEntitySet[], schema) => { return entitySetsToReduce.concat(schema.entitySets); }, []); const singletons = schemas.reduce((singletonsToReduce: RawSingleton[], schema) => { return singletonsToReduce.concat(schema.singletons); }, []); const entityTypes = schemas.reduce((entityTypesToReduce: RawEntityType[], schema) => { return entityTypesToReduce.concat(schema.entityTypes); }, []); const actions = schemas.reduce((actionsToReduce: RawAction[], schema) => { return actionsToReduce.concat(schema.actions); }, []); const complexTypes = schemas.reduce((complexTypesToReduces: RawComplexType[], schema) => { return complexTypesToReduces.concat(schema.complexTypes); }, []); const typeDefinitions = schemas.reduce((typeDefinitionsToReduce: RawTypeDefinition[], schema) => { return typeDefinitionsToReduce.concat(schema.typeDefinitions); }, []); let annotationMap = {}; schemas.forEach((schema) => { annotationMap = Object.assign(annotationMap, schema.annotations); }); let entityContainer!: RawEntityContainer; let namespace!: string; schemas.forEach((schema) => { if (schema.entityContainer && Object.keys(schema.entityContainer).length > 0) { entityContainer = schema.entityContainer; namespace = schema.namespace; } }); // V2 case entitySets.forEach((entitySet) => { const entityType = entityTypes.find( (rawEntityType) => rawEntityType.fullyQualifiedName === entitySet.entityTypeName ); entityType?.navigationProperties.forEach((navProp: any) => { const v2NavProp: RawV2NavigationProperty = navProp as RawV2NavigationProperty; const associationSet = associationSets.find((assoc) => assoc.association === v2NavProp.relationship); if (associationSet) { const associationEndEntitySets = associationSet.associationEnd.map( (associationEnd: RawAssociationSetEnd) => { return entitySets.find((rawEntitySet) => rawEntitySet.name === associationEnd.entitySet); } ); const targetEntitySet = associationEndEntitySets.find( (associationEntitySet: any) => associationEntitySet?.fullyQualifiedName !== entitySet.fullyQualifiedName ); if (targetEntitySet) { entitySet.navigationPropertyBinding[navProp.name] = targetEntitySet; } } }); }); entityTypes.forEach((entityType) => { entityType.navigationProperties.forEach((navProp: any) => { const v2NavProp: RawV2NavigationProperty = navProp as RawV2NavigationProperty; const association = associations.find((assoc) => assoc.fullyQualifiedName === v2NavProp.relationship); if (association && association.referentialConstraints) { v2NavProp.referentialConstraint = association.referentialConstraints; } }); }); return { associations, associationSets, annotations: annotationMap, entityContainer, namespace: namespace, entitySets, singletons, complexTypes, typeDefinitions, actions, entityTypes }; } /** * Parse an edmx file and return an object structure representing the service definition. * * @param xml {string} the original XML string * @param fileIdentification {string} a way to identify this file * @returns the parsed metadata definition */ export function parse(xml: string, fileIdentification: string = 'serviceFile'): RawMetadata { const jsonObj: EDMX.Edmx = xml2js(xml, { compact: true }) as EDMX.Edmx; const schemas: EDMX.Schema[] = ensureArray(jsonObj['edmx:Edmx']['edmx:DataServices'].Schema); const references = parseReferences(ensureArray(jsonObj['edmx:Edmx']['edmx:Reference']), schemas); referenceMap = references.reduce((map: Record, reference) => { map[reference.alias] = reference; return map; }, {}); const parsedSchemas = schemas.map((schema) => { return parseSchema(schema, fileIdentification); }); const edmxDocument: RawMetadata = new RawMetadataInstance( fileIdentification, jsonObj['edmx:Edmx']._attributes.Version, mergeSchemas(parsedSchemas), references ); return edmxDocument; }