@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
JavaScript
'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