UNPKG

@sap/odata-v4

Version:

OData V4.0 server library

282 lines (249 loc) 12.7 kB
'use strict'; const TokenKind = require('./UriTokenizer').TokenKind; const EdmTypeKind = require('../edm/EdmType').TypeKind; const FullQualifiedName = require('../FullQualifiedName'); const UriResource = require('./UriResource'); const ResourceKind = UriResource.ResourceKind; const SelectItem = require('./SelectItem'); const UriQueryOptionSemanticError = require('../errors/UriQueryOptionSemanticError'); const UriSemanticError = require('../errors/UriSemanticError'); const UriSyntaxError = require('../errors/UriSyntaxError'); const FeatureSupport = require('../FeatureSupport'); class SelectParser { /** * Create a select parser. * @param {Edm} edm entity data model */ constructor(edm) { this._edm = edm; } /** * Parse a select string into an array of select items. * @param {UriTokenizer} tokenizer tokenizer containing the string to be parsed * @param {?EdmType} referencedType type the expression references * @param {boolean} referencedIsCollection whether a collection is referenced * @returns {SelectItem[]} the parsed items * @throws {UriSyntaxError} * @throws {UriQueryOptionSemanticError} */ parse(tokenizer, referencedType, referencedIsCollection) { let selectItems = []; do { selectItems.push(this._parseItem(tokenizer, referencedType, referencedIsCollection)); } while (tokenizer.next(TokenKind.COMMA)); return selectItems; } /** * Parse a select string into a select item. * @param {UriTokenizer} tokenizer tokenizer containing the string to be parsed * @param {?EdmType} referencedType type the expression references * @param {boolean} referencedIsCollection whether a collection is referenced * @returns {SelectItem} the parsed item * @throws {UriSyntaxError} * @throws {UriQueryOptionSemanticError} * @private */ _parseItem(tokenizer, referencedType, referencedIsCollection) { let item = new SelectItem(); let pathSegments = []; if (tokenizer.next(TokenKind.STAR)) { item.setAll(true); } else if (tokenizer.next(TokenKind.QualifiedName)) { // The namespace or its alias could consist of dot-separated OData identifiers. const allOperationsInSchema = this._parseAllOperationsInSchema(tokenizer); if (allOperationsInSchema) { item.setAllOperationsInSchema(allOperationsInSchema); } else { if (referencedType == null) { throw new UriQueryOptionSemanticError(UriQueryOptionSemanticError.Message.REFERENCED_NOT_TYPED); } const qualifiedName = FullQualifiedName.createFromNameSpaceAndName(tokenizer.getText()); const type = this._edm.getEntityType(qualifiedName) || this._edm.getComplexType(qualifiedName); if (type == null) { pathSegments.push( this._parseBoundOperation(tokenizer, qualifiedName, referencedType, referencedIsCollection)); } else { /** ********************************************************** * Type cast in $select is currently not supported *************************************************************/ FeatureSupport .failUnsupported(FeatureSupport.features.TypeCast, qualifiedName, tokenizer.getPosition()); // ************************************************************ if (type.compatibleTo(referencedType)) { pathSegments.push(new UriResource().setKind(ResourceKind.TYPE_CAST).setTypeCast(type)); if (tokenizer.next(TokenKind.SLASH)) { tokenizer.requireNext(TokenKind.ODataIdentifier); this._addSelectPath(tokenizer, type, pathSegments); } } else { throw new UriQueryOptionSemanticError(UriSemanticError.Message.INCOMPATIBLE_TYPE, qualifiedName.toString(), referencedType.getFullQualifiedName().toString()); } } } } else { tokenizer.requireNext(TokenKind.ODataIdentifier); // The namespace or its alias could be a single OData identifier. let allOperationsInSchema = this._parseAllOperationsInSchema(tokenizer); if (allOperationsInSchema) { item.setAllOperationsInSchema(allOperationsInSchema); } else { if (referencedType == null) { throw new UriQueryOptionSemanticError(UriQueryOptionSemanticError.Message.REFERENCED_NOT_TYPED); } this._addSelectPath(tokenizer, referencedType, pathSegments); } } item.setPathSegments(pathSegments); return item; } /** * Parse a namespace plus dot plus star indicating all operations in the schema are selected. * @param {UriTokenizer} tokenizer tokenizer containing the string to be parsed * @returns {?string} the parsed namespace * @throws {UriSyntaxError} * @throws {UriQueryOptionSemanticError} * @private */ _parseAllOperationsInSchema(tokenizer) { const namespace = tokenizer.getText(); if (tokenizer.next(TokenKind.DOT)) { tokenizer.requireNext(TokenKind.STAR); // Validate the namespace. Currently a namespace from a non-default schema is not supported. // There is no direct access to the namespace without loading the whole schema; // however, the default entity container should always be there, so its access methods can be used. if (this._edm.getEntityContainer( new FullQualifiedName(namespace, this._edm.getEntityContainer().getName())) == null) { throw new UriQueryOptionSemanticError(UriQueryOptionSemanticError.Message.WRONG_NAMESPACE, namespace); } return namespace; } return null; } /** * Parse a bound operation. * @param {UriTokenizer} tokenizer tokenizer containing the string to be parsed * @param {FullQualifiedName} qualifiedName the qualified name of the operation * @param {?EdmType} referencedType type the expression references * @param {boolean} referencedIsCollection whether a collection is referenced * @returns {UriResource} a resource element describing the parsed operation * @throws {UriQueryOptionSemanticError} * @private */ _parseBoundOperation(tokenizer, qualifiedName, referencedType, referencedIsCollection) { const boundAction = this._edm.getBoundAction(qualifiedName, referencedType.getFullQualifiedName(), referencedIsCollection); if (boundAction) { return new UriResource() .setKind(ResourceKind.BOUND_ACTION) .setAction(boundAction); } const parameterNames = this._parseFunctionParameterNames(tokenizer); const boundFunction = this._edm.getBoundFunction(qualifiedName, referencedType.getFullQualifiedName(), referencedIsCollection, parameterNames); if (boundFunction == null) { throw new UriQueryOptionSemanticError(UriSemanticError.Message.FUNCTION_NOT_FOUND, qualifiedName, parameterNames.join(', ')); } return new UriResource() .setKind(ResourceKind.BOUND_FUNCTION) .setFunction(boundFunction) .setFunctionParameters(null); } /** * Parse a comma-separated list of function-parameter names enclosed in parentheses. * @param {UriTokenizer} tokenizer tokenizer containing the string to be parsed * @returns {string[]} the parsed names * @throws {UriSyntaxError} * @private */ _parseFunctionParameterNames(tokenizer) { let names = []; if (tokenizer.next(TokenKind.OPEN)) { do { tokenizer.requireNext(TokenKind.ODataIdentifier); names.push(tokenizer.getText()); } while (tokenizer.next(TokenKind.COMMA)); tokenizer.requireNext(TokenKind.CLOSE); } return names; } /** * Add more to the select path. * @param {UriTokenizer} tokenizer tokenizer containing the string to be parsed * @param {?EdmType} referencedType type the expression references * @param {UriResource[]} path the path segments parsed so far where more will be added to * @throws {UriSyntaxError} * @throws {UriQueryOptionSemanticError} * @private */ _addSelectPath(tokenizer, referencedType, path) { const name = tokenizer.getText(); const property = referencedType.getStructuralProperty(name); if (property == null) { const navigationProperty = referencedType.getNavigationProperty(name); if (navigationProperty == null) { throw new UriQueryOptionSemanticError(UriSemanticError.Message.PROPERTY_NOT_FOUND, name, referencedType.getName()); } path.push(new UriResource().setNavigationProperty(navigationProperty) .setKind(ResourceKind.NAVIGATION_TO_ONE) .setIsCollection(navigationProperty.isCollection())); } else { let kind = null; switch (property.getType().getKind()) { case EdmTypeKind.PRIMITIVE: case EdmTypeKind.ENUM: case EdmTypeKind.DEFINITION: kind = property.isCollection() ? ResourceKind.PRIMITIVE_COLLECTION_PROPERTY : ResourceKind.PRIMITIVE_PROPERTY; break; case EdmTypeKind.COMPLEX: kind = property.isCollection() ? ResourceKind.COMPLEX_COLLECTION_PROPERTY : ResourceKind.COMPLEX_PROPERTY; break; default: } path.push(new UriResource().setProperty(property).setKind(kind).setIsCollection(property.isCollection())); if (property.getType().getKind() === EdmTypeKind.COMPLEX && tokenizer.next(TokenKind.SLASH)) { if (tokenizer.next(TokenKind.QualifiedName)) { const qualifiedName = FullQualifiedName.createFromNameSpaceAndName(tokenizer.getText()); const type = this._edm.getComplexType(qualifiedName); if (type == null) { throw new UriQueryOptionSemanticError( UriQueryOptionSemanticError.Message.COMPLEX_TYPE_NOT_FOUND, qualifiedName.toString()); } /** ********************************************************** * Type cast in $select is currently not supported *************************************************************/ FeatureSupport .failUnsupported(FeatureSupport.features.TypeCast, qualifiedName, tokenizer.getPosition()); // ************************************************************ if (type.compatibleTo(property.getType())) { path.push(new UriResource() .setKind(ResourceKind.TYPE_CAST) .setTypeCast(type)); if (tokenizer.next(TokenKind.SLASH)) { tokenizer.requireNext(TokenKind.ODataIdentifier); this._addSelectPath(tokenizer, type, path); } } else { throw new UriQueryOptionSemanticError(UriSemanticError.Message.INCOMPATIBLE_TYPE, qualifiedName.toString(), property.getType().getFullQualifiedName().toString()); } } else if (tokenizer.next(TokenKind.ODataIdentifier)) { this._addSelectPath(tokenizer, property.getType(), path); } else { throw new UriSyntaxError(UriSyntaxError.Message.TOKEN_KINDS_EXPECTED, [TokenKind.QualifiedName, TokenKind.ODataIdentifier].join(', '), tokenizer.getParseString(), tokenizer.getPosition()); } } } } } module.exports = SelectParser;