UNPKG

@sap/odata-v4

Version:

OData V4.0 server library

354 lines (317 loc) 16.5 kB
'use strict'; const FilterParser = require('./FilterParser'); const OrderByParser = require('./OrderByParser'); const SearchParser = require('./SearchParser'); const SelectParser = require('./SelectParser'); const TokenKind = require('./UriTokenizer').TokenKind; const EdmTypeKind = require('../edm/EdmType').TypeKind; const FullQualifiedName = require('../FullQualifiedName'); const UriResource = require('./UriResource'); const ResourceKind = UriResource.ResourceKind; const ExpandItem = require('./ExpandItem'); const UriSyntaxError = require('../errors/UriSyntaxError'); const UriQueryOptionSemanticError = require('../errors/UriQueryOptionSemanticError'); const UriSemanticError = require('../errors/UriSemanticError'); const FeatureSupport = require('../FeatureSupport'); class ExpandParser { /** * Create an expand parser. * @param {Edm} edm entity data model */ constructor(edm) { this._edm = edm; } /** * Parse a string into an array of expand items. * @param {UriTokenizer} tokenizer tokenizer containing the string to be parsed * @param {?EdmType} referencedType type the expression references * @param {?(EdmEntitySet[])} crossjoinEntitySets entity sets in case of a $crossjoin request * @param {?Object} aliases alias definitions * @returns {ExpandItem[]} the parsed items * @throws {UriSyntaxError} * @throws {UriQueryOptionSemanticError} */ parse(tokenizer, referencedType, crossjoinEntitySets, aliases) { let expandItems = []; let navigationProperties = []; do { const expandItem = this._parseItem(tokenizer, referencedType, crossjoinEntitySets, aliases); const navigationSegment = expandItem.getPathSegments().find(segment => segment.getKind() === ResourceKind.NAVIGATION_TO_ONE || segment.getKind() === ResourceKind.NAVIGATION_TO_MANY); if (navigationSegment) { const navigationProperty = navigationSegment.getNavigationProperty(); if (navigationProperties.includes(navigationProperty)) { throw new UriSyntaxError(UriSyntaxError.Message.EXPAND_DUPLICATED_NAVIGATION_PROPERTY, navigationProperty.getName()); } navigationProperties.push(navigationProperty); } expandItems.push(expandItem); } while (tokenizer.next(TokenKind.COMMA)); return expandItems; } /** * Parse a string into an expand item. * @param {UriTokenizer} tokenizer tokenizer containing the string to be parsed * @param {?EdmType} referencedType type the expression references * @param {?(EdmEntitySet[])} crossjoinEntitySets entity sets in case of a $crossjoin request * @param {?Object} aliases alias definitions * @returns {ExpandItem} the parsed item * @throws {UriSyntaxError} * @throws {UriQueryOptionSemanticError} * @private */ _parseItem(tokenizer, referencedType, crossjoinEntitySets, aliases) { let item = new ExpandItem(); if (tokenizer.next(TokenKind.STAR)) { item.setAll(true); if (tokenizer.next(TokenKind.SLASH)) { tokenizer.requireNext(TokenKind.REF); FeatureSupport.failUnsupported(FeatureSupport.features.Ref); item.setPathSegments([new UriResource().setKind(ResourceKind.REF)]); } else if (tokenizer.next(TokenKind.OPEN)) { tokenizer.requireNext(TokenKind.LEVELS); const name = tokenizer.getText(); tokenizer.requireNext(TokenKind.EQ); item.setOption(name, this._parseLevels(tokenizer)); tokenizer.requireNext(TokenKind.CLOSE); FeatureSupport.failUnsupported(FeatureSupport.features.Levels); } } else { let path = this.parseExpandPath(tokenizer, referencedType, crossjoinEntitySets); if (path.length === 0) { throw new UriSyntaxError(UriSyntaxError.Message.EXPAND_NO_VALID_PATH, tokenizer.getParseString(), tokenizer.getPosition()); } let lastPart = path[path.length - 1]; let hasSlash = tokenizer.getText() === '/'; if (hasSlash || tokenizer.next(TokenKind.SLASH)) { hasSlash = true; if (lastPart.getKind() === ResourceKind.NAVIGATION_TO_ONE || lastPart.getKind() === ResourceKind.NAVIGATION_TO_MANY) { const navigationProperty = lastPart.getNavigationProperty(); const typeCastSuffix = this._parseTypeCast(tokenizer, navigationProperty.getEntityType()); if (typeCastSuffix != null) { lastPart = new UriResource() .setKind(ResourceKind.TYPE_CAST) .setTypeCast(typeCastSuffix) .setIsCollection(navigationProperty.isCollection()); path.push(lastPart); hasSlash = false; } } } const isCollection = lastPart.isCollection(); let isRef = false; let isCount = false; if (hasSlash || tokenizer.next(TokenKind.SLASH)) { if (tokenizer.next(TokenKind.REF)) { FeatureSupport.failUnsupported(FeatureSupport.features.Ref); path.push(new UriResource() .setKind(isCollection ? ResourceKind.REF_COLLECTION : ResourceKind.REF) .setIsCollection(isCollection)); isRef = true; } else if (isCollection && tokenizer.next(TokenKind.COUNT)) { path.push(new UriResource().setKind(ResourceKind.COUNT)); isCount = true; } else { tokenizer.requireNext(TokenKind.STAR); item.setAll(true); } } const type = lastPart.getEdmType(); const isCyclic = item.isAll() || referencedType && type.compatibleTo(referencedType); this._parseOptions(tokenizer, type, isCollection, aliases, item, isRef, isCount, isCyclic); item.setPathSegments(path); } return item; } /** * Parse a string into an expand path. * @param {UriTokenizer} tokenizer tokenizer containing the string to be parsed * @param {?EdmType} referencedType type the expression references * @param {?(EdmEntitySet[])} crossjoinEntitySets entity sets in case of a $crossjoin request * @returns {UriResource[]} the parsed path * @throws {UriSyntaxError} * @throws {UriQueryOptionSemanticError} */ parseExpandPath(tokenizer, referencedType, crossjoinEntitySets) { let path = []; let type = referencedType; let name = null; // In the crossjoin case the start has to be an EntitySet name which will dictate the reference type. if (type == null && crossjoinEntitySets != null && crossjoinEntitySets.length > 0) { tokenizer.requireNext(TokenKind.ODataIdentifier); name = tokenizer.getText(); const crossjoinEntitySet = crossjoinEntitySets.find(entitySet => entitySet.getName() === name); if (crossjoinEntitySet == null) { throw new UriQueryOptionSemanticError(UriSemanticError.Message.ENTITY_SET_NOT_FOUND, name); } else { path.push(new UriResource() .setKind(ResourceKind.ENTITY_COLLECTION) .setEntitySet(crossjoinEntitySet)); type = crossjoinEntitySet.getEntityType(); tokenizer.requireNext(TokenKind.SLASH); } } const typeCast = this._parseTypeCast(tokenizer, type); if (typeCast != null) { path.push(new UriResource().setKind(ResourceKind.TYPE_CAST).setTypeCast(typeCast)); type = typeCast; tokenizer.requireNext(TokenKind.SLASH); } while (tokenizer.next(TokenKind.ODataIdentifier)) { name = tokenizer.getText(); const property = type.getStructuralProperty(name); if (property != null && property.getType().getKind() === EdmTypeKind.COMPLEX) { name = null; type = property.getType(); path.push(new UriResource() .setKind(ResourceKind.COMPLEX_PROPERTY) .setProperty(property) .setIsCollection(property.isCollection())); tokenizer.requireNext(TokenKind.SLASH); const typeCastSuffix = this._parseTypeCast(tokenizer, type); if (typeCastSuffix != null) { path.push(new UriResource() .setKind(ResourceKind.TYPE_CAST) .setTypeCast(typeCastSuffix) .setIsCollection(property.isCollection())); tokenizer.requireNext(TokenKind.SLASH); type = typeCastSuffix; } } } const navigationProperty = type.getNavigationProperty(name); if (navigationProperty == null) { if (name) { throw new UriQueryOptionSemanticError( UriQueryOptionSemanticError.Message.NAVIGATION_PROPERTY_NOT_FOUND, name, type.getFullQualifiedName().toString()); } } else { const isCollection = navigationProperty.isCollection(); path.push(new UriResource() .setKind(isCollection ? ResourceKind.NAVIGATION_TO_MANY : ResourceKind.NAVIGATION_TO_ONE) .setNavigationProperty(navigationProperty) .setIsCollection(isCollection)); } return path; } /** * Parse a string supposed to contain a type cast. * @param {UriTokenizer} tokenizer tokenizer containing the string to be parsed * @param {?EdmType} referencedType type the expression references * @returns {?(EdmEntityType|EdmComplexType)} the parsed type cast (or null if nothing found) * @throws {UriQueryOptionSemanticError} * @private */ _parseTypeCast(tokenizer, referencedType) { if (tokenizer.next(TokenKind.QualifiedName)) { const qualifiedName = FullQualifiedName.createFromNameSpaceAndName(tokenizer.getText()); const isEntityType = referencedType.getKind() === EdmTypeKind.ENTITY; const type = isEntityType ? this._edm.getEntityType(qualifiedName) : this._edm.getComplexType(qualifiedName); if (type == null) { throw new UriQueryOptionSemanticError( isEntityType ? UriSemanticError.Message.ENTITY_TYPE_NOT_FOUND : UriQueryOptionSemanticError.Message.COMPLEX_TYPE_NOT_FOUND, tokenizer.getText()); } else if (type.compatibleTo(referencedType)) { return type; } else { throw new UriQueryOptionSemanticError(UriSemanticError.Message.INCOMPATIBLE_TYPE, qualifiedName.toString(), referencedType.getFullQualifiedName().toString()); } } return null; } /** * Parse a string with expand options. * @param {UriTokenizer} tokenizer tokenizer containing the string to be parsed * @param {?EdmType} referencedType type the options reference * @param {boolean} referencedIsCollection whether the referenced expression is a collection * @param {?Object} aliases alias definitions * @param {ExpandItem} item the expand item where the parsed options will be set * @param {boolean} forRef whether the referenced expression ends with $ref * @param {boolean} forCount whether the referenced expression ends with $count * @param {boolean} isCyclic whether the expansion is of the same or a compatible type * @throws {UriSyntaxError} * @throws {UriQueryOptionSemanticError} * @private */ _parseOptions(tokenizer, referencedType, referencedIsCollection, aliases, item, forRef, forCount, isCyclic) { if (tokenizer.next(TokenKind.OPEN)) { do { let name = null; let value = null; if (referencedIsCollection && !forCount && tokenizer.next(TokenKind.COUNT)) { name = tokenizer.getText(); tokenizer.requireNext(TokenKind.EQ); tokenizer.requireNext(TokenKind.BooleanValue); value = tokenizer.getText() === 'true'; } else if (!forRef && !forCount && tokenizer.next(TokenKind.EXPAND)) { name = tokenizer.getText(); tokenizer.requireNext(TokenKind.EQ); value = new ExpandParser(this._edm).parse(tokenizer, referencedType, null, aliases); } else if (referencedIsCollection && tokenizer.next(TokenKind.FILTER)) { name = tokenizer.getText(); tokenizer.requireNext(TokenKind.EQ); value = new FilterParser(this._edm).parse(tokenizer, referencedType, null, aliases); } else if (isCyclic && !forRef && !forCount && tokenizer.next(TokenKind.LEVELS)) { name = tokenizer.getText(); tokenizer.requireNext(TokenKind.EQ); value = this._parseLevels(tokenizer); FeatureSupport.failUnsupported(FeatureSupport.features.Levels); } else if (referencedIsCollection && !forCount && tokenizer.next(TokenKind.ORDERBY)) { name = tokenizer.getText(); tokenizer.requireNext(TokenKind.EQ); value = new OrderByParser(this._edm).parse(tokenizer, referencedType, null, aliases); } else if (referencedIsCollection && tokenizer.next(TokenKind.SEARCH)) { name = tokenizer.getText(); tokenizer.requireNext(TokenKind.EQ); value = new SearchParser().parse(tokenizer); } else if (!forRef && !forCount && tokenizer.next(TokenKind.SELECT)) { name = tokenizer.getText(); tokenizer.requireNext(TokenKind.EQ); value = new SelectParser(this._edm).parse(tokenizer, referencedType, referencedIsCollection); } else if (referencedIsCollection && !forCount && (tokenizer.next(TokenKind.SKIP) || tokenizer.next(TokenKind.TOP))) { name = tokenizer.getText(); tokenizer.requireNext(TokenKind.EQ); tokenizer.requireNext(TokenKind.UnsignedIntegerValue); value = Number.parseInt(tokenizer.getText(), 10); if (!Number.isSafeInteger(value)) { throw new UriSyntaxError(UriSyntaxError.Message.OPTION_NON_NEGATIVE_INTEGER, name); } } else { throw new UriSyntaxError(UriSyntaxError.Message.WRONG_OPTION_NAME); } if (item.getOption(name) == null) { item.setOption(name, value); } else { throw new UriSyntaxError(UriSyntaxError.Message.DUPLICATED_OPTION, name); } } while (tokenizer.next(TokenKind.SEMI)); tokenizer.requireNext(TokenKind.CLOSE); } } /** * Parse a string supposed to contain a value for the $levels option. * @param {UriTokenizer} tokenizer tokenizer containing the string to be parsed * @returns {string} the parsed $levels value * @throws {UriSyntaxError} * @private */ _parseLevels(tokenizer) { if (tokenizer.next(TokenKind.MAX)) return tokenizer.getText(); tokenizer.requireNext(TokenKind.UnsignedIntegerValue); const result = Number.parseInt(tokenizer.getText(), 10); if (Number.isSafeInteger(result)) return result; throw new UriSyntaxError(UriSyntaxError.Message.OPTION_NON_NEGATIVE_INTEGER, '$levels'); } } module.exports = ExpandParser;