UNPKG

@sap/odata-v4

Version:

OData V4.0 server library

1,262 lines (1,070 loc) 53.3 kB
'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