@sap/odata-v4
Version:
OData V4.0 server library
1,262 lines (1,070 loc) • 53.3 kB
JavaScript
'use strict';
const CsdlProvider = require('./CsdlProvider');
const CsdlEntityContainerInfo = require('../csdl/CsdlEntityContainerInfo');
const CsdlEntityContainer = require('../csdl/CsdlEntityContainer');
const CsdlEntitySet = require('../csdl/CsdlEntitySet');
const CsdlNavigationPropertyBinding = require('../csdl/CsdlNavigationPropertyBinding');
const CsdlSingleton = require('../csdl/CsdlSingleton');
const CsdlActionImport = require('../csdl/CsdlActionImport');
const CsdlFunctionImport = require('../csdl/CsdlFunctionImport');
const CsdlSchema = require('../csdl/CsdlSchema');
const CsdlEnumType = require('../csdl/CsdlEnumType');
const CsdlEnumMember = require('../csdl/CsdlEnumMember');
const CsdlTypeDefinition = require('../csdl/CsdlTypeDefinition');
const CsdlEntityType = require('../csdl/CsdlEntityType');
const CsdlPropertyRef = require('../csdl/CsdlPropertyRef');
const CsdlProperty = require('../csdl/CsdlProperty');
const CsdlNavigationProperty = require('../csdl/CsdlNavigationProperty');
const CsdlReferentialConstraint = require('../csdl/CsdlReferentialConstraint');
const CsdlComplexType = require('../csdl/CsdlComplexType');
const CsdlAction = require('../csdl/CsdlAction');
const CsdlFunction = require('../csdl/CsdlFunction');
const CsdlParameter = require('../csdl/CsdlParameter');
const CsdlReturnType = require('../csdl/CsdlReturnType');
const CsdlTerm = require('../csdl/CsdlTerm');
const CsdlAliasInfo = require('../csdl/CsdlAliasInfo');
const CsdlOnDelete = require('../csdl/CsdlOnDelete');
const CsdlReference = require('../csdl/CsdlReference');
const CsdlInclude = require('../csdl/CsdlInclude');
const CsdlAnnotation = require('../csdl/CsdlAnnotation');
const CsdlAnnotations = require('../csdl/CsdlAnnotations');
const CsdlConstantExpression = require('../csdl/annotationExpression/CsdlConstantExpression');
const ConstantExpressionType = CsdlConstantExpression.Types;
const CsdlNotExpression = require('../csdl/annotationExpression/CsdlNotExpression');
const CsdlBinaryExpression = require('../csdl/annotationExpression/CsdlBinaryExpression');
const LogicalOperators = CsdlBinaryExpression.LogicalOperators;
const ComparisonOperators = CsdlBinaryExpression.ComparisonOperators;
const CsdlArithmeticExpression = require('../csdl/annotationExpression/CsdlArithmeticExpression');
const CsdlNegationExpression = require('../csdl/annotationExpression/CsdlNegationExpression');
const CsdlAnnotationPathExpression = require('../csdl/annotationExpression/CsdlAnnotationPathExpression');
const CsdlApplyExpression = require('../csdl/annotationExpression/CsdlApplyExpression');
const CsdlCastExpression = require('../csdl/annotationExpression/CsdlCastExpression');
const CsdlCollectionExpression = require('../csdl/annotationExpression/CsdlCollectionExpression');
const CsdlIfExpression = require('../csdl/annotationExpression/CsdlIfExpression');
const CsdlIsOfExpression = require('../csdl/annotationExpression/CsdlIsOfExpression');
const CsdlLabeledElementExpression = require('../csdl/annotationExpression/CsdlLabeledElementExpression');
const CsdlLabeledElementReferenceExpression =
require('../csdl/annotationExpression/CsdlLabeledElementReferenceExpression');
const CsdlModelElementPathExpression = require('../csdl/annotationExpression/CsdlModelElementPathExpression');
const CsdlPathExpression = require('../csdl/annotationExpression/CsdlPathExpression');
const CsdlNullExpression = require('../csdl/annotationExpression/CsdlNullExpression');
const CsdlNavigationPropertyPathExpression =
require('../csdl/annotationExpression/CsdlNavigationPropertyPathExpression');
const CsdlPropertyPathExpression = require('../csdl/annotationExpression/CsdlPropertyPathExpression');
const CsdlRecordExpression = require('../csdl/annotationExpression/CsdlRecordExpression');
const CsdlPropertyValueExpression = require('../csdl/annotationExpression/CsdlPropertyValueExpression');
const CsdlUrlRefExpression = require('../csdl/annotationExpression/CsdlUrlRefExpression');
const FullQualifiedName = require('../FullQualifiedName');
const validateThat = require('../validator/ParameterValidator').validateThat;
const defaultEdmTypeFQN = new FullQualifiedName('Edm', 'String');
class AnnotationTokenizer {
constructor(input) {
this._input = input;
this._result = null;
this._target = null;
this._index = 0;
}
getTarget() { return this._target; }
next() {
if (this._result == null) {
this._result = this._input.split('@')
.filter(elem => elem !== '')
.map((element1) => {
return element1.split('#').filter(element2 => element2 !== '');
});
if (this._input.startsWith('@') === false) {
this._target = this._result.shift()[0];
}
}
if (this._index >= this._result.length) {
return [null, null];
}
const result = this._result[this._index];
this._index++;
return result;
}
}
class AnnotationParser {
constructor(options = { target: null }) { this._options = options; }
parse(element, annotationBuilder, annotations = []) {
const keys = Object.keys(element).filter(key => key.includes('@'));
const findExistingAnnotation = (annotationArray, term) => {
return annotationArray.find(an => an.toString() === term);
};
for (const key of keys) {
let currentNode = null;
let root = null;
// @Annotation+, Property@Annotation+, Property@Annotation+#Qualifier
const tokenizer = new AnnotationTokenizer(key);
let [term, qualifier] = tokenizer.next();
const target = tokenizer.getTarget();
if (target === this._options.target) {
const path = [target]; // If target is null, join() will left out target
do {
if (term != null) {
const termAndQualifier = term + (qualifier && qualifier.length > 0 ? '#' + qualifier : '');
path.push(termAndQualifier);
const value = element[path.join('@')];
if (root == null) {
root = findExistingAnnotation(annotations, termAndQualifier);
if (root == null) {
root = annotationBuilder(term, qualifier, value);
annotations.push(root);
}
currentNode = root;
} else {
const existingAnnotation = findExistingAnnotation(
currentNode.annotations, termAndQualifier
);
if (existingAnnotation == null) {
const newAnnotation = annotationBuilder(term, qualifier, value);
currentNode.annotations.push(newAnnotation);
currentNode = newAnnotation;
} else {
currentNode = existingAnnotation;
}
}
}
[term, qualifier] = tokenizer.next();
} while (term != null);
}
}
// return an array;
return annotations;
}
}
/**
* CSDL Provider from a JSON structure as input
* @extends CsdlProvider
*/
class CsdlJsonProvider extends CsdlProvider {
/**
* @param {Object} edmJson - The input JSON structure
*/
constructor(edmJson) {
validateThat('edmJson', edmJson).truthy().typeOf('object');
super();
/**
* The input JSON structure
* @private
* @type {Object}
*/
this._edmJson = edmJson;
/**
* the qualified name of the entity container
* @private
* @type {FullQualifiedName}
*/
if (edmJson.$EntityContainer) {
this._containerName = FullQualifiedName.createFromNameSpaceAndName(edmJson.$EntityContainer);
}
/**
* the entity-container part of the input JSON structure
* @private
* @type {Object}
*/
Object.keys(edmJson).filter(key => !key.startsWith('$')).forEach((key) => {
this._containerJson = edmJson[key];
});
}
/**
* Returns a new CsdlEntityContainerInfo of the given entity container name,
* or null if the given container name does not match this container name.
*
* @param {FullQualifiedName} entityContainerName
* @returns {?CsdlEntityContainerInfo}
* @override
*/
getEntityContainerInfo(entityContainerName) {
if (entityContainerName == null) {
return new CsdlEntityContainerInfo(this._containerName);
}
const value = this._getPropertyValue(this._edmJson, entityContainerName.namespace, entityContainerName.name);
if (value == null) {
return null;
}
return new CsdlEntityContainerInfo(entityContainerName);
}
/**
* Returns the value found by a path lookup in the provided object.
*
* @private
* @param {Objec} data Any object to lookup the value
* @param {...string} path The property path's
* @returns {?Object} The value found or null
*/
_getPropertyValue(data, ...path) {
let context = data;
path.forEach((elem) => {
if (context) {
context = context[elem];
}
});
return context;
}
/**
* Returns the entire entity container with all entitySets, singletons, action imports and function imports.
*@param {FullQualifiedName} containerFqnParam The full qualified name of the container
* @returns {CsdlEntityContainer}
* @override
*/
getEntityContainer(containerFqnParam) {
let containerFqn = containerFqnParam;
if (containerFqn == null) {
containerFqn = this._containerName;
}
const containerData = this._getPropertyValue(this._edmJson, containerFqn.namespace, containerFqn.name);
if (containerData == null) {
return null;
}
let entityContainer = new CsdlEntityContainer(containerFqn.name)
.setAnnotations(this._extractInlineAnnotations(containerData));
let entitySets = [];
let singletons = [];
let actionImports = [];
let functionImports = [];
for (const name of Object.keys(containerData)) {
if (this.getEntitySet(containerFqn, name) != null) {
entitySets.push(this.getEntitySet(containerFqn, name));
} else if (this.getActionImport(containerFqn, name) != null) {
actionImports.push(this.getActionImport(containerFqn, name));
} else if (this.getFunctionImport(containerFqn, name) != null) {
functionImports.push(this.getFunctionImport(containerFqn, name));
} else if (this.getSingleton(containerFqn, name)) {
singletons.push(this.getSingleton(containerFqn, name));
}
}
entityContainer.setEntitySets(entitySets);
entityContainer.setSingletons(singletons);
entityContainer.setActionImports(actionImports);
entityContainer.setFunctionImports(functionImports);
return entityContainer;
}
/**
* Returns the entitySet of the given name, or null if not found.
*
* @param {FullQualifiedName} entityContainerName - Entity container name
* @param {string} entitySetName - Entity set name
* @returns {?CsdlEntitySet}
* @override
*/
getEntitySet(entityContainerName, entitySetName) {
const containerData = this._getPropertyValue(
this._edmJson, entityContainerName.namespace, entityContainerName.name
);
if (containerData == null) {
return null;
}
let entitySetJson = containerData[entitySetName];
if (!entitySetJson) return null;
if (entitySetJson.$Kind != null) {
if (entitySetJson.$Kind !== 'EntitySet') return null;
} else {
if (entitySetJson.$Collection !== true) return null;
if (entitySetJson.$Action != null) return null;
if (entitySetJson.$Function != null) return null;
if (entitySetJson.$Type == null) return null;
}
let entitySet = new CsdlEntitySet(entitySetName, this._getTypeFQN(entitySetJson))
.setAnnotations(this._extractInlineAnnotations(entitySetJson));
if (Object.prototype.hasOwnProperty.call(entitySetJson, '$IncludeInServiceDocument')) {
entitySet.setIncludeInServiceDocument(entitySetJson.$IncludeInServiceDocument);
}
if (Object.prototype.hasOwnProperty.call(entitySetJson, '$NavigationPropertyBinding')) {
const navPropBindings = Object.keys(entitySetJson.$NavigationPropertyBinding).map(binding =>
new CsdlNavigationPropertyBinding(binding, entitySetJson.$NavigationPropertyBinding[binding]));
entitySet.setNavigationPropertyBindings(navPropBindings);
}
return entitySet;
}
/**
* Returns the singleton of the given name, or null if not found.
*
* @param {FullQualifiedName} entityContainerName - Entity container name
* @param {string} singletonName - Singleton name
* @returns {?CsdlSingleton}
* @override
*/
getSingleton(entityContainerName, singletonName) {
const containerData = this._getPropertyValue(
this._edmJson, entityContainerName.namespace, entityContainerName.name
);
if (containerData == null) {
return null;
}
let singletonJson = containerData[singletonName];
if (!singletonJson) return null;
if (singletonJson.$Kind != null) {
if (singletonJson.$Kind !== 'Singleton') return null;
} else {
if (singletonJson.$Collection != null) return null;
if (singletonJson.$Action != null) return null;
if (singletonJson.$Function != null) return null;
if (singletonJson.$Type == null) return null;
}
let singleton = new CsdlSingleton(singletonName, this._getTypeFQN(singletonJson))
.setAnnotations(this._extractInlineAnnotations(singletonJson));
if (Object.prototype.hasOwnProperty.call(singletonJson, '$NavigationPropertyBinding')) {
const navPropBindings = Object.keys(singletonJson.$NavigationPropertyBinding).map(binding =>
new CsdlNavigationPropertyBinding(binding, singletonJson.$NavigationPropertyBinding[binding]));
singleton.setNavigationPropertyBindings(navPropBindings);
}
return singleton;
}
/**
* Returns the action import of the given name, or null if not found.
*
* @param {FullQualifiedName} entityContainerName - Entity container name
* @param {string} actionImportName - Action import name
* @returns {?CsdlActionImport}
* @override
*/
getActionImport(entityContainerName, actionImportName) {
const containerData = this._getPropertyValue(
this._edmJson, entityContainerName.namespace, entityContainerName.name
);
if (containerData == null) {
return null;
}
let actionJson = containerData[actionImportName];
if (!actionJson) return null;
if (actionJson.$Action == null) return null;
let action = new CsdlActionImport(
actionImportName,
FullQualifiedName.createFromNameSpaceAndName(actionJson.$Action)
).setAnnotations(this._extractInlineAnnotations(actionJson));
if (Object.prototype.hasOwnProperty.call(actionJson, '$EntitySet')) {
action.setEntitySet(actionJson.$EntitySet);
}
return action;
}
/**
* Returns the function import of the given name, or null if not found.
*
* @param {FullQualifiedName} entityContainerName - Entity container name
* @param {string} functionImportName - Function import name
* @returns {?CsdlFunctionImport}
* @override
*/
getFunctionImport(entityContainerName, functionImportName) {
const containerData = this._getPropertyValue(
this._edmJson, entityContainerName.namespace, entityContainerName.name
);
if (containerData == null) return null;
let functionImpJson = containerData[functionImportName];
if (!functionImpJson) return null;
if (functionImpJson.$Function == null) return null;
let functionImp = new CsdlFunctionImport(
functionImportName,
FullQualifiedName.createFromNameSpaceAndName(functionImpJson.$Function)
).setAnnotations(this._extractInlineAnnotations(functionImpJson));
if (Object.prototype.hasOwnProperty.call(functionImpJson, '$EntitySet')) {
functionImp.setEntitySet(functionImpJson.$EntitySet);
}
if (Object.prototype.hasOwnProperty.call(functionImpJson, '$IncludeInServiceDocument')) {
functionImp.setIncludeInServiceDocument(functionImpJson.$IncludeInServiceDocument);
}
return functionImp;
}
/**
* Returns an array of all schemas, each with all the corresponding type
* definitions, entity types, complex types, actions and functions.
*
* @returns {CsdlSchema[]}
* @override
*/
getSchemas() {
let schemas = [];
for (const schemaName of Object.keys(this._edmJson)) {
if (schemaName.startsWith('$')) {
// exclude $Reference, $Version, $EntityContainer.
continue;
}
const schemaJson = this._edmJson[schemaName];
let schema = new CsdlSchema(schemaName);
if (Object.prototype.hasOwnProperty.call(schemaJson, '$Alias')) {
schema.setAlias(schemaJson.$Alias);
}
// set external annotations
if (Object.prototype.hasOwnProperty.call(schemaJson, '$Annotations')) {
schema.setExternalAnnotations(this._extractExternalAnnotations(schemaJson.$Annotations));
}
// set inline annotations: belong to schema
schema.setAnnotations(this._extractInlineAnnotations(schemaJson));
let actions = [];
let functions = [];
let enumTypes = [];
let typeDefinitions = [];
let entityTypes = [];
let complexTypes = [];
let terms = [];
for (const name of Object.keys(schemaJson)) {
const fqn = new FullQualifiedName(schemaName, name);
const elementJson = schemaJson[name];
if (Array.isArray(elementJson)) { // the entry then is either an action or a function
let res = this.getActions(fqn);
if (res.length !== 0) {
actions = actions.concat(res);
}
functions = functions.concat(this.getFunctions(fqn));
}
switch (elementJson.$Kind) {
case 'EnumType':
enumTypes.push(this.getEnumType(fqn));
break;
case 'TypeDefinition':
typeDefinitions.push(this.getTypeDefinition(fqn));
break;
case 'EntityType':
entityTypes.push(this.getEntityType(fqn));
break;
case 'ComplexType':
complexTypes.push(this.getComplexType(fqn));
break;
case 'Term':
terms.push(this.getTerm(fqn));
break;
case 'EntityContainer':
schema.setEntityContainer(this.getEntityContainer(fqn));
break;
default:
break;
}
}
schema.setActions(actions);
schema.setFunctions(functions);
schema.setEnumTypes(enumTypes);
schema.setTypeDefinitions(typeDefinitions);
schema.setEntityTypes(entityTypes);
schema.setComplexTypes(complexTypes);
schema.setTerms(terms);
schemas.push(schema);
}
return schemas;
}
/**
* Returns the enumeration type of the given name, or null if not found
*
* @param {FullQualifiedName} enumTypeName - enumeration-type name
* @returns {?CsdlEnumType}
* @override
*/
getEnumType(enumTypeName) {
let enumTypeJson = this._getElementJson(enumTypeName, 'EnumType');
if (enumTypeJson === null) return null;
let enumType = new CsdlEnumType(
enumTypeName.name,
FullQualifiedName.createFromNameSpaceAndName(enumTypeJson.$UnderlyingType || 'Edm.Int32')
).setAnnotations(this._extractInlineAnnotations(enumTypeJson));
if (Object.prototype.hasOwnProperty.call(enumTypeJson, '$IsFlags')) enumType.setIsFlags(enumTypeJson.$IsFlags);
enumType.setMembers(
Object.keys(enumTypeJson)
.filter(member => !member.startsWith('$') && !member.includes('@'))
.map(member => new CsdlEnumMember(member, enumTypeJson[member])
.setAnnotations(this._extractImplicitAnnotations(enumTypeJson, member))));
return enumType;
}
/**
* Returns the type definition of the given name, or null if not found
*
* @param {FullQualifiedName} typeDefName - Type definition
* @returns {?CsdlTypeDefinition}
* @override
*/
getTypeDefinition(typeDefName) {
let typeDefJson = this._getElementJson(typeDefName, 'TypeDefinition');
if (typeDefJson === null) {
return null;
}
let typeDef = new CsdlTypeDefinition(
typeDefName.name,
FullQualifiedName.createFromNameSpaceAndName(typeDefJson.$UnderlyingType)
).setAnnotations(this._extractInlineAnnotations(typeDefJson));
this._readFacets(typeDef, typeDefJson, ['$MaxLength', '$Precision', '$Scale', '$SRID']);
return typeDef;
}
/**
* Returns the entity type of the given name, or null if not found
*
* @param {FullQualifiedName} entityTypeName - Entity type name
* @returns {?CsdlEntityType}
* @override
*/
getEntityType(entityTypeName) {
let entityTypeJson = this._getElementJson(entityTypeName, 'EntityType');
if (entityTypeJson === null) {
return null;
}
let entityType = new CsdlEntityType(entityTypeName.name)
.setAnnotations(this._extractInlineAnnotations(entityTypeJson));
if (Object.prototype.hasOwnProperty.call(entityTypeJson, '$BaseType')) {
entityType.setBaseType(
FullQualifiedName.createFromNameSpaceAndName(entityTypeJson.$BaseType)
);
}
if (Object.prototype.hasOwnProperty.call(entityTypeJson, '$HasStream')) {
entityType.setHasStream(entityTypeJson.$HasStream);
}
if (Object.prototype.hasOwnProperty.call(entityTypeJson, '$Key')) {
const key = entityTypeJson.$Key.map(value => {
if (typeof value === 'object') { // propertyRef is with an Alias
let alias = Object.keys(value)[0];
return new CsdlPropertyRef(value[alias]).setAlias(alias);
}
return new CsdlPropertyRef(value);
});
entityType.setKey(key);
}
this._setProperties(entityType, entityTypeJson);
return entityType;
}
/**
* Sets the properties and navigation properties for the given entity type or complex type.
*
* @param {CsdlEntityType | CsdlComplexType} type - the given entity type or complex type
* @param {Object} typeJson - the corresponding JSON object for the given entity type or complex type
* @private
*/
_setProperties(type, typeJson) {
let props = [];
let navProps = [];
for (const propertyName of Object.keys(typeJson)) {
if (propertyName.startsWith('$') || propertyName.includes('@')) {
// properties and navigation properties do not start with '$' and don't include '@'
continue;
}
const kind = typeJson[propertyName].$Kind;
if (!kind || kind === 'Property') {
props.push(this._createCsdlProperty(typeJson[propertyName], propertyName));
} else if (kind === 'NavigationProperty') {
navProps.push(this._createCsdlNavigationProperty(typeJson[propertyName], propertyName));
}
}
type.setProperties(props);
type.setNavigationProperties(navProps);
}
/**
* Returns the complex type of the given name, or null if not found
*
* @param {FullQualifiedName} complexTypeName - Complex type name
* @returns {?CsdlComplexType}
* @override
*/
getComplexType(complexTypeName) {
let complexTypeJson = this._getElementJson(complexTypeName, 'ComplexType');
if (complexTypeJson === null) {
return null;
}
let complexType = new CsdlComplexType(complexTypeName.name)
.setAnnotations(this._extractInlineAnnotations(complexTypeJson));
if (Object.prototype.hasOwnProperty.call(complexTypeJson, '$BaseType')) {
complexType.setBaseType(FullQualifiedName.createFromNameSpaceAndName(complexTypeJson.$BaseType));
}
this._setProperties(complexType, complexTypeJson);
return complexType;
}
/**
* Creates a CsdlProperty object of a given property in JSON format
*
* @param {JSON} propertyJson - The corresponding property as a JSON object
* @param {string} propertyName - Property name
* @returns {CsdlProperty}
* @private
*/
_createCsdlProperty(propertyJson, propertyName) {
let property = new CsdlProperty(propertyName, this._getTypeFQN(propertyJson) || defaultEdmTypeFQN)
.setAnnotations(this._extractInlineAnnotations(propertyJson));
this._readFacets(property, propertyJson,
['$Nullable', '$MaxLength', '$Precision', '$Scale', '$SRID', '$DefaultValue', '$Collection']);
return property;
}
/**
* Creates a CsdlNavigationProperty object of a given navigation property in JSON format
*
* @param {JSON} navPropertyJson - The corresponding navigation property as a JSON object
* @param {string} navPropertyName - Navigation property name
* @returns {CsdlNavigationProperty}
* @private
*/
_createCsdlNavigationProperty(navPropertyJson, navPropertyName) {
let navProperty = new CsdlNavigationProperty(navPropertyName, this._getTypeFQN(navPropertyJson))
.setAnnotations(this._extractInlineAnnotations(navPropertyJson));
this._readFacets(navProperty, navPropertyJson, ['$Collection']);
// Nullable MUST NOT be specified for a collection-valued navigation property.
if (!navProperty.isCollection) this._readFacets(navProperty, navPropertyJson, ['$Nullable']);
if (Object.prototype.hasOwnProperty.call(navPropertyJson, '$Partner')) {
navProperty.setPartner(navPropertyJson.$Partner);
}
if (Object.prototype.hasOwnProperty.call(navPropertyJson, '$ContainsTarget')) {
navProperty.setContainsTarget(navPropertyJson.$ContainsTarget);
}
if (Object.prototype.hasOwnProperty.call(navPropertyJson, '$OnDelete')) {
navProperty.setOnDelete(
new CsdlOnDelete(navPropertyJson.$OnDelete)
.setAnnotations(this._extractImplicitAnnotations(navPropertyJson, '$OnDelete')));
}
if (Object.prototype.hasOwnProperty.call(navPropertyJson, '$ReferentialConstraint')) {
let refConstraints = new Map();
for (const prop of Object.keys(navPropertyJson.$ReferentialConstraint)) {
const propValue = navPropertyJson.$ReferentialConstraint[prop];
if (!prop.includes('@')) { // TODO property refers to a dependant, and not an annotation
refConstraints.set(prop,
new CsdlReferentialConstraint(prop /* dependent */, propValue /* principal */)
);
} else {
// Annotation of a ref constraint, example:
// "$ReferentialConstraint": {
// "dependent1": "principal1",
// "dependent1@Org.OData.Core.V1.Description#q": "ReferentialConstraint annotation"
// }
const [propKey, term, qualifier] = this._splitAnnotationString(prop);
let annotation = this._createAnnotation(term, qualifier, propValue);
refConstraints.get(propKey).annotations.push(annotation);
}
}
navProperty.setReferentialConstraints([...refConstraints.values()]);
}
return navProperty;
}
/**
* Creates and returns a CsdlAnnotation object based on input parameters.
*
* @param {string} termFqn - Term name as a full-qualified string
* @param {string} qualifier - Term qualifier
* @param {Object} expression - The value of the annotation
* @returns {CsdlAnnotation}
* @private
*/
_createAnnotation(termFqn, qualifier, expression) {
let annotation = new CsdlAnnotation(FullQualifiedName.createFromNameSpaceAndName(termFqn))
.setExpression(this._interpretAnnotationExpression(expression));
if (qualifier) annotation.setQualifier(qualifier);
return annotation;
}
/**
* Splits the string of an annotation of element / property / annotation, and returns an array of the parts.
* Property annotation: propertyKey@term#qualifier => return [propertyKey, term, qualifier]
* Annotation annotation: @annotationKey@term#qualifier => return [@annotationKey, term, qualifier]
* Element annotation: @term#qualifier => return [term, qualifier]
*
* @param {string} annStr - Input annotation string.
* @returns {string[]}
* @private
*/
_splitAnnotationString(annStr) {
let res = [];
if (annStr.startsWith('@')) {
if (annStr.lastIndexOf('@') === 0) { // element annotation
res = annStr.substring(1).split('#');
} else { // annotation annotation
let [propKey, termq] = annStr.substring(1).split('@');
propKey = annStr.charAt(0) + propKey;
const [term, qualifier] = termq.split('#');
res = [propKey, term, qualifier];
}
} else { // property annotation
let [propKey, termq] = annStr.split('@');
const [term, qualifier] = termq.split('#');
res = [propKey, term, qualifier];
}
return res;
}
/**
* Returns the array of actions of the given name, or null if not found
*
* @param {FullQualifiedName} actionName - Action name
* @returns {?(CsdlAction[])}
* @override
*/
getActions(actionName) {
if (!this._edmJson[actionName.namespace]) {
return null;
}
let actionsJson = this._edmJson[actionName.namespace][actionName.name];
if (!actionsJson || !Array.isArray(actionsJson)) {
return null;
}
let actions = [];
for (const actionObj of actionsJson) {
if (actionObj.$Kind !== 'Action') {
continue;
}
let action = new CsdlAction(actionName.name)
.setAnnotations(this._extractInlineAnnotations(actionObj));
if (Object.prototype.hasOwnProperty.call(actionObj, '$IsBound')) {
action.setBound(actionObj.$IsBound);
}
if (Object.prototype.hasOwnProperty.call(actionObj, '$EntitySetPath')) {
action.setEntitySetPath(actionObj.$EntitySetPath);
}
if (Object.prototype.hasOwnProperty.call(actionObj, '$Parameter')) {
let parameters = actionObj.$Parameter.map(this._getActionFunctionParameter, this);
action.setParameters(parameters);
}
if (Object.prototype.hasOwnProperty.call(actionObj, '$ReturnType')) {
action.setReturnType(this._getActionFunctionReturnType(actionObj.$ReturnType));
}
actions.push(action);
}
return actions;
}
/**
* Returns the array of functions of the given name, or null if not found
*
* @param {FullQualifiedName} funcName - Function name
* @returns {?(CsdlFunction[])}
* @override
*/
getFunctions(funcName) {
if (!this._edmJson[funcName.namespace]) {
return null;
}
let functionsJson = this._edmJson[funcName.namespace][funcName.name];
if (!functionsJson || !Array.isArray(functionsJson)) {
return null;
}
let functions = [];
for (const funcObj of functionsJson) {
if (funcObj.$Kind !== 'Function') {
continue;
}
let func = new CsdlFunction(
funcName.name,
this._getActionFunctionReturnType(funcObj.$ReturnType)
).setAnnotations(this._extractInlineAnnotations(funcObj));
if (Object.prototype.hasOwnProperty.call(funcObj, '$IsBound')) {
func.setBound(funcObj.$IsBound);
}
if (Object.prototype.hasOwnProperty.call(funcObj, '$IsComposable')) {
func.setComposable(funcObj.$IsComposable);
}
if (Object.prototype.hasOwnProperty.call(funcObj, '$EntitySetPath')) {
func.setEntitySetPath(funcObj.$EntitySetPath);
}
if (Object.prototype.hasOwnProperty.call(funcObj, '$Parameter')) {
let parameters = funcObj.$Parameter.map(this._getActionFunctionParameter, this);
func.setParameters(parameters);
}
functions.push(func);
}
return functions;
}
/**
* Creats a CsdlParameter object of a given action or function parameter in JSON format
*
* @param {JSON} parameterJson - The corresponding parameter as a JSON object
* @returns {CsdlParameter}
* @private
*/
_getActionFunctionParameter(parameterJson) {
let parameter = new CsdlParameter(parameterJson.$Name, this._getTypeFQN(parameterJson) || defaultEdmTypeFQN)
.setAnnotations(this._extractInlineAnnotations(parameterJson));
this._readFacets(parameter, parameterJson,
['$Collection', '$Nullable', '$MaxLength', '$Precision', '$Scale', '$SRID']);
return parameter;
}
/**
* Creats a CsdlReturnType object of a given action or function returnType in JSON format
*
* @param {JSON} returnTypeJson - The corresponding return type as a JSON object
* @returns {CsdlReturnType}
* @private
*/
_getActionFunctionReturnType(returnTypeJson) {
let returnType = new CsdlReturnType(this._getTypeFQN(returnTypeJson) || defaultEdmTypeFQN)
.setAnnotations(this._extractInlineAnnotations(returnTypeJson));
this._readFacets(returnType, returnTypeJson,
['$Collection', '$Nullable', '$MaxLength', '$Precision', '$Scale', '$SRID']);
return returnType;
}
/**
* Returns the term of the given name, or null if not found.
*
* @param {FullQualifiedName} termName - Term name
* @returns {?CsdlTerm}
* @override
*/
getTerm(termName) {
let termJson = this._getElementJson(termName, 'Term');
if (termJson === null) {
return null;
}
let term = new CsdlTerm(termName.name, this._getTypeFQN(termJson) || defaultEdmTypeFQN)
.setAnnotations(this._extractInlineAnnotations(termJson));
if (Object.prototype.hasOwnProperty.call(termJson, '$BaseTerm')) {
term.setBaseTerm(FullQualifiedName.createFromNameSpaceAndName(termJson.$BaseTerm));
}
if (Object.prototype.hasOwnProperty.call(termJson, '$AppliesTo')) {
term.setAppliesTo(termJson.$AppliesTo);
}
this._readFacets(term, termJson,
['$Collection', '$DefaultValue', '$Nullable', '$MaxLength', '$Precision', '$Scale', '$SRID']);
return term;
}
/**
* Returns an array of the defined aliases under schemas and $Reference.refUri.$include
*
* @returns {CsdlAliasInfo[]}
* @override
*/
getAliasInfos() {
let aliasInfos = [];
const reference = this._edmJson.$Reference || {};
const aliasInfoMap = new Map();
const addAliasInfo = (namespace, alias) => {
const fqn = namespace + '.' + alias;
// We filter already existing alias infos because of cross service referencing.
// Currently we 'fake' the cross service reference by including the referenced
// external model as a new namespace inside the current json model.
// Therefore the second model alias must not be part of the real alias infos.
if (aliasInfoMap.get(fqn) == null) {
const aliasInfo = new CsdlAliasInfo(namespace, alias);
aliasInfos.push(aliasInfo);
aliasInfoMap.set(aliasInfo.toString(), aliasInfo);
}
};
// get aliases from Ref-Uri.$Include
for (const refUriName of Object.keys(reference)) {
for (const include of reference[refUriName].$Include) {
addAliasInfo(include.$Namespace, include.$Alias);
}
}
// get aliases from schema
for (const name of Object.keys(this._edmJson)) {
let element = this._edmJson[name];
if (!name.startsWith('$') && Object.prototype.hasOwnProperty.call(element, '$Alias')) {
addAliasInfo(name, element.$Alias);
}
}
return aliasInfos;
}
/**
* Returns an array of all references.
*
* @returns {CsdlReference[]}
* @override
*/
getReferences() {
let references = [];
let refJson = this._edmJson.$Reference;
if (!refJson) {
return references;
}
references = Object.keys(refJson).map(uri => {
let reference = new CsdlReference(uri).setAnnotations(this._extractInlineAnnotations(refJson[uri]));
const includes = refJson[uri].$Include.map(include =>
new CsdlInclude(include.$Namespace)
.setAlias(include.$Alias)
.setAnnotations(this._extractInlineAnnotations(include))
);
reference.setIncludes(includes);
return reference;
});
return references;
}
/**
* Reads the $Annotations property (referring to OData CSDL # 14.2) of the JSON structure, e.g.
* "$Annotations": {
"targetElement": {
"@term1": "annotation value",
"@term2": "annotation value"
}
}
* and returns a corresponding array of CsdlAnnotations elements for each given target
*
* @param {Object} exAnnotationsJson - Refers to the $Annotations
* @returns {CsdlAnnotations[]} List of external annotations
*/
_extractExternalAnnotations(exAnnotationsJson) {
return Object.keys(exAnnotationsJson).map(target =>
new CsdlAnnotations(target).setAnnotations(
// annotations are written in the same syntax as inline annotations,
// e.g. "@term1": "annotation value"
// so we can use _extractInlineAnnotations to interpret them
this._extractInlineAnnotations(exAnnotationsJson[target])));
}
/**
* Interprets the given expression (in JSON format) and returns a corresponding CSDL expression object,
* e.g. CsdlConstantExpression or a specific CSDL dynamic expression.
*
* @param {*} expression - The input expression, which can be of any type
* @returns {*} CSDL expression object
* @private
*/
_interpretAnnotationExpression(expression) {
if (typeof expression === 'string') {
return new CsdlConstantExpression(ConstantExpressionType.String, expression);
} else if (typeof expression === 'boolean') {
return new CsdlConstantExpression(ConstantExpressionType.Bool, expression);
} else if (typeof expression === 'number') {
return new CsdlConstantExpression(ConstantExpressionType.Float, expression);
} else if (expression === null) {
return new CsdlNullExpression();
} else if (typeof expression === 'object') {
const expType = ['$Binary', '$Date', '$DateTimeOffset', '$Decimal', '$Duration', '$EnumMember',
'$Float', '$Guid', '$Int', '$TimeOfDay'].find(type => expression[type]);
if (!expType) {
return this._interpretDynamicExpression(expression)
.setAnnotations(this._extractInlineAnnotations(expression));
}
return new CsdlConstantExpression(
ConstantExpressionType[expType.substring(1)], // Type is the same as expType without '$'
// Binary values are saved as Buffer.
expType === '$Binary' ? Buffer.from(expression[expType], 'base64') : expression[expType])
.setAnnotations(this._extractInlineAnnotations(expression)
.concat(this._extractImplicitAnnotations(expression, expType)));
}
return null;
}
/**
* Interprets expressions when they refer to a dynamic expression,
* i.e. they are of type 'object' and do not have a constant expression property ($Binary, $Date, ..etc.),
* and returns a corresponding CSDL expression object, e.g. CsdlBinaryExpression,
* CsdlAnnotationPathExpression, ..etc
*
* @param {Object} dynExpression - The dynamic expression JSON object
* @returns {*} CSDL dynamic expression object
* @private
*/
_interpretDynamicExpression(dynExpression) {
if (Array.isArray(dynExpression)) { // Collection
return new CsdlCollectionExpression(
dynExpression.map(elem => this._interpretAnnotationExpression(elem)));
}
const expType = Object.keys(dynExpression).filter(key => !key.startsWith('@'))[0];
switch (expType) {
case '$And':
case '$Or': {
const operands = dynExpression[expType];
return new CsdlBinaryExpression(
LogicalOperators[expType.substring(1)], // And, Or
this._interpretAnnotationExpression(operands[0]), // left
this._interpretAnnotationExpression(operands[1])); // right
}
case '$Not':
return new CsdlNotExpression(this._interpretAnnotationExpression(dynExpression[expType]));
case '$Eq':
case '$Ne':
case '$Gt':
case '$Ge':
case '$Lt':
case '$Le':
case '$In': {
const operands = dynExpression[expType];
return new CsdlBinaryExpression(
ComparisonOperators[expType.substring(1)],
this._interpretAnnotationExpression(operands[0]), // left
this._interpretAnnotationExpression(operands[1])); // right
}
case '$Add':
case '$Sub':
case '$Mul':
case '$Div':
case '$DivBy':
case '$Mod': {
const operands = dynExpression[expType];
return new CsdlArithmeticExpression(expType.substring(1),
this._interpretAnnotationExpression(operands[0]), // left
this._interpretAnnotationExpression(operands[1])); // right
}
case '$Neg':
return new CsdlNegationExpression(this._interpretAnnotationExpression(dynExpression[expType]));
case '$AnnotationPath':
return new CsdlAnnotationPathExpression(dynExpression.$AnnotationPath);
case '$Apply': {
const func = dynExpression.$Function;
const params = dynExpression.$Apply.map(parameter => this._interpretAnnotationExpression(parameter));
return new CsdlApplyExpression(FullQualifiedName.createFromNameSpaceAndName(func), params);
}
case '$Cast': {
let castExpression = new CsdlCastExpression(this._getTypeFQN(dynExpression),
this._interpretAnnotationExpression(dynExpression.$Cast));
this._readFacets(castExpression, dynExpression,
['$Collection', '$MaxLength', '$Precision', '$Scale', '$SRID']);
return castExpression;
}
case '$If':
return new CsdlIfExpression(
this._interpretAnnotationExpression(dynExpression.$If[0]),
this._interpretAnnotationExpression(dynExpression.$If[1]),
this._interpretAnnotationExpression(dynExpression.$If[2]));
case '$IsOf': {
let isOf = new CsdlIsOfExpression(
this._getTypeFQN(dynExpression),
this._interpretAnnotationExpression(dynExpression.$IsOf));
this._readFacets(isOf, dynExpression, ['$Collection', '$MaxLength', '$Precision', '$Scale', '$SRID']);
return isOf;
}
case '$LabeledElement':
return new CsdlLabeledElementExpression(dynExpression.$Name,
this._interpretAnnotationExpression(dynExpression.$LabeledElement));
case '$LabeledElementReference':
return new CsdlLabeledElementReferenceExpression(
FullQualifiedName.createFromNameSpaceAndName(dynExpression.$LabeledElementReference));
case '$Null':
return new CsdlNullExpression();
case '$Path':
return new CsdlPathExpression(dynExpression.$Path);
case '$ModelElementPath':
return new CsdlModelElementPathExpression(dynExpression.$ModelElementPath);
case '$NavigationPropertyPath':
return new CsdlNavigationPropertyPathExpression(dynExpression.$NavigationPropertyPath);
case '$PropertyPath':
return new CsdlPropertyPathExpression(dynExpression.$PropertyPath);
case '$UrlRef':
return new CsdlUrlRefExpression(this._interpretAnnotationExpression(dynExpression.$UrlRef));
default: { // Record expression
let propertyValues = new Map();
let type = null;
for (const propertyName of Object.keys(dynExpression)) {
if (propertyName === '$Type') {
type = this._getTypeFQN(dynExpression);
} else if (!propertyName.includes('@')) {
// annotations of the record expression start with '@',
// but the annotations of the property values include '@' at a further position.
propertyValues.set(
propertyName,
new CsdlPropertyValueExpression(
propertyName,
this._interpretAnnotationExpression(dynExpression[propertyName]))
.setAnnotations(this._extractImplicitAnnotations(dynExpression, propertyName))
);
}
}
return new CsdlRecordExpression(type, [...propertyValues.values()]);
}
}
}
/**
* Extracts annotations of the form:
*
*{
* element: {..},
* 'element@term': '..'
* }
* @param {*} objJson
* @param {string} elementName
* @returns {Array}
* @private
*/
_extractImplicitAnnotations(objJson, elementName = null) {
let annotations = [];
new AnnotationParser({ target: elementName })
.parse(objJson, this._createAnnotation.bind(this), annotations);
return annotations;
}
/**
* Extracts the inline annotations (that start with an '@') of an element, and returns an array of CsdlAnnotation
* instances. It also extracts the inline annotations of the inline annotations. Example:
* {
$And: [
true,
false
],
'@Org.OData.Core.V1.Description': 'annotation1',
'@Org.OData.Core.V1.Description@Org.OData.Core.V1.Desc': 'annotation of annotation1'
}
*
* @param {Object} element - The input JSON element.
* @param {Array} annotations
* @retu