import type { Action, ActionImport, ActionParameter, Annotation, AnnotationPath, AnnotationRecord, ArrayWithIndex, BaseNavigationProperty, ComplexType, ConvertedMetadata, EntityContainer, EntitySet, EntityType, EnumMember, EnumType, Expression, FullyQualifiedName, NavigationProperty, NavigationPropertyPath, PathAnnotationExpression, Property, PropertyPath, RawAction, RawActionImport, RawAnnotation, RawComplexType, RawEntityContainer, RawEntitySet, RawEntityType, RawEnumMember, RawEnumType, RawMetadata, RawNavigationPropertyBinding, RawProperty, RawSchema, RawSingleton, RawTypeDefinition, RawV2NavigationProperty, RawV4NavigationProperty, RemoveAnnotationAndType, ResolutionTarget, ServiceObjectAndAnnotation, Singleton, TypeDefinition } from '@sap-ux/vocabularies-types'; import { CommonAnnotationTerms } from '@sap-ux/vocabularies-types/vocabularies/Common'; import { VocabularyReferences } from '@sap-ux/vocabularies-types/vocabularies/VocabularyReferences'; import type { IResettable } from './utils'; import { addGetByValue, alias, Decimal, Double, EnumIsFlag, initialSymbol, lazy, mergeAnnotations, mergeRawMetadata, splitAtFirst, splitAtLast, substringBeforeFirst, substringBeforeLast, TermToTypes, unalias } from './utils'; /** * Symbol to extend an annotation with the reference to its target. */ const ANNOTATION_TARGET = Symbol('Annotation Target'); export const CONVERTER_ROOT = Symbol('Converter Root'); /** * Append an object to the list of visited objects if it is different from the last object in the list. * * @param objectPath The list of visited objects * @param visitedObject The object * @returns The list of visited objects */ function appendObjectPath(objectPath: any[], visitedObject: any): any[] { if (objectPath[objectPath.length - 1] !== visitedObject) { objectPath.push(visitedObject); } return objectPath; } /** * Resolves a (possibly relative) path. * * @param converter Converter * @param startElement The starting point in case of relative path resolution * @param path The path to resolve * @param annotationsTerm Only for error reporting: The annotation term * @returns An object containing the resolved target and the elements that were visited while getting to the target. */ function resolveTarget( converter: Converter, startElement: any, path: string | undefined, annotationsTerm?: string ): ResolutionTarget { if (path === undefined) { return { target: undefined, objectPath: [], messages: [] }; } // absolute paths always start at the entity container if (path.startsWith('/')) { path = path.substring(1); startElement = undefined; // will resolve to the entity container (see below) } const pathSegments = path.split('/').reduce((targetPath, segment) => { if (segment.includes('@')) { // Separate out the annotation const [pathPart, annotationPart] = splitAtFirst(segment, '@'); targetPath.push(pathPart); targetPath.push(`@${annotationPart}`); } else { targetPath.push(segment); } return targetPath; }, [] as string[]); // determine the starting point for the resolution if (startElement === undefined) { // no starting point given: start at the entity container if ( pathSegments[0].startsWith(`${converter.rawSchema.namespace}.`) && pathSegments[0] !== converter.getConvertedEntityContainer()?.fullyQualifiedName ) { // We have a fully qualified name in the path that is not the entity container. startElement = converter.getConvertedEntityType(pathSegments[0]) ?? converter.getConvertedComplexType(pathSegments[0]) ?? converter.getConvertedAction(pathSegments[0]) ?? converter.getConvertedAction(`${pathSegments[0]}()`); // unbound action pathSegments.shift(); // Let's remove the first path element } else { startElement = converter.getConvertedEntityContainer(); } } else if (startElement[ANNOTATION_TARGET] !== undefined) { // annotation: start at the annotation target startElement = startElement[ANNOTATION_TARGET]; } else if (startElement._type === 'Property') { // property: start at the entity type or complex type the property belongs to const parentElementFQN = substringBeforeFirst(startElement.fullyQualifiedName, '/'); startElement = converter.getConvertedEntityType(parentElementFQN) ?? converter.getConvertedComplexType(parentElementFQN); } const result = pathSegments.reduce( (current: ResolutionTarget, segment: string) => { const error = (message: string) => { current.messages.push({ message }); current.target = undefined; return current; }; if (current.target === undefined) { return current; } current.objectPath = appendObjectPath(current.objectPath, current.target); // Annotation if (segment.startsWith('@') && segment !== '@$ui5.overload') { const [vocabularyAlias, term] = converter.splitTerm(segment); const annotation = current.target.annotations[vocabularyAlias.substring(1)]?.[term]; if (annotation !== undefined) { current.target = annotation; return current; } return error( `Annotation '${segment.substring(1)}' not found on ${current.target._type} '${ current.target.fullyQualifiedName }'` ); } // $Path / $AnnotationPath syntax if (current.target.$target) { let subPath: string | undefined; if (segment === '$AnnotationPath') { subPath = current.target.value; } else if (segment === '$Path') { subPath = current.target.path; } if (subPath !== undefined) { const subTarget = resolveTarget(converter, current.target[ANNOTATION_TARGET], subPath); subTarget.objectPath.forEach((visitedSubObject: any) => { if (!current.objectPath.includes(visitedSubObject)) { current.objectPath = appendObjectPath(current.objectPath, visitedSubObject); } }); current.target = subTarget.target; current.objectPath = appendObjectPath(current.objectPath, current.target); return current; } } // traverse based on the element type switch (current.target?._type) { case 'Schema': // next element: EntityType, ComplexType, Action, EntityContainer ? break; case 'EntityContainer': { const thisElement = current.target as EntityContainer; if (segment === '' || converter.unalias(segment) === thisElement.fullyQualifiedName) { return current; } // next element: EntitySet, Singleton or ActionImport? const nextElement: EntitySet | Singleton | ActionImport | undefined = thisElement.entitySets.by_name(segment) ?? thisElement.singletons.by_name(segment) ?? thisElement.actionImports.by_name(segment); if (nextElement) { current.target = nextElement; return current; } } break; case 'EntitySet': case 'Singleton': { const thisElement = current.target as EntitySet | Singleton; if (segment === '' || segment === '$Type') { // Empty Path after an EntitySet or Singleton means EntityType current.target = thisElement.entityType; return current; } if (segment === '$') { return current; } if (segment === '$NavigationPropertyBinding') { const navigationPropertyBindings = thisElement.navigationPropertyBinding; current.target = navigationPropertyBindings; return current; } // continue resolving at the EntitySet's or Singleton's type const result = resolveTarget(converter, thisElement.entityType, segment); current.target = result.target; current.objectPath = result.objectPath.reduce(appendObjectPath, current.objectPath); return current; } case 'EntityType': { const thisElement = current.target as EntityType; if (segment === '' || segment === '$Type') { return current; } const property = thisElement.entityProperties.by_name(segment); if (property) { current.target = property; return current; } const navigationProperty = thisElement.navigationProperties.by_name(segment); if (navigationProperty) { current.target = navigationProperty; return current; } const actionName = substringBeforeFirst(converter.unalias(segment), '('); const action = thisElement.actions[actionName]; if (action) { current.target = action; return current; } } break; case 'ActionImport': { // continue resolving at the Action const result = resolveTarget(converter, current.target.action, segment); current.target = result.target; current.objectPath = result.objectPath.reduce(appendObjectPath, current.objectPath); return current; } case 'Action': { const thisElement = current.target as Action; if (segment === '') { return current; } if (segment === '@$ui5.overload' || segment === '0') { return current; } if (segment === '$Parameter' && thisElement.isBound) { current.target = thisElement.parameters; return current; } const nextElement = thisElement.parameters[segment as any] ?? thisElement.parameters.find((param: ActionParameter) => param.name === segment); if (nextElement) { current.target = nextElement; return current; } break; } case 'Property': { const thisElement = current.target as Property; // Property or NavigationProperty of the ComplexType const type = thisElement.targetType as ComplexType | undefined; if (type !== undefined) { const property = type.properties.by_name(segment); if (property) { current.target = property; return current; } const navigationProperty = type.navigationProperties.by_name(segment); if (navigationProperty) { current.target = navigationProperty; return current; } } } break; case 'ActionParameter': const referencedType = (current.target as ActionParameter).typeReference; if (referencedType !== undefined) { const result = resolveTarget(converter, referencedType, segment); current.target = result.target; current.objectPath = result.objectPath.reduce(appendObjectPath, current.objectPath); return current; } break; case 'NavigationProperty': // continue at the NavigationProperty's target type const result = resolveTarget(converter, (current.target as NavigationProperty).targetType, segment); current.target = result.target; current.objectPath = result.objectPath.reduce(appendObjectPath, current.objectPath); return current; default: if (segment === '') { return current; } if (current.target[segment]) { current.target = current.target[segment]; current.objectPath = appendObjectPath(current.objectPath, current.target); return current; } } return error( `Element '${segment}' not found at ${current.target._type} '${current.target.fullyQualifiedName}'` ); }, { target: startElement, objectPath: [], messages: [] } ); // Diagnostics result.messages.forEach((message) => converter.logError(message.message)); if (!result.target) { if (annotationsTerm) { const annotationType = inferTypeFromTerm(converter, annotationsTerm, startElement.fullyQualifiedName); converter.logError( 'Unable to resolve the path expression: ' + '\n' + path + '\n' + '\n' + 'Hint: Check and correct the path values under the following structure in the metadata (annotation.xml file or CDS annotations for the application): \n\n' + '' + '\n' + '' + '\n' + '' ); } else { converter.logError( 'Unable to resolve the path expression: ' + path + '\n' + '\n' + 'Hint: Check and correct the path values under the following structure in the metadata (annotation.xml file or CDS annotations for the application): \n\n' + '' + '\n' + '' ); } } return result; } /** * Typeguard to check if the path contains an annotation. * * @param pathStr the path to evaluate * @returns true if there is an annotation in the path. */ function isAnnotationPath(pathStr: string): boolean { return pathStr.includes('@'); } type AnnotationValue = T & { [ANNOTATION_TARGET]: any; [CONVERTER_ROOT]: () => ConvertedMetadata }; function mapPropertyPath( converter: Converter, propertyPath: { type: 'PropertyPath'; PropertyPath: string }, fullyQualifiedName: FullyQualifiedName, currentTarget: any, currentTerm: string ) { const result: Omit, '$target' | typeof CONVERTER_ROOT> = { type: 'PropertyPath', value: propertyPath.PropertyPath, fullyQualifiedName: fullyQualifiedName, [ANNOTATION_TARGET]: currentTarget }; (result as AnnotationValue)[CONVERTER_ROOT] = () => converter.getConvertedOutput(); lazy( converter, result as AnnotationValue, '$target', () => resolveTarget(converter, currentTarget, propertyPath.PropertyPath, currentTerm).target ); return result as AnnotationValue; } function mapAnnotationPath( converter: Converter, annotationPath: { type: 'AnnotationPath'; AnnotationPath: string }, fullyQualifiedName: FullyQualifiedName, currentTarget: any, currentTerm: string ) { const result: Omit>, '$target' | typeof CONVERTER_ROOT> = { type: 'AnnotationPath', value: converter.unalias(annotationPath.AnnotationPath), fullyQualifiedName: fullyQualifiedName, [ANNOTATION_TARGET]: currentTarget }; (result as AnnotationValue>)[CONVERTER_ROOT] = () => converter.getConvertedOutput(); lazy( converter, result as AnnotationValue>, '$target', () => resolveTarget(converter, currentTarget, result.value, currentTerm).target ); return result as AnnotationValue>; } function mapNavigationPropertyPath( converter: Converter, navigationPropertyPath: { type: 'NavigationPropertyPath'; NavigationPropertyPath: string }, fullyQualifiedName: FullyQualifiedName, currentTarget: any, currentTerm: string ) { const result: Omit, '$target' | typeof CONVERTER_ROOT> = { type: 'NavigationPropertyPath', value: navigationPropertyPath.NavigationPropertyPath ?? '', fullyQualifiedName: fullyQualifiedName, [ANNOTATION_TARGET]: currentTarget }; (result as AnnotationValue)[CONVERTER_ROOT] = () => converter.getConvertedOutput(); lazy( converter, result as AnnotationValue, '$target', () => resolveTarget( converter, currentTarget, navigationPropertyPath.NavigationPropertyPath, currentTerm ).target ); return result as AnnotationValue; } function mapPath( converter: Converter, path: { type: 'Path'; Path: string }, fullyQualifiedName: FullyQualifiedName, currentTarget: any, currentTerm: string ) { const result: Omit>, '$target' | typeof CONVERTER_ROOT> = { type: 'Path', path: path.Path, fullyQualifiedName: fullyQualifiedName, getValue(): any { return undefined; // TODO: Required according to the type... }, [ANNOTATION_TARGET]: currentTarget }; (result as AnnotationValue>)[CONVERTER_ROOT] = () => converter.getConvertedOutput(); lazy( converter, result as AnnotationValue>, '$target', () => resolveTarget(converter, currentTarget, path.Path, currentTerm).target ); return result as AnnotationValue>; } function parseValue( converter: Converter, currentTarget: any, currentTerm: string, currentProperty: string, currentSource: string, propertyValue: Expression, valueFQN: string ) { if (propertyValue === undefined) { return undefined; } switch (propertyValue.type) { case 'String': return propertyValue.String; case 'Int': return propertyValue.Int; case 'Bool': return propertyValue.Bool; case 'Double': return Double(propertyValue.Double); case 'Decimal': return Decimal(propertyValue.Decimal); case 'Date': return propertyValue.Date; case 'EnumMember': const splitEnum = propertyValue.EnumMember.split(' ').map((enumValue) => { const unaliased = converter.unalias(enumValue) ?? ''; return alias(VocabularyReferences, unaliased); }); if (splitEnum[0] !== undefined && EnumIsFlag[substringBeforeFirst(splitEnum[0], '/')]) { return splitEnum; } return splitEnum[0]; case 'PropertyPath': return mapPropertyPath(converter, propertyValue, valueFQN, currentTarget, currentTerm); case 'NavigationPropertyPath': return mapNavigationPropertyPath(converter, propertyValue, valueFQN, currentTarget, currentTerm); case 'AnnotationPath': return mapAnnotationPath(converter, propertyValue, valueFQN, currentTarget, currentTerm); case 'Path': { if (isAnnotationPath(propertyValue.Path)) { // inline the target return resolveTarget(converter, currentTarget, converter.unalias(propertyValue.Path), currentTerm) .target; } else { return mapPath(converter, propertyValue, valueFQN, currentTarget, currentTerm); } } case 'Record': return parseRecord( converter, currentTerm, currentTarget, currentProperty, currentSource, propertyValue.Record, valueFQN ); case 'Collection': return parseCollection( converter, currentTarget, currentTerm, currentProperty, currentSource, propertyValue.Collection, valueFQN ); case 'Apply': case 'Null': case 'Not': case 'Eq': case 'Ne': case 'Gt': case 'Ge': case 'Lt': case 'Le': case 'If': case 'And': case 'Or': default: return propertyValue; } } /** * Infer the type of a term based on its type. * * @param converter Converter * @param annotationsTerm The annotation term * @param annotationTarget The annotation target * @param currentProperty The current property of the record * @returns The inferred type. */ function inferTypeFromTerm( converter: Converter, annotationsTerm: string, annotationTarget: string, currentProperty?: string ) { let targetType = (TermToTypes as any)[annotationsTerm]; if (currentProperty) { annotationsTerm = `${substringBeforeLast(annotationsTerm, '.')}.${currentProperty}`; targetType = (TermToTypes as any)[annotationsTerm]; } converter.logError( `The type of the record used within the term ${annotationsTerm} was not defined and was inferred as ${targetType}. Hint: If possible, try to maintain the Type property for each Record. ... ` ); return targetType; } function isDataFieldWithForAction(annotationContent: any) { return ( annotationContent.hasOwnProperty('Action') && (annotationContent.$Type === 'com.sap.vocabularies.UI.v1.DataFieldForAction' || annotationContent.$Type === 'com.sap.vocabularies.UI.v1.DataFieldWithAction') ); } function parseRecordType( converter: Converter, currentTerm: string, currentTarget: any, currentProperty: string | undefined, recordDefinition: AnnotationRecord ) { let targetType; if (!recordDefinition.type && currentTerm) { targetType = inferTypeFromTerm(converter, currentTerm, currentTarget.fullyQualifiedName, currentProperty); } else { targetType = converter.unalias(recordDefinition.type); } return targetType; } function parseRecord( converter: Converter, currentTerm: string, currentTarget: any, currentProperty: string | undefined, currentSource: string, annotationRecord: AnnotationRecord, currentFQN: string ) { const record: any = { $Type: parseRecordType(converter, currentTerm, currentTarget, currentProperty, annotationRecord), fullyQualifiedName: currentFQN, [ANNOTATION_TARGET]: currentTarget, __source: currentSource }; (record as AnnotationValue>)[CONVERTER_ROOT] = () => converter.getConvertedOutput(); for (const propertyValue of annotationRecord.propertyValues) { lazy(converter, record, propertyValue.name, () => parseValue( converter, currentTarget, currentTerm, propertyValue.name, currentSource, propertyValue.value, `${currentFQN}/${propertyValue.name}` ) ); } // annotations on the record lazy(converter, record, 'annotations', resolveAnnotationsOnAnnotation(converter, annotationRecord, record)); if (record.hasOwnProperty('CollectionPath')) { lazy(converter, record, 'CollectionPathTarget', () => { const entityContainerFQN: string = converter.rawSchema.entityContainers?.[currentSource].fullyQualifiedName ?? converter.rawSchema.entityContainer.fullyQualifiedName; return converter.getConvertedEntitySet(entityContainerFQN + '/' + record.CollectionPath); }); } if (isDataFieldWithForAction(record)) { lazy(converter, record, 'ActionTarget', () => { const actionName = converter.unalias(record.Action?.toString()); // (1) Bound action of the annotation target? let actionTarget = currentTarget.actions[actionName]; if (!actionTarget) { // (2) ActionImport (= unbound action)? actionTarget = converter.getConvertedActionImport(actionName)?.action; } if (!actionTarget) { // (3) Bound action of a different EntityType (the actionName is fully qualified in this case) actionTarget = converter.getConvertedAction(actionName); if (!actionTarget?.isBound) { actionTarget = undefined; } } if (!actionTarget) { converter.logError( `${record.fullyQualifiedName}: Unable to resolve '${record.Action}' ('${actionName}')` ); } return actionTarget; }); } return record; } export type CollectionType = | 'PropertyPath' | 'Path' | 'If' | 'Apply' | 'Null' | 'And' | 'Eq' | 'Ne' | 'Not' | 'Gt' | 'Ge' | 'Lt' | 'Le' | 'Or' | 'AnnotationPath' | 'NavigationPropertyPath' | 'Record' | 'String' | 'EmptyCollection'; /** * Retrieve or infer the collection type based on its content. * * @param collectionDefinition * @returns the type of the collection */ function getOrInferCollectionType(collectionDefinition: any[]): CollectionType { let type: CollectionType = (collectionDefinition as any).type; if (type === undefined && collectionDefinition.length > 0) { const firstColItem = collectionDefinition[0]; if (firstColItem.hasOwnProperty('PropertyPath')) { type = 'PropertyPath'; } else if (firstColItem.hasOwnProperty('Path')) { type = 'Path'; } else if (firstColItem.hasOwnProperty('AnnotationPath')) { type = 'AnnotationPath'; } else if (firstColItem.hasOwnProperty('NavigationPropertyPath')) { type = 'NavigationPropertyPath'; } else if ( typeof firstColItem === 'object' && (firstColItem.hasOwnProperty('type') || firstColItem.hasOwnProperty('propertyValues')) ) { type = 'Record'; } else if (typeof firstColItem === 'string') { type = 'String'; } } else if (type === undefined) { type = 'EmptyCollection'; } return type; } function parseCollection( converter: Converter, currentTarget: any, currentTerm: string, currentProperty: string, currentSource: string, collectionDefinition: any[], parentFQN: string ) { const collectionDefinitionType = getOrInferCollectionType(collectionDefinition); switch (collectionDefinitionType) { case 'PropertyPath': return collectionDefinition.map((path, index) => mapPropertyPath(converter, path, `${parentFQN}/${index}`, currentTarget, currentTerm) ); case 'Path': // TODO: make lazy? return collectionDefinition.map((pathValue) => { return resolveTarget(converter, currentTarget, pathValue.Path, currentTerm).target; }); case 'AnnotationPath': return collectionDefinition.map((path, index) => mapAnnotationPath(converter, path, `${parentFQN}/${index}`, currentTarget, currentTerm) ); case 'NavigationPropertyPath': return collectionDefinition.map((path, index) => mapNavigationPropertyPath(converter, path, `${parentFQN}/${index}`, currentTarget, currentTerm) ); case 'Record': return collectionDefinition.map((recordDefinition, recordIdx) => { return parseRecord( converter, currentTerm, currentTarget, currentProperty, currentSource, recordDefinition, `${parentFQN}/${recordIdx}` ); }); case 'Apply': case 'Null': case 'If': case 'Eq': case 'Ne': case 'Lt': case 'Gt': case 'Le': case 'Ge': case 'Not': case 'And': case 'Or': return collectionDefinition.map((ifValue) => ifValue); case 'String': return collectionDefinition.map((stringValue) => { if (typeof stringValue === 'string' || stringValue === undefined) { return stringValue; } else { return stringValue.String; } }); default: if (collectionDefinition.length === 0) { return []; } throw new Error('Unsupported case'); } } function isV4NavigationProperty( navProp: RawV2NavigationProperty | RawV4NavigationProperty ): navProp is RawV4NavigationProperty { return !!(navProp as BaseNavigationProperty).targetTypeName; } function convertAnnotation(converter: Converter, target: any, rawAnnotation: RawAnnotation): Annotation { let annotation: any; if (rawAnnotation.record) { annotation = parseRecord( converter, rawAnnotation.term, target, '', (rawAnnotation as any).__source, rawAnnotation.record, (rawAnnotation as any).fullyQualifiedName ); } else if (rawAnnotation.collection === undefined) { annotation = parseValue( converter, target, rawAnnotation.term, '', (rawAnnotation as any).__source, rawAnnotation.value ?? { type: 'Bool', Bool: true }, (rawAnnotation as any).fullyQualifiedName ); } else if (rawAnnotation.collection) { annotation = parseCollection( converter, target, rawAnnotation.term, '', (rawAnnotation as any).__source, rawAnnotation.collection, (rawAnnotation as any).fullyQualifiedName ); } else { throw new Error('Unsupported case'); } switch (typeof annotation) { case 'string': // eslint-disable-next-line no-new-wrappers annotation = new String(annotation); break; case 'boolean': // eslint-disable-next-line no-new-wrappers annotation = new Boolean(annotation); break; case 'number': annotation = new Number(annotation); break; default: // do nothing break; } annotation.fullyQualifiedName = (rawAnnotation as any).fullyQualifiedName; annotation[ANNOTATION_TARGET] = target; if (!annotation[CONVERTER_ROOT]) { annotation[CONVERTER_ROOT] = () => converter.getConvertedOutput(); } const [vocAlias, vocTerm] = converter.splitTerm(rawAnnotation.term); annotation.term = converter.unalias(`${vocAlias}.${vocTerm}`, VocabularyReferences); annotation.qualifier = rawAnnotation.qualifier; annotation.__source = (rawAnnotation as any).__source; try { lazy( converter, annotation, 'annotations', resolveAnnotationsOnAnnotation(converter, rawAnnotation, annotation) ); } catch (e) { // not an error: parseRecord() already adds annotations, but the other parseXXX functions don't, so this can happen } return annotation as Annotation; } class Converter implements IResettable { private annotationsByTarget?: Record; getConvertedOutput(): ConvertedMetadata { return this.convertedOutput; } /** * Get preprocessed annotations on the specified target. * * @param target The annotation target * @returns An array of annotations */ getAnnotations(target: FullyQualifiedName): Annotation[] { if (this.annotationsByTarget === undefined) { const annotationSources = Object.keys(this.rawSchema.annotations).map((source) => ({ name: source, annotationList: this.rawSchema.annotations[source] })); this.annotationsByTarget = mergeAnnotations(this.rawMetadata.references, ...annotationSources); } return this.annotationsByTarget[target] ?? []; } getConvertedEntityContainer() { return this.getConvertedElement( this.rawMetadata.schema.entityContainer.fullyQualifiedName, this.rawMetadata.schema.entityContainer, convertEntityContainer ); } getConvertedEntitySet(fullyQualifiedName: FullyQualifiedName) { return this.convertedOutput.entitySets.by_fullyQualifiedName(fullyQualifiedName); } getConvertedSingleton(fullyQualifiedName: FullyQualifiedName) { return this.convertedOutput.singletons.by_fullyQualifiedName(fullyQualifiedName); } getConvertedEntityType(fullyQualifiedName: FullyQualifiedName) { return this.convertedOutput.entityTypes.by_fullyQualifiedName(fullyQualifiedName); } getConvertedComplexType(fullyQualifiedName: FullyQualifiedName) { return this.convertedOutput.complexTypes.by_fullyQualifiedName(fullyQualifiedName); } getConvertedTypeDefinition(fullyQualifiedName: FullyQualifiedName) { return this.convertedOutput.typeDefinitions.by_fullyQualifiedName(fullyQualifiedName); } getConvertedActionImport(fullyQualifiedName: FullyQualifiedName) { let actionImport = this.convertedOutput.actionImports.by_fullyQualifiedName(fullyQualifiedName); if (!actionImport) { actionImport = this.convertedOutput.actionImports.by_name(fullyQualifiedName); } return actionImport; } getConvertedAction(fullyQualifiedName: FullyQualifiedName) { return this.convertedOutput.actions.by_fullyQualifiedName(fullyQualifiedName); } convert>( rawValue: Raw, map: (converter: Converter, raw: Raw) => Converted ): () => Converted; convert, IndexProperty extends Extract>( rawValue: Raw[], map: (converter: Converter, raw: Raw) => Converted ): () => ArrayWithIndex; convert, IndexProperty extends Extract>( rawValue: Raw | Raw[], map: (converter: Converter, raw: Raw) => Converted ): (() => Converted) | (() => ArrayWithIndex) { if (Array.isArray(rawValue)) { return () => { const converted = rawValue.reduce((convertedElements, rawElement) => { const convertedElement = this.getConvertedElement( (rawElement as any).fullyQualifiedName, rawElement, map ); if (convertedElement) { convertedElements.push(convertedElement); } return convertedElements; }, [] as Converted[]); addGetByValue(converted, 'name' as any); addGetByValue(converted, 'fullyQualifiedName' as any); return converted as ArrayWithIndex; }; } else { return () => this.getConvertedElement(rawValue.fullyQualifiedName, rawValue, map)!; } } reset(): void { for (const dynamicElementsKey in this.dynamicElements) { const keys = this.dynamicElements[dynamicElementsKey].keys; const targetObject = this.dynamicElements[dynamicElementsKey].target as any; for (const key of keys) { targetObject[key] = initialSymbol; } } this.annotationsByTarget = undefined; } private unknownCount = 0; collectDynamic(object: any, property: string) { let objectName = object.fullyQualifiedName; if (objectName === undefined) { objectName = `Unknown` + this.unknownCount++; } this.dynamicElements[objectName] ??= { target: object, keys: [] }; this.dynamicElements[objectName].keys.push(property); } private rawMetadata: RawMetadata; private convertedElements: Map = new Map(); private dynamicElements: Record = {}; private convertedOutput: ConvertedMetadata; rawSchema: RawSchema; constructor(rawMetadata: RawMetadata, convertedOutput: ConvertedMetadata) { this.rawMetadata = rawMetadata; this.rawSchema = rawMetadata.schema; this.convertedOutput = convertedOutput; } addExtraMetadata(extraMetadata: RawMetadata): void { mergeRawMetadata(this.rawMetadata, extraMetadata); } getConvertedElement>( fullyQualifiedName: FullyQualifiedName, rawElement: RawType | undefined | ((fullyQualifiedName: FullyQualifiedName) => RawType | undefined), map: (converter: Converter, raw: RawType) => ConvertedType ): ConvertedType | undefined { let converted: ConvertedType | undefined = this.convertedElements.get(fullyQualifiedName); if (converted === undefined) { const rawMetadata = typeof rawElement === 'function' ? rawElement.apply(undefined, [fullyQualifiedName]) : rawElement; if (rawMetadata !== undefined) { converted = map.apply(undefined, [this, rawMetadata]); this.convertedElements.set(fullyQualifiedName, converted); } } return converted; } logError(message: string) { this.convertedOutput.diagnostics.push({ message }); } /** * Split the alias from the term value. * * @param term the value of the term * @returns the term alias and the actual term value */ splitTerm(term: string) { const aliased = alias(VocabularyReferences, term); return splitAtLast(aliased, '.'); } unalias(value: string | undefined, references = this.rawMetadata.references) { return unalias(references, value, this.rawSchema.namespace) ?? ''; } } type RawType = RemoveAnnotationAndType & { fullyQualifiedName: FullyQualifiedName }; function resolveEntityType(converter: Converter, fullyQualifiedName: FullyQualifiedName) { return () => { let entityType = converter.getConvertedEntityType(fullyQualifiedName); if (!entityType) { converter.logError(`EntityType '${fullyQualifiedName}' not found`); entityType = {} as EntityType; } return entityType; }; } function resolveNavigationPropertyBindings( converter: Converter, rawNavigationPropertyBindings: RawNavigationPropertyBinding ) { return () => Object.keys(rawNavigationPropertyBindings).reduce((navigationPropertyBindings, bindingPath) => { const rawBindingTarget = rawNavigationPropertyBindings[bindingPath]; lazy( converter, navigationPropertyBindings, bindingPath, () => // the NavigationPropertyBinding will lead to either an EntitySet or a Singleton, it cannot be undefined (converter.getConvertedEntitySet(rawBindingTarget) ?? converter.getConvertedSingleton(rawBindingTarget))! ); return navigationPropertyBindings; }, {} as EntitySet['navigationPropertyBinding'] | Singleton['navigationPropertyBinding']); } function resolveAnnotations(converter: Converter, rawAnnotationTarget: any) { const nestedAnnotations = rawAnnotationTarget.annotations; return () => createAnnotationsObject( converter, rawAnnotationTarget, nestedAnnotations ?? converter.getAnnotations(rawAnnotationTarget.fullyQualifiedName) ); } function resolveAnnotationsOnAnnotation( converter: Converter, annotationRecord: AnnotationRecord | RawAnnotation, annotationTerm: any ) { return () => { const currentFQN = annotationTerm.fullyQualifiedName; // be graceful when resolving annotations on annotations: Sometimes they are referenced directly, sometimes they // are part of the global annotations list let annotations; if (annotationRecord.annotations && annotationRecord.annotations.length > 0) { annotations = annotationRecord.annotations; } else { annotations = converter.getAnnotations(currentFQN); } annotations?.forEach((annotation: any) => { annotation.target = currentFQN; annotation.__source = annotationTerm.__source; annotation[ANNOTATION_TARGET] = annotationTerm[ANNOTATION_TARGET]; annotation[CONVERTER_ROOT] = () => converter.getConvertedOutput(); annotation.fullyQualifiedName = `${currentFQN}@${annotation.term}`; }); return createAnnotationsObject(converter, annotationTerm, annotations ?? []); }; } function createAnnotationsObject(converter: Converter, target: any, rawAnnotations: RawAnnotation[]) { return rawAnnotations.reduce((vocabularyAliases, annotation) => { const [vocAlias, vocTerm] = converter.splitTerm(annotation.term); const vocTermWithQualifier = `${vocTerm}${annotation.qualifier ? '#' + annotation.qualifier : ''}`; if (vocabularyAliases[vocAlias] === undefined) { vocabularyAliases[vocAlias] = { _keys: [] }; } if (!vocabularyAliases[vocAlias].hasOwnProperty(vocTermWithQualifier)) { vocabularyAliases[vocAlias]._keys.push(vocTermWithQualifier); lazy(converter, vocabularyAliases[vocAlias], vocTermWithQualifier, () => converter.getConvertedElement( (annotation as Annotation).fullyQualifiedName, annotation, (converter, rawAnnotation) => convertAnnotation(converter, target, rawAnnotation) ) ); } return vocabularyAliases; }, {} as any); } /** * Converts an EntityContainer. * * @param converter Converter * @param rawEntityContainer Unconverted EntityContainer * @returns The converted EntityContainer */ function convertEntityContainer(converter: Converter, rawEntityContainer: RawEntityContainer): EntityContainer { const convertedEntityContainer = rawEntityContainer as EntityContainer; lazy(converter, convertedEntityContainer, 'annotations', resolveAnnotations(converter, rawEntityContainer)); lazy( converter, convertedEntityContainer, 'entitySets', converter.convert(converter.rawSchema.entitySets, convertEntitySet) ); lazy( converter, convertedEntityContainer, 'singletons', converter.convert(converter.rawSchema.singletons, convertSingleton) ); lazy( converter, convertedEntityContainer, 'actionImports', converter.convert(converter.rawSchema.actionImports, convertActionImport) ); return convertedEntityContainer; } /** * Converts a Singleton. * * @param converter Converter * @param rawSingleton Unconverted Singleton * @returns The converted Singleton */ function convertSingleton(converter: Converter, rawSingleton: RawSingleton): Singleton { const convertedSingleton = rawSingleton as unknown as Singleton; lazy(converter, convertedSingleton, 'entityType', resolveEntityType(converter, rawSingleton.entityTypeName)); lazy(converter, convertedSingleton, 'annotations', resolveAnnotations(converter, convertedSingleton)); const _rawNavigationPropertyBindings = rawSingleton.navigationPropertyBinding; lazy( converter, convertedSingleton, 'navigationPropertyBinding', resolveNavigationPropertyBindings(converter, _rawNavigationPropertyBindings) ); return convertedSingleton; } /** * Converts an EntitySet. * * @param converter Converter * @param rawEntitySet Unconverted EntitySet * @returns The converted EntitySet */ function convertEntitySet(converter: Converter, rawEntitySet: RawEntitySet): EntitySet { const convertedEntitySet = rawEntitySet as unknown as EntitySet; lazy(converter, convertedEntitySet, 'entityType', resolveEntityType(converter, rawEntitySet.entityTypeName)); lazy(converter, convertedEntitySet, 'annotations', resolveAnnotations(converter, convertedEntitySet)); const _rawNavigationPropertyBindings = rawEntitySet.navigationPropertyBinding; lazy( converter, convertedEntitySet, 'navigationPropertyBinding', resolveNavigationPropertyBindings(converter, _rawNavigationPropertyBindings) ); return convertedEntitySet; } /** * Converts an EntityType. * * @param converter Converter * @param rawEntityType Unconverted EntityType * @returns The converted EntityType */ function convertEntityType(converter: Converter, rawEntityType: RawEntityType): EntityType { const convertedEntityType = rawEntityType as EntityType; rawEntityType.keys.forEach((keyProp: any) => { keyProp.isKey = true; }); lazy(converter, convertedEntityType, 'annotations', resolveAnnotations(converter, rawEntityType)); lazy(converter, convertedEntityType, 'keys', converter.convert(rawEntityType.keys, convertProperty)); lazy( converter, convertedEntityType, 'entityProperties', converter.convert(rawEntityType.entityProperties, convertProperty) ); lazy( converter, convertedEntityType, 'navigationProperties', converter.convert(rawEntityType.navigationProperties as any[], convertNavigationProperty) ); lazy(converter, convertedEntityType, 'actions', () => converter.rawSchema.actions .filter( (rawAction) => rawAction.isBound && (rawAction.sourceType === rawEntityType.fullyQualifiedName || rawAction.sourceType === `Collection(${rawEntityType.fullyQualifiedName})`) ) .reduce((actions, rawAction) => { const name = `${converter.rawSchema.namespace}.${rawAction.name}`; actions[name] = converter.getConvertedAction(rawAction.fullyQualifiedName)!; return actions; }, {} as EntityType['actions']) ); convertedEntityType.resolvePath = (relativePath: string, includeVisitedObjects?: boolean) => { const resolved = resolveTarget(converter, rawEntityType, relativePath); if (includeVisitedObjects) { return { target: resolved.target, visitedObjects: resolved.objectPath, messages: resolved.messages }; } else { return resolved.target; } }; return convertedEntityType; } /** * Converts a Property. * * @param converter Converter * @param rawProperty Unconverted Property * @returns The converted Property */ function convertProperty(converter: Converter, rawProperty: RawProperty): Property { const convertedProperty = rawProperty as Property; lazy(converter, convertedProperty, 'annotations', resolveAnnotations(converter, rawProperty)); lazy(converter, convertedProperty, 'targetType', () => { const type = rawProperty.type; const typeName = type.startsWith('Collection') ? type.substring(11, type.length - 1) : type; return converter.getConvertedComplexType(typeName) ?? converter.getConvertedTypeDefinition(typeName); }); return convertedProperty; } /** * Converts a NavigationProperty. * * @param converter Converter * @param rawNavigationProperty Unconverted NavigationProperty * @returns The converted NavigationProperty */ function convertNavigationProperty( converter: Converter, rawNavigationProperty: RawV2NavigationProperty | RawV4NavigationProperty ): NavigationProperty { const convertedNavigationProperty = rawNavigationProperty as NavigationProperty; convertedNavigationProperty.referentialConstraint = convertedNavigationProperty.referentialConstraint ?? []; if (convertedNavigationProperty.referentialConstraint.length === 0) { const annotations = converter.getAnnotations(rawNavigationProperty.fullyQualifiedName); const refConstraints = annotations.find( (annotation) => annotation.term === CommonAnnotationTerms.ReferentialConstraint ); if (refConstraints) { convertedNavigationProperty.referentialConstraint = (refConstraints.collection?.map((record) => { const referencedProperty = (record as AnnotationRecord).propertyValues.find((prop) => { return prop.name === 'ReferencedProperty'; })?.value as { PropertyPath?: string }; let targetedProperty = ''; if (referencedProperty.PropertyPath) { const targetedPropertySplit = referencedProperty.PropertyPath.split('/'); targetedPropertySplit.shift(); targetedProperty = targetedPropertySplit.join('/'); } return { sourceProperty: ( (record as AnnotationRecord).propertyValues.find((prop) => { return prop.name === 'Property'; })?.value as { PropertyPath?: string } ).PropertyPath, targetTypeName: convertedNavigationProperty.targetTypeName, targetProperty: targetedProperty }; }) as any) ?? []; } } if (!isV4NavigationProperty(rawNavigationProperty)) { const associationEnd = converter.rawSchema.associations .find((association) => association.fullyQualifiedName === rawNavigationProperty.relationship) ?.associationEnd.find((end) => end.role === rawNavigationProperty.toRole); convertedNavigationProperty.isCollection = associationEnd?.multiplicity === '*'; convertedNavigationProperty.targetTypeName = associationEnd?.type ?? ''; } lazy( converter, convertedNavigationProperty, 'targetType', resolveEntityType(converter, (rawNavigationProperty as NavigationProperty).targetTypeName) ); lazy(converter, convertedNavigationProperty, 'annotations', resolveAnnotations(converter, rawNavigationProperty)); return convertedNavigationProperty; } /** * Converts an ActionImport. * * @param converter Converter * @param rawActionImport Unconverted ActionImport * @returns The converted ActionImport */ function convertActionImport(converter: Converter, rawActionImport: RawActionImport): ActionImport { const convertedActionImport = rawActionImport as ActionImport; lazy(converter, convertedActionImport, 'annotations', resolveAnnotations(converter, rawActionImport)); lazy(converter, convertedActionImport, 'action', () => { const rawActions = converter.rawSchema.actions.filter( (rawAction) => !rawAction.isBound && rawAction.fullyQualifiedName.startsWith(rawActionImport.actionName + '(') ); // this always resolves to a unique unbound action, but resolution of unbound functions can be ambiguous: // unbound function FQNs have overloads depending on all of their parameters if (rawActions.length > 1) { converter.logError(`Ambiguous reference in action import: ${rawActionImport.fullyQualifiedName}`); } // return the first matching action or function return converter.getConvertedAction(rawActions[0].fullyQualifiedName)!; }); return convertedActionImport; } /** * Converts an Action. * * @param converter Converter * @param rawAction Unconverted Action * @returns The converted Action */ function convertAction(converter: Converter, rawAction: RawAction): Action { const convertedAction = rawAction as Action; if (convertedAction.sourceType) { lazy(converter, convertedAction, 'sourceEntityType', resolveEntityType(converter, rawAction.sourceType)); } if (convertedAction.returnType) { lazy(converter, convertedAction, 'returnEntityType', resolveEntityType(converter, rawAction.returnType)); lazy(converter, convertedAction, 'returnTypeReference', () => { const typeName = convertedAction.returnType.startsWith('Collection') ? convertedAction.returnType.substring(11, convertedAction.returnType.length - 1) : convertedAction.returnType; return ( converter.getConvertedEntityType(typeName) ?? converter.getConvertedComplexType(typeName) ?? converter.getConvertedTypeDefinition(typeName) ); }); } lazy(converter, convertedAction, 'parameters', converter.convert(rawAction.parameters, convertActionParameter)); lazy(converter, convertedAction, 'annotations', () => { /* Annotation resolution rule for actions: (1) annotation target: the specific unbound or bound overload, e.g. - for actions: "x.y.z.unboundAction()" / "x.y.z.boundAction(x.y.z.Entity)" - for functions: "x.y.z.unboundFunction(Edm.String)" / "x.y.z.unboundFunction(x.y.z.Entity,Edm.String)" (2) annotation target: unspecified overload, e.g. - for actions: "x.y.z.unboundAction" / "x.y.z.boundAction" - for functions: "x.y.z.unboundFunction" / "x.y.z.unboundFunction" When resolving (1) takes precedence over (2). That is, annotations on the specific overload overwrite annotations on the unspecific overload, on term/qualifier level. */ const unspecificOverloadTarget = substringBeforeFirst(rawAction.fullyQualifiedName, '('); const specificOverloadTarget = rawAction.fullyQualifiedName; const effectiveAnnotations = converter.getAnnotations(specificOverloadTarget); const unspecificAnnotations = converter.getAnnotations(unspecificOverloadTarget); for (const unspecificAnnotation of unspecificAnnotations) { if ( !effectiveAnnotations.some( (annotation) => annotation.term === unspecificAnnotation.term && annotation.qualifier === unspecificAnnotation.qualifier ) ) { effectiveAnnotations.push(unspecificAnnotation); } } return createAnnotationsObject(converter, rawAction, effectiveAnnotations); }); return convertedAction; } /** * Converts an ActionParameter. * * @param converter Converter * @param rawActionParameter Unconverted ActionParameter * @returns The converted ActionParameter */ function convertActionParameter( converter: Converter, rawActionParameter: RawAction['parameters'][number] ): ActionParameter { const convertedActionParameter = rawActionParameter as ActionParameter; lazy(converter, convertedActionParameter, 'typeReference', () => { let targetType = rawActionParameter.type; if (targetType.startsWith('Collection(')) { targetType = targetType.substring(11, targetType.length - 1); } return ( converter.getConvertedEntityType(targetType) ?? converter.getConvertedComplexType(targetType) ?? converter.getConvertedTypeDefinition(targetType) ); }); lazy(converter, convertedActionParameter, 'annotations', () => { // annotations on action parameters are resolved following the rules for actions const unspecificOverloadTarget = rawActionParameter.fullyQualifiedName.substring(0, rawActionParameter.fullyQualifiedName.indexOf('(')) + rawActionParameter.fullyQualifiedName.substring(rawActionParameter.fullyQualifiedName.lastIndexOf(')') + 1); const specificOverloadTarget = rawActionParameter.fullyQualifiedName; const effectiveAnnotations = converter.getAnnotations(specificOverloadTarget); const unspecificAnnotations = converter.getAnnotations(unspecificOverloadTarget); for (const unspecificAnnotation of unspecificAnnotations) { if ( !effectiveAnnotations.some( (annotation) => annotation.term === unspecificAnnotation.term && annotation.qualifier === unspecificAnnotation.qualifier ) ) { effectiveAnnotations.push(unspecificAnnotation); } } return createAnnotationsObject(converter, rawActionParameter, effectiveAnnotations); }); return convertedActionParameter; } /** * Converts a ComplexType. * * @param converter Converter * @param rawComplexType Unconverted ComplexType * @returns The converted ComplexType */ function convertComplexType(converter: Converter, rawComplexType: RawComplexType): ComplexType { const convertedComplexType = rawComplexType as ComplexType; lazy(converter, convertedComplexType, 'properties', converter.convert(rawComplexType.properties, convertProperty)); lazy( converter, convertedComplexType, 'navigationProperties', converter.convert(rawComplexType.navigationProperties as any[], convertNavigationProperty) ); lazy(converter, convertedComplexType, 'annotations', resolveAnnotations(converter, rawComplexType)); return convertedComplexType; } /** * Convers an EnumMember. * @param converter * @param rawEnumMember * @returns The converted EnumMember */ function convertEnumMember(converter: Converter, rawEnumMember: RawEnumMember): EnumMember { const convertedEnumMember = rawEnumMember as EnumMember; lazy(converter, convertedEnumMember, 'annotations', resolveAnnotations(converter, rawEnumMember)); return convertedEnumMember; } /** * Converts an EnumType. * @param converter Converter * @param rawEnumType Unconverted EnumType * @returns The converted EnumType */ function convertEnumType(converter: Converter, rawEnumType: RawEnumType): EnumType { const convertedEnumType = rawEnumType as EnumType; lazy(converter, convertedEnumType, 'members', converter.convert(rawEnumType.members, convertEnumMember)); lazy(converter, convertedEnumType, 'annotations', resolveAnnotations(converter, rawEnumType)); return convertedEnumType; } /** * Converts a TypeDefinition. * * @param converter Converter * @param rawTypeDefinition Unconverted TypeDefinition * @returns The converted TypeDefinition */ function convertTypeDefinition(converter: Converter, rawTypeDefinition: RawTypeDefinition): TypeDefinition { const convertedTypeDefinition = rawTypeDefinition as TypeDefinition; lazy(converter, convertedTypeDefinition, 'annotations', resolveAnnotations(converter, rawTypeDefinition)); return convertedTypeDefinition; } type ConvertedMetadataInternal = ConvertedMetadata & { getConverter: () => Converter; }; /** * Convert a RawMetadata into an object representation to be used to easily navigate a metadata object and its annotation. * * @param rawMetadata * @returns the converted representation of the metadata. */ export function convert(rawMetadata: RawMetadata): ConvertedMetadata { // Converter Output const convertedOutput: ConvertedMetadataInternal = { version: rawMetadata.version, namespace: rawMetadata.schema.namespace, annotations: rawMetadata.schema.annotations, references: VocabularyReferences, diagnostics: [] } as any; // Converter const converter = new Converter(rawMetadata, convertedOutput); convertedOutput.getConverter = () => converter; lazy( converter, convertedOutput, 'entityContainer', converter.convert(converter.rawSchema.entityContainer, convertEntityContainer) ); lazy(converter, convertedOutput, 'entitySets', converter.convert(converter.rawSchema.entitySets, convertEntitySet)); lazy(converter, convertedOutput, 'singletons', converter.convert(converter.rawSchema.singletons, convertSingleton)); lazy( converter, convertedOutput, 'entityTypes', converter.convert(converter.rawSchema.entityTypes, convertEntityType) ); lazy(converter, convertedOutput, 'actions', converter.convert(converter.rawSchema.actions, convertAction)); lazy( converter, convertedOutput, 'complexTypes', converter.convert(converter.rawSchema.complexTypes, convertComplexType) ); lazy(converter, convertedOutput, 'enumTypes', converter.convert(converter.rawSchema.enumTypes, convertEnumType)); lazy( converter, convertedOutput, 'actionImports', converter.convert(converter.rawSchema.actionImports, convertActionImport) ); lazy( converter, convertedOutput, 'typeDefinitions', converter.convert(converter.rawSchema.typeDefinitions, convertTypeDefinition) ); convertedOutput.resolvePath = function resolvePath( path: string, startingPoint?: ServiceObjectAndAnnotation ): ResolutionTarget { const targetResolution = resolveTarget(converter, startingPoint, path); if (targetResolution.target) { appendObjectPath(targetResolution.objectPath, targetResolution.target); } return targetResolution; }; /** * Adds an additional metadata file referencing value list information into the main converted data. * * @param {Array} rawVHMetadata - The list of values to be added to the converted output * @returns {void} */ convertedOutput.addValueListWithReferences = function (rawVHMetadata: RawMetadata): void { const converter = (convertedOutput as ConvertedMetadataInternal).getConverter(); converter.addExtraMetadata(rawVHMetadata); // Force reset the converted data converter.reset(); }; return convertedOutput; }