UNPKG

@sap/cds-dk

Version:

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

1,288 lines (1,125 loc) 105 kB
'use strict'; const message = require("../../message").getMessages(); const vocabulariesMap = new Map([ ['org.odata.core.v1', () => require('../vocabularies/Org.OData.Core.V1.json')], ['org.odata.aggregation.v1', () => require('../vocabularies/Org.OData.Aggregation.V1.json')], ['org.odata.authorization.v1', () => require('../vocabularies/Org.OData.Authorization.V1.json')], ['org.odata.capabilities.v1', () => require('../vocabularies/Org.OData.Capabilities.V1.json')], ['org.odata.measures.v1', () => require('../vocabularies/Org.OData.Measures.V1.json')], ['org.odata.validation.v1', () => require('../vocabularies/Org.OData.Validation.V1.json')] ]); const createCast = (value, type) => { return { $Cast: value, $Type: type }; }; /** * The MetadataConverter is responsible for executing provided Strategies. It handles occuring errors * also. * * @class MetadataConverter */ class MetadataConverter { /** * Creates an instance of MetadataConverter. * @param {Object} options the options object * @memberof MetadataConverter */ // constructor(options = { omit$TypeOnEdmString: true }) { constructor(options = {}) { this._options = options; this._conversion = {}; } /** * Adds new conversion strategies. The conversion strategy is an array of {@link Strategy} which * will be executed by this {@link MetadataConverter}. * * @param {Array<MetadataConverter#Strategy>} strategy The chain of strategies to use * @returns {MetadataConverter} This instance of MetadataConverter * @memberof MetadataConverter */ addConversion(strategy) { this._conversion = { strategy }; return this; } /** * Filter out a property found by filter parameter * * @param {Object} input The input to start * @param {Function} filter The filter function. Should return false to filter a certain property * @returns {Object} The result without the filtered properties * @memberof MetadataConverter * @private */ _filterProperties(input, filter) { const target = {}; Object .keys(input) .filter(key => filter(key, input)) .forEach((key) => { const value = input[key]; if (typeof value === 'object' && !Array.isArray(value) && value != null) { target[key] = this._filterProperties(value, filter); } else if (Array.isArray(value)) { target[key] = value.map((item) => { if (typeof item === 'object') { return this._filterProperties(item, filter); } return item; }); } else { target[key] = value; } }); return target; } getOptions() { return this._options; } /** * This callback is displayed as part of the MetadataConverter class. * This callback is called when the metadata converter finishes the conversion or if any * error occurs. * * @callback MetadataConverter~executeCallback * @param {Error} error If an error occurs else null * @param {Object} result The result of the conversion */ /** * This method executes a provided chain of {@link Strategy}s (provided through * {@link MetadataConverter#addConversion}) and injects the input into the first Strategy. * To execute a chain of Strategies the same name as used in {@link MetadataConverter#addConversion}) * method must be used. The callback will be called on finish or on error. * * @param {any} input The input to provide into the first Strategy * @param {MetadataConverter~executeCallback} callback This callback will be called on finish or on error * @param {number} [index=0] A private variable to select the strategy * @returns {undefined} nothing * @memberof MetadataConverter */ execute(input, callback, index = 0) { let lastResult = null; const strategy = this._conversion.strategy[index]; if (strategy == null) { let result = input; return callback(null, result, []); } try { strategy.execute( input, (error, result, ...args) => { lastResult = result; if (error) { return callback(error, result, ...args); } return this.execute(result, callback, index + 1); }, this ); return undefined; } catch (error) { return callback(error, lastResult); } } } const ODATA_NAMESPACES = { v4: [ 'http://docs.oasis-open.org/odata/ns/edm', 'http://docs.oasis-open.org/odata/ns/edmx' ] }; MetadataConverter.ODATA_NAMESPACES = ODATA_NAMESPACES; MetadataConverter.TARGETS = { LIBRARY: 'CS01' }; class Emitter { constructor() { this._listeners = new Map(); } _on(listeners, name, callback) { if (listeners.has(name) === false) { listeners.set(name, []); } listeners.get(name).push(callback); return this; } on(name, callback) { return this._on(this._listeners, name, callback); } emit(name, ...args) { const listeners = (this._listeners.get('pre ' + name) || []) .concat(this._listeners.get(name) || []) .concat(this._listeners.get('post ' + name) || []); this._emit(listeners, 0, null, name === 'error', ...args); return this; } _emit(list, index, error, isErrorEmitting, ...args) { if (error != null) { return this.emit('error', error); } let _index = index; const callback = list[_index]; if (callback != null) { if (isErrorEmitting === true) { callback(...args); } else { try { callback((innerError) => { this._emit(list, _index + 1, innerError, isErrorEmitting, ...args); }, ...args); } catch (innerError) { this.emit('error', innerError); } } } return this; } } MetadataConverter.Emitter = Emitter; class Strategy extends Emitter { constructor(strategy, options = {}) { super(); this._options = options; this._strategy = strategy; this._logger = options.logger; } setLogger(logger) { this._logger = logger; return this; } getLogger() { return this._logger; } execute(input, callback) { // callback(error, result) -> result = conversion result return this._strategy(input, callback); } } MetadataConverter.Strategy = Strategy; class Expression { constructor(element, parentExpression, context) { this._element = element; this._parentExpression = parentExpression; this._stack = []; this._target = {}; this._type = this.constructor.name; this._context = context; this._availableExpressions = new Map(); if (element.attributes && element.attributes.Alias && element.attributes.Namespace) { context.setAlias(element.attributes.Alias, element.attributes.Namespace); } context.getLogger().debug(`Creating Expression '${this.constructor.name}'`); } static convertDefaultValue(type, value) { if (type === 'Edm.Boolean') return value === 'true'; if (type === 'Edm.Binary' || type === 'Edm.String') return value; if (type === 'Edm.Int16' || type === 'Edm.Int32' || type === 'Edm.Byte' || type === 'Edm.SByte') { return parseInt(value, 10); } if (type === 'Edm.Float' || type === 'Edm.Decimal' || type === 'Edm.Single') return parseFloat(value); if (type === 'EnumType') { return value.split(' ').map((elem) => { const split = elem.split('/'); if (split.length === 1) { return split[0]; } return split[1]; }).join(','); } return value; } createExpressionFromAttributes(element) { const target = this.getContext().getConverterOptions().target; const isLibTarget = target === MetadataConverter.TARGETS.LIBRARY; const attr = element.attributes; if (attr.String) return attr.String; if (attr.Bool) return attr.Bool === 'true'; if (attr.Binary) return createCast(attr.Binary, 'Edm.Binary'); if (attr.Date) return createCast(attr.Date, 'Edm.Date'); if (attr.DateTimeOffset) return createCast(attr.DateTimeOffset, 'Edm.DateTimeOffset'); if (attr.Decimal) return createCast(attr.Decimal, 'Edm.Decimal'); if (attr.Duration) return createCast(attr.Duration, 'Edm.Duration'); if (attr.Float) return parseFloat(attr.Float); if (attr.Double) return parseFloat(attr.Double); if (attr.Guid) return createCast(attr.Guid, 'Edm.Guid'); if (attr.Int) return createCast(attr.Int, 'Edm.Int64'); if (attr.TimeOfDay) return createCast(attr.TimeOfDay, 'Edm.TimeOfDay'); if (isLibTarget === true) { if (attr.AnnotationPath) { return { $AnnotationPath: this.resolveNamespace(attr.AnnotationPath, true) }; } if (attr.PropertyPath) return { $PropertyPath: attr.PropertyPath }; if (attr.ModelElementPath) return { $ModelElementPath: attr.ModelElementPath }; if (attr.NavigationPropertyPath) { return { $NavigationPropertyPath: attr.NavigationPropertyPath }; } /** * For entries like: * <PropertyValue Property="Rollup" EnumMember="SAP__aggregation.RollupType/None"/> */ if (attr.EnumMember) { return { "#" : this.convertEnumMemberExpression(attr.EnumMember) }; } } else { if (attr.AnnotationPath) { return this.resolveNamespace(attr.AnnotationPath, true); } if (attr.PropertyPath) return attr.PropertyPath; if (attr.ModelElementPath) return attr.ModelElementPath; if (attr.NavigationPropertyPath) return attr.NavigationPropertyPath; if (attr.EnumMember) { return { "#" : this.convertEnumMemberExpression(attr.EnumMember) }; } } if (attr.Path) return { $Path: attr.Path }; if (attr.UrlRef) return { $UrlRef: attr.UrlRef }; return null; } convertEnumMemberExpression(enumMemberStr, typeFqn) { let type = typeFqn; const result = { $EnumMember: enumMemberStr.split(' ').map((member) => { const split = member.split('/'); if (split.length === 1) { type = typeFqn; return split[0]; } if (type === typeFqn) { type = split[0]; } return split[1]; }).join(','), '$EnumMember@odata.type': `#${type}` }; const target = this.getContext().getConverterOptions().target; const isLibTarget = target === MetadataConverter.TARGETS.LIBRARY; if (isLibTarget === true) { return result; } return result.$EnumMember; } getType() { return this._type; } getElement() { return this._element; } getParentExpression() { return this._parentExpression; } getTarget() { return this._target; } setTarget(target) { this._target = target; return this; } getStack() { return this._stack; } getTargetPropertyName() { return this._targetPropertyName; } setTargetPropertyName(name) { this._targetPropertyName = name; return this; } getContext() { return this._context; } interpret() { const target = this.getTarget(); return Object.assign(target, ...this.getStack().map(expression => expression.interpret())); } static lookupXmlNamespace(element, parentExpression, prefix, logger) { let currentXmlns; let attr = element.attributes; if (element.name != null) { if (attr) { if (logger) { logger.debug(`Lookup namespace for element '${element.name}' with prefix '${prefix || ''}'`); } currentXmlns = prefix == null ? attr.xmlns : attr[`xmlns:${prefix}`]; if (currentXmlns != null) { return currentXmlns; } } } if (parentExpression != null) { return Expression.lookupXmlNamespace( parentExpression.getElement(), parentExpression.getParentExpression(), prefix, logger ); } return null; } annotate(prefix = '', target, annotationExpressionResult) { let _target = target; for (const key of Object.keys(annotationExpressionResult)) { if (prefix !== '') prefix = prefix + "."; _target[prefix + key] = annotationExpressionResult[key]; } return target; } resolveAlias(name) { return this.getContext().resolveAlias(name); } resolveNamespace(namespacedType, forceReplace) { return this.getContext().resolveNamespace(namespacedType, forceReplace); } buildTypeCollectionAndFacets(...args) { const target = this.getTarget(); const element = this.getElement(); const attribs = element.attributes; if (attribs == null) return; let realType = attribs.Type || attribs.EntityType || attribs.UnderlyingType; const propertyName = attribs.UnderlyingType == null ? '$Type' : '$UnderlyingType'; if ((args.includes('$Type') || args.includes('$UnderlyingType')) && realType) { const omit$Type = propertyName === '$Type' && realType.indexOf('Edm.String') > -1 && target.$Cast === undefined; if (realType.startsWith('Collection(')) { realType = realType.substring(11, realType.length - 1); target.$Collection = true; } if (omit$Type === true) { let current = this; const stack = []; do { stack.push(current); current = current.getParentExpression(); } while (current != null); } target[propertyName] = realType; } /* HasStream Spec odata v 4.0: XML: If no value is provided for the HasStream attribute, and no BaseType attribute is specified, the value of the HasStream attribute is set to false. JSON: Absence of the member means false XML | JSON ----------------------- true | true undefined | undefined false | undefined */ if (args.includes('$HasStream') && attribs.HasStream === 'true') target.$HasStream = true; /* IsFlags Spec odata v 4.0: XML: If no value is specified for this attribute, its value defaults to false. JSON: Absence of the member means false XML | JSON ----------------------- true | true undefined | undefined false | undefined */ if (args.includes('$IsFlags') && attribs.IsFlags === 'true') target.$IsFlags = true; if (args.includes('$Alias') && attribs.Alias) target.$Alias = attribs.Alias; if (args.includes('$Namespace') && attribs.Namespace) target.$Namespace = attribs.Namespace; if (args.includes('$OpenType') && attribs.OpenType) target.$OpenType = attribs.OpenType; if (args.includes('$BaseType') && attribs.BaseType) target.$BaseType = attribs.BaseType; if (args.includes('$MaxLength') && attribs.MaxLength != null && attribs.MaxLength.toLowerCase() !== 'max') { target.$MaxLength = parseInt(attribs.MaxLength, 10); } /* Scale Spec odata v 4.0: XML: If no value is specified, the Scale facet defaults to zero. JSON: If no value is specified, the Scale facet defaults to zero OLD NEW XML | JSON XML | JSON --------------------- ----------------------- variable | variable variable | variable 1-* | 1-* 0-* | 0-* 0 | undefined undefined | undefined undefined | undefined * In case EDMX doesn't specify Scale, we are not adding default `0` for it anymore * beause we want to easily identify and preserve the explicitly mentioned Scale="0" in EDMX */ if (args.includes('$Scale') && (target.$Type === 'Edm.Decimal' || target.$UnderlyingType === 'Edm.Decimal')) { if (attribs.Scale != null) { if (attribs.Scale === 'variable' || attribs.Scale === 'floating') { target.$Scale = attribs.Scale; } // for now considering only non-negative integers else if (Number(attribs.Scale) >= 0) { target.$Scale = parseInt(attribs.Scale, 10); } } } /* Precision Spec odata v 4.0: XML: For a temporal property [...] If no value is specified, the temporal property has a precision of zero. JSON: If no value is specified, the temporal property has a precision of zero. XML (Temp prop) | JSON (Temp prop) ---------------------------------- 1-12 | 1-12 0 | undefined XML | JSON ----------------------- * | * undefined | undefined */ if (args.includes('$Precision')) { if (attribs.Precision == null) { if (['Edm.Duration', 'Edm.Date', 'Edm.TimeOfDay', 'Edm.DateTimeOffset'].includes(target.$Type)) { target.$Precision = 0; } } else if (target.$Precision !== '0') { target.$Precision = parseInt(attribs.Precision, 10); } } if (args.includes('$Partner') && attribs.Partner != null) target.$Partner = attribs.Partner; /* IsComposable Spec odata v 4.0: XML: If no value is assigned [...], the attribute defaults to false JSON: Absence of the member means false XML | JSON ----------------------- true | true undefined | undefined false | undefined */ if (args.includes('$IsComposable') && attribs.IsComposable === 'true') target.$IsComposable = true; /* ContainsTarget Spec odata v 4.0: XML: If no value is assigned [...], the attribute defaults to false JSON: Absence of the member means false XML | JSON ----------------------- true | true undefined | undefined false | undefined */ if (args.includes('$ContainsTarget') && attribs.ContainsTarget === 'true') target.$ContainsTarget = true; /* IsBound Spec odata v 4.0: XML: Actions whose IsBound attribute is false or not specified are considered unbound --> false JSON: Absence of the member means false XML | JSON ----------------------- true | true undefined | undefined false | undefined */ if (args.includes('$IsBound') && attribs.IsBound === 'true') target.$IsBound = true; if (args.includes('$DefaultValue') && attribs.DefaultValue !== undefined) { target.$DefaultValue = attribs.DefaultValue; } if (args.includes('$EntitySet') && attribs.EntitySet != null) { target.$EntitySet = attribs.EntitySet; } if (args.includes('$EntitySetPath') && attribs.EntitySetPath != null) { target.$EntitySetPath = attribs.EntitySetPath; } /* Nullable Spec odata v 4.0: XML: If not specified, the Nullable attribute defaults to true. JSON: Absence of the member means false XML | JSON ----------------------- true | true undefined | true false | undefined */ if (args.includes('$Nullable') && (attribs.Nullable == null || attribs.Nullable === 'true') && !(target.$Kind === 'NavigationProperty' && target.$Collection === true)) { target.$Nullable = true; } this.includeExternalAnnotations(element, target); } includeExternalAnnotations(element, target) { const nonStdAttributes = this.getNonStdAttributes(element.attributes); for (let i = 0; i < nonStdAttributes.length; i++) { let attr = nonStdAttributes[i]; let [prefix, attrName] = attr.split(':'); if (attrName) attrName = attrName.replace(/-/g, '.'); let attribute; if ((MetadataConverter.custom_namespace.includes(prefix) || MetadataConverter.custom_namespace.includes(true)) && attrName !== undefined) attribute = "@" + prefix + "." + attrName; else if (attrName === undefined) attribute = "$" + prefix; if (attribute !== undefined && !attribute.startsWith('$')) { target[attribute] = element.attributes[attr]; } } } getNonStdAttributes(attributes) { const attributeList = []; const standardArrtibutes = ['MaxLength', 'Precision', 'Scale', 'Nullable', 'DefaultValue', 'Name', 'Type', 'HasStream', 'IsFlags', 'Alias', 'Namespace', 'BaseType', 'Partner', 'IsComposable', 'ContainsTarget', 'IsBound', 'EntitySet', 'EntitySetPath', 'EntityType', 'UnderlyingType']; attributeList.push(...Object.keys(attributes).filter(attribute => standardArrtibutes.indexOf(attribute) === -1)); return attributeList; } } class EdmPrimitiveType { constructor(type) { this._type = type; } toString() { return this._type; } static from(typeFqn) { if (typeFqn.startsWith('Edm.')) return new EdmPrimitiveType(typeFqn); return null; } } class Named { constructor(edm, name, target) { this._edm = edm; this._name = name; this._target = target; } getTarget() { return this._target; } getType() { if (this._type) return this._type; const typeFqn = this._target.$Type || this._target.$UnderlyingType || 'Edm.String'; this._type = this._edm.getEntityType(typeFqn) || this._edm.getComplexType(typeFqn) || this._edm.getEnumType(typeFqn) || this._edm.getTypeDefinition(typeFqn) || EdmPrimitiveType.from(typeFqn); return this._type; } isCollection() { return this._target.$Collection === true; } setType(type) { this._type = type; return this; } toString() { return (this._target.$Type || this._target.$UnderlyingType || 'Edm.String'); } getName() { return this._name; } } class StructuredType extends Named { constructor(edm, name, fqn, target) { super(edm, name, target); this._structuredTypeFqn = fqn; } } class Term extends Named { constructor(edm, name, target) { super(edm, name, target); } getDefaultValue() { return this._target.$DefaultValue; } } class EnumType extends Named { constructor(edm, name, target) { super(edm, name, target); } } class TypeDefinition extends Named { constructor(edm, name, target) { super(edm, name, target); } } class EntityType extends StructuredType { constructor(edm, name, fqn, target) { super(edm, name, fqn, target); } } class ComplexType extends StructuredType { constructor(edm, name, fqn, target) { super(edm, name, fqn, target); } } class JsonEdm { constructor(csdl) { this._csdl = csdl; this._schemas = Object.keys(csdl) .filter(key => !['$Reference', '$Version', '$EntityContainer'].includes(key)); } toFqn(aliasedName) { const [alias, type] = aliasedName.split('.'); const target = Object.keys(this._csdl.$Reference) .map((key) => { return this._csdl.$Reference[key].$Include.find(item => item.$Alias === alias); }) .filter(item => item != null); return `${target[0].$Namespace}.${type}`; } getSchemas() { return this._schemas; } _getNode(name, kind) { let _name = name; const targetSchema = this._schemas.find( schema => name.startsWith(schema) || name.startsWith(this._csdl[schema].$Alias) ); if (targetSchema == null) return null; let target = this._csdl; if (name.startsWith(targetSchema)) { _name = name.substring(targetSchema.length + 1); target = target[targetSchema]; } if (name.startsWith(this._csdl[targetSchema].$Alias)) { _name = name.substring(this._csdl[targetSchema].$Alias.length + 1); target = target[targetSchema]; } let realName; _name.split('.').map(namePart => namePart.split('#')[0]).forEach((pathElem) => { if (target != null) { realName = pathElem; target = target[pathElem]; } }); if (kind && target && target.$Kind !== kind) return null; return { name: realName, target }; } getReferenceUriForType(namespaceOrAliasWithType) { if (namespaceOrAliasWithType == null) return null; if (this._csdl.$Reference == null) return null; const namespaceOrAlias = namespaceOrAliasWithType.split('.').slice(0, -1).join('.'); return Object .keys(this._csdl.$Reference) .find((refName) => { const ref = this._csdl.$Reference[refName]; if (ref.$Include == null) return null; const result = ref.$Include.find((include) => { return include.$Alias === namespaceOrAlias || include.$Namespace === namespaceOrAlias; }); return result != null; }); } getEntityType(fqn) { const result = this._getNode(fqn, 'EntityType'); if (result && result.target) return new EntityType(this, result.name, fqn, result.target); return null; } getTypeDefinition(name) { const result = this._getNode(name, 'TypeDefinition'); if (result && result.target) return new TypeDefinition(this, result.name, result.target); return null; } getEnumType(name) { const result = this._getNode(name, 'EnumType'); if (result && result.target) return new EnumType(this, result.name, result.target); return null; } getComplexType(fqn) { const result = this._getNode(fqn, 'ComplexType'); if (result && result.target) return new ComplexType(this, result.name, fqn, result.target); return null; } getTerm(name) { const result = this._getNode(name, 'Term'); if (result && result.target) return new Term(this, result.name, result.target); return null; } } class DefaultValueConverter { constructor(target, currentDefaultValue, context) { this._target = target; this._currentDefaultValue = currentDefaultValue; this._context = context; } convert() { const target = this._target; const currentDefaultValue = this._currentDefaultValue; const context = this._context; if (currentDefaultValue == null) return null; if (target.$Type.startsWith('Edm.')) { target.$DefaultValue = Expression.convertDefaultValue(target.$Type, currentDefaultValue); return null; } context.on('pre finalize', (callback) => { context.resolveType(target.$Type).then((edmType) => { if (edmType instanceof TypeDefinition) { target.$DefaultValue = Expression.convertDefaultValue( edmType.getType().toString(), currentDefaultValue ); return callback(); } if (edmType instanceof EnumType) { target.$DefaultValue = Expression.convertDefaultValue( 'EnumType', currentDefaultValue ); return callback(); } return callback(new Error(target.$Type + ' is not supported to create a term default value')); }).catch(callback); }); return null; } } /** * The Context class is a helper/util class where an instance will be provided to each processor factory. * * @class Context * @extends {Emitter} */ class Context extends Emitter { constructor(strategy, logger = { info() { }, debug() { }, path() { }, warn() { }, error() { } }) { super(); this._strategy = strategy; this._aliases = new Map(); this._logger = logger; this._edmCache = new Map(); this._missingTypes = []; this._converterOptions = {}; } setConverterOptions(options) { this._converterOptions = options; return this; } getConverterOptions() { return this._converterOptions; } getMissingTypes() { return this._missingTypes; } setEdmCache(edmCache) { this._edmCache = edmCache; return this; } getEdmCache() { return this._edmCache; } setResult(result) { this._result = result; return this; } getResult() { return this._result; } /** * Sets an alias for a namespace * * @param {string} alias The alias * @param {string} namespace The namespace * @returns {Context} This instance of context * @memberof Context */ setAlias(alias, namespace) { this._aliases.set(alias, namespace); return this; } /** * Returns the current logger. If no logger was provided through Strategy a default logger with * empty methods is used. The logger must have the api methods: info, warn, debug, path, error. * Each method will be provided with a different amount of parameters regarding to the corresponding * log event. * * @returns {Object} An instance of a logger. * @memberof Context */ getLogger() { return this._logger; } /** * Resolves an alias to a namespace. * * @param {string} aliasToResolve The alias * @returns {string} The namespace for the alias. Null if alias was null. Alias itself if no entry could be found * @memberof Context */ resolveAlias(aliasToResolve) { if (aliasToResolve == null) return aliasToResolve; for (const [alias, namespace] of this._aliases.entries()) { if (aliasToResolve.startsWith(alias + '.')) return aliasToResolve.replace(alias, namespace); } return aliasToResolve; } /** * Resolves a namespace to an alias. * * @param {string} namespaceToResolve The namespace and type * @param {boolean} forceReplace True if any namespace should be replaced, else false (default) * @returns {string} The alias for the namespace. Null if namepsace was null. Namespace itself if no entry could be found * @memberof Context */ resolveNamespace(namespaceToResolve, forceReplace = false) { if (forceReplace) { for (const [alias, namespace] of this._aliases.entries()) { if (namespaceToResolve.indexOf(namespace) > -1) { return namespaceToResolve.replace(namespace, alias); } } } else { const [schema, type] = this._getSchemaAndType(namespaceToResolve); if (namespaceToResolve == null) return namespaceToResolve; for (const [alias, namespace] of this._aliases.entries()) { if (namespace === schema) return `${alias}.${type}`; } } return namespaceToResolve; } _getSchemaAndType(typeFqnString) { const lastIndex = typeFqnString.lastIndexOf('.'); const schema = typeFqnString.substring(0, lastIndex); const type = typeFqnString.substring(lastIndex + 1); return [schema, type]; } _getEdm(typeFqn, edmCache, callback) { this.getLogger().path(`Entering Context.getEdm(${typeFqn}, callback)...`); const [schema, type] = this._getSchemaAndType(typeFqn); if (edmCache.has(schema)) { this.getLogger().debug(`Edm for '${typeFqn}' found local in cache`); return callback(null, edmCache.get(schema)); } const currentResult = this.getResult(); const isInCurrentEdm = currentResult != null && currentResult[schema] != null && currentResult[schema][type] != null; if (isInCurrentEdm === true) { const edm = new JsonEdm(currentResult); edmCache.set(schema, edm); this.getLogger().debug(`Edm for '${typeFqn}' found in current local interpretation`); return callback(null, edm); } const ns = typeFqn.split('.').slice(0, -1).join('.').toLowerCase(); const vocabulary = vocabulariesMap.get(ns); if (vocabulary) { this.getLogger().debug(`Create and cache edm for '${typeFqn}'`); const edm = new JsonEdm(vocabulary()); edmCache.set(schema, edm); return callback(null, edm); } const referenceUri = new JsonEdm(currentResult).getReferenceUriForType(typeFqn); const namespace = typeFqn.split('.').slice(0, -1).join('.'); return this._strategy.getMetadataFactory()(namespace, referenceUri, (error, metadata) => { const logger = this.getLogger(); logger.path(`Entering Strategy.getMetadataFactory()(${namespace}, callback)...`); if (error) return callback(error); if (metadata == null) { const missingTypes = this.getMissingTypes(); const existingMissingType = missingTypes.find(missing => missing.namespace === namespace); if (existingMissingType == null) missingTypes.push({ namespace, uri: referenceUri }); return callback(); } logger.debug(`Parsing provided metadata for '${typeFqn}': `, metadata); let metadataAst = metadata; if (typeof metadata === 'string') { metadataAst = this._strategy.getASTFactory()(metadata); } return new MetadataConverter({}).addConversion([ MetadataConverter.createOdataV4MetadataXmlToOdataV4CsdlStrategy() .setASTFactory(this._strategy.getASTFactory()) .setXmlNodeFactory(this._strategy.getXmlNodeFactory()) .setMetadataFactory(this._strategy.getMetadataFactory()) .setLogger(this.getLogger()) .setEdmCache(this.getEdmCache()) .use('http://docs.oasis-open.org/odata/ns/edm:Annotation', () => null) .use('http://docs.oasis-open.org/odata/ns/edm:Annotations', () => null) ]).execute(metadataAst, (conversionError, result) => { if (conversionError) return callback(conversionError); this.getLogger().debug(`Create and cache edm for '${typeFqn}'`); const edm = new JsonEdm(result); edmCache.set(schema, edm); return callback(null, edm); }); }); } /** * Resolves type to a corresponding edm object. This method can also resolve the type through * dependent metadata documents which then must be provided through {@link DefaultXmlStrategy#setMetadataFactory} factory * implementation. * * @param {string} type The name of the type to resolve * @returns {Promise<Named, Error>} Resolves to an EDM object or rejects to an error * @memberof Context */ resolveType(type) { return new Promise((resolve, reject) => { const typeFqn = this.resolveAlias(type); const edmCache = this.getEdmCache(); return this._getEdm(typeFqn, edmCache, (error, edm) => { if (error) return reject(error); if (edm == null) return resolve(); let artifact = edm.getEntityType(typeFqn) || edm.getComplexType(typeFqn) || edm.getEnumType(typeFqn); if (artifact != null) return resolve(artifact); artifact = edm.getTerm(typeFqn) || edm.getTypeDefinition(typeFqn); if (artifact == null) { throw new Error( `Could not find artifact '${typeFqn}' in provided EDM Schemas '${edm.getSchemas().join(',')}'` ); } const edmArtifactType = artifact.getType(); if (edmArtifactType == null) { const artifactType = artifact.getTarget().$Type; const fqnType = edm.toFqn(artifactType); this.resolveType(fqnType).then((resolvedType) => { artifact.setType(resolvedType); resolve(artifact); }).catch(reject); } else { resolve(artifact); } return undefined; }); }); } } MetadataConverter.Context = Context; /** * This callback is displayed as part of the {@link DefaultXmlStrategy#getMetadataFactory}. * This callback is provided as a parameter of the metadata factory. * It must be called to provide the metadata abstract syntax tree for the given type. * * @callback DefaultXmlStrategy~metadataFactoryCallback * @param {Error} error Provide an error the type can not be found else null * @param {Object} result The metadata abstract syntax tree of the depended metadata document */ /** * This callback is displayed as part of the DefaultXmlStrategy class. * This factory is called when the converter can not find a needed type within the current metadata document. * This factory then must provide the metadata abstract syntax tree where the depended type exists in. * * @callback DefaultXmlStrategy~metadataFactory * @param {String} type The full qualified type name which does not exist in the current metadata document * @param {DefaultXmlStrategy~metadataFactoryCallback} callback To be called with the new metadata abstract * syntax tree. */ /** * This callback is displayed as part of the DefaultXmlStrategy class. * This factory is called on each element visiting while convertig the abstract syntax tree. This factory * is called with each and every element and must convert the provided element into an expected structure * like this: { name: 'The name of the element node without <>' // Like 'Annotation' for <Annotation>...</Annotation> attributes: { Name: 'The attribute value', AnotherAttribute: 'Another value' }, elements: [ { attributes: {...}, elements: [...] }, ... ] } The result must be returned via 'return' statement. * * @callback DefaultXmlStrategy~nodeBuilder * @param {Object} element The element which is currently visited. Starting with the first 'input' element * @returns {Object} The converted element node must be returned * syntax tree. */ /** * This factory is displayed as part of the DefaultXmlStrategy class. * This factory is called on each odata known node to create a corresponding {@link Expression} object * for interpretation. * * @callback DefaultXmlStrategy~useFactory * @param {Object} element The current element to interpret * @param {Expression} parentExpression The parent expression. Can be null if the element is the root node. * @param {Context} context A context object with some helper/util methods * @returns {Expression} The expression to interpret the current element node */ /** * This callback is displayed as part of the DefaultXmlStrategy class. * This callback is called on finishing a strategy. * * @callback DefaultXmlStrategy~executeCallback * @param {Error} error An error if occurs or null if not * @param {Any} result The output of the Strategy */ class DefaultXmlStrategy extends Strategy { constructor(options) { super(undefined, options); this._nodeProcessorMap = new Map(); this.setXmlNodeFactory((element) => element); this.setMetadataFactory((path, uri, callback) => { let message = `No metadata factory provided. Can not resolve '${path}'.`; message += ' Please add one via Strategy.setMetadataFactory(function(type, callback){})'; callback(new Error(message)); }); this.setASTFactory(() => { throw new Error('No abstract syntax tree factory provided.'); }); } /** * This method sets the strategy to provide a metadata abstract syntax tree for a given type with * full qualified name. * * @param {DefaultXmlStrategy~metadataFactory} factory The factory to set * @returns {DefaultXmlStrategy} This instance of DefaultXmlStrategy * @memberof DefaultXmlStrategy */ setMetadataFactory(factory) { this._metadataFactory = factory; return this; } getMetadataFactory() { return this._metadataFactory; } setASTFactory(astFactory) { this._astFactory = astFactory; return this; } getASTFactory() { return this._astFactory; } setEdmCache(edmCache) { this._edmCache = edmCache; return this; } getEdmCache() { return this._edmCache; } /** * Sets the node factory to create the expected abstract syntax tree structure for each element. * The default is returning each element as is. * * @param {DefaultXmlStrategy~nodeBuilder} nodeFactory The factory to create abstract syntax tree nodes * @returns {DefaultXmlStrategy} This instance of DefaultXmlStrategy * @memberof DefaultXmlStrategy */ setXmlNodeFactory(nodeFactory) { this._nodeFactory = nodeFactory; return this; } getXmlNodeFactory() { return this._nodeFactory; } /** * This method registers a processor factory which will be called on each odata known node. This factory must provide * a correspnding {@link Expression} object which will be used to interpret the current node. * The factory will be called with following params * * @param {string} name The case senstive name of the xml node prefixed with the corresponding odata namesapce. * Example: 'http://docs.oasis-open.org/odata/ns/edm:Annotation' for Annotation nodes. * Example: 'http://docs.oasis-open.org/odata/ns/edmx:DataServices' for DataServices nodes. * @param {DefaultXmlStrategy~useFactory} callback The factory to create an {@link Expression} for a provided * element node * @returns {DefaultXmlStrategy} This instance of DefaultXmlStrategy * @memberof DefaultXmlStrategy */ use(name, callback) { this._nodeProcessorMap.set(name, callback); return this; } /** * Executes this strategy. * * @param {any} input Any input from {@link MetadataConverter#execute} if this is the root strategy else the output * from a previous executed strategy. * @param {DefaultXmlStrategy~executeCallback} callback The callback to be called on finishing the Strategy * @param {MetadataConverter} converterInstance The metadata converter instance * @memberof DefaultXmlStrategy */ execute(input, callback, converterInstance) { // callback(error, result) -> result = conversion result const logger = this.getLogger(); if (logger) logger.path('Entering MetadataConverter.DefaultXmlStrategy.execute()...'); const processElements = (elements, parentExpression, context) => { for (let index = 0; index < elements.length; index++) { const element = this._nodeFactory(elements[index]); let nodeName = element.name; let prefix = null; if (element && element.name) { if (element.name.indexOf(':') > -1) [prefix, nodeName] = element.name.split(':'); } const namespace = Expression.lookupXmlNamespace(element, parentExpression, prefix, logger); let isOdataElement = false; let processor = null; if (namespace != null) { if (logger) logger.debug(`Found namespace '${namespace}'`); isOdataElement = ODATA_NAMESPACES.v4.includes(namespace); if (isOdataElement === true) { const processorName = `${namespace}:${nodeName}`; if (logger) logger.debug(`Loading processor: '${processorName}'`); processor = this._nodeProcessorMap.get(processorName); } } else if (logger) logger.warn(`No namespace found for element name '${nodeName}'`); if (processor) { if (logger) { const name = element.name; logger.debug( `Start processing of element ${name}: ${JSON.stringify(element.attributes, null, 2)}` ); } let target = element; const childExpression = processor(target, parentExpression, context); if (childExpression) { parentExpression.getStack().push(childExpression); if (target.elements) { processElements(target.elements, childExpression, context); } } } else if (logger) logger.warn('No processor found'); } return parentExpression; }; let _input = input; if (typeof input === 'string') { _input = this.getASTFactory()(input); } const context = new Context(this, logger) .setConverterOptions(converterInstance.getOptions()) .setEdmCache(this.getEdmCache() || new Map()); const rootNode = this._nodeFactory(_input); const root = processElements(rootNode.elements, new Expression(rootNode, null, context), context); const result = root.interpret(); context .on('error', (error) => { callback(error, result, context.getMissingTypes()); }) .on('post finalize', () => { const missingTypes = context.getMissingTypes(); let error = null; if (missingTypes.length > 0) { error = new Error('Could not convert document: Missing referenced documents'); error._missingReferences = context.getMissingTypes(); } callback(error, result, missingTypes); }); context.setResult(result); context.emit('finalize', result); } } MetadataConverter.DefaultXmlStrategy = DefaultXmlStrategy; class EdmxRootExpression extends Expression { constructor(element, expression, context) { super(element, expression, context); } interpret() { const target = { $Version: this.getElement().attributes.Version }; this.setTarget(target); this.getStack() .map((expr) => { return { type: expr.getType(), result: expr.interpret() }; }) .filter(interpretation => interpretation.result != null) .forEach((interpretation) => { if (interpretation.type === 'ReferenceExpression') { if (target.$Reference == null) { target.$Reference = {}; } Object.assign(target.$Reference, interpretation.result.$Reference); } else { Object.assign(target, interpretation.result); } }); return target; } } class ReferenceIncludeExpression extends Expression { constructor(element, expression, context) { super(element, expression, context); } annotate(result) { super.annotate(undefined, this.getTarget(), result); } interpret() { const target = {}; this.setTarget(target); this.buildTypeCollectionAndFacets('$Namespace', '$Alias'); super.interpret(); return target; } } class StringExpression extends Expression { constructor(element, expression, context) { super(element, expression, context); } interpret() { const element = this.getElement(); if (element.elements == null || element