UNPKG

@sap/odata-v4

Version:

OData V4.0 server library

857 lines (792 loc) 41.7 kB
'use strict'; const ExpandParser = require('./ExpandParser'); const ExpressionParser = require('./ExpressionParser'); const FilterParser = require('./FilterParser'); const OrderByParser = require('./OrderByParser'); const SearchParser = require('./SearchParser'); const TokenKind = require('./UriTokenizer').TokenKind; const EdmPrimitiveTypeKind = require('../edm/EdmPrimitiveTypeKind'); const EdmTypeKind = require('../edm/EdmType').TypeKind; const TransientStructuredType = require('../edm/TransientStructuredType'); const FullQualifiedName = require('../FullQualifiedName'); const UriResource = require('./UriResource'); const ResourceKind = UriResource.ResourceKind; const QueryOption = require('./UriInfo').QueryOptions; const UriSyntaxError = require('../errors/UriSyntaxError'); const UriSemanticError = require('../errors/UriSemanticError'); const UriQueryOptionSemanticError = require('../errors/UriQueryOptionSemanticError'); const FeatureSupport = require('../FeatureSupport'); const AggregateTransformation = require('./apply/AggregateTransformation'); const AggregateExpression = require('./apply/AggregateExpression'); const BottomTopTransformation = require('./apply/BottomTopTransformation'); const ComputeTransformation = require('./apply/ComputeTransformation'); const ComputeExpression = require('./apply/ComputeExpression'); const ConcatTransformation = require('./apply/ConcatTransformation'); const CustomFunctionTransformation = require('./apply/CustomFunctionTransformation'); const ExpandTransformation = require('./apply/ExpandTransformation'); const FilterTransformation = require('./apply/FilterTransformation'); const GroupByTransformation = require('./apply/GroupByTransformation'); const GroupByItem = require('./apply/GroupByItem'); const IdentityTransformation = require('./apply/IdentityTransformation'); const OrderByTransformation = require('./apply/OrderByTransformation'); const SearchTransformation = require('./apply/SearchTransformation'); const SkipTransformation = require('./apply/SkipTransformation'); const TopTransformation = require('./apply/TopTransformation'); const ExpandItem = require('./ExpandItem'); const MemberExpression = require('./MemberExpression'); class ApplyParser { /** * Create an apply parser. * @param {Edm} edm entity data model */ constructor(edm) { this._edm = edm; this._expressionParser = new ExpressionParser(edm); } /** * Parse a string into an array of transformations and adapt the referenced type to the resulting structure. * @param {UriTokenizer} tokenizer tokenizer containing the string to be parsed * @param {?TransientStructuredType} referencedType type that the apply option references * @param {?(string[])} crossjoinEntitySetNames entityset names in case of a $crossjoin request * @param {?Object} aliases alias definitions * @returns {Transformation[]} the transformations * @throws {UriSyntaxError} * @throws {UriQueryOptionSemanticError} */ parse(tokenizer, referencedType, crossjoinEntitySetNames, aliases) { this._tokenizer = tokenizer; this._crossjoinEntitySetNames = crossjoinEntitySetNames; this._aliases = aliases; return this._parseApply(referencedType); } /** * Parse an apply option. * @param {?TransientStructuredType} referencedType type that the apply option references * @returns {Transformation[]} the parsed transformations * @throws {UriSyntaxError} * @throws {UriQueryOptionSemanticError} * @private */ _parseApply(referencedType) { let apply = []; do { apply.push(this._parseTrafo(referencedType)); } while (this._tokenizer.next(TokenKind.SLASH)); return apply; } /** * Parse a transformation. * @param {?TransientStructuredType} referencedType type that the transformation references * @returns {Transformation} the parsed transformation * @throws {UriSyntaxError} * @throws {UriQueryOptionSemanticError} * @private */ _parseTrafo(referencedType) { if (this._tokenizer.next(TokenKind.AggregateTrafo)) { return this._parseAggregateTrafo(referencedType); } else if (this._tokenizer.next(TokenKind.IDENTITY)) { return new IdentityTransformation(); } else if (this._tokenizer.next(TokenKind.ComputeTrafo)) { return this._parseComputeTrafo(referencedType); } else if (this._tokenizer.next(TokenKind.ConcatMethod)) { return this._parseConcatTrafo(referencedType); } else if (this._tokenizer.next(TokenKind.ExpandTrafo)) { return new ExpandTransformation().setExpand(this._parseExpandTrafo(referencedType)); } else if (this._tokenizer.next(TokenKind.FilterTrafo)) { const filter = new FilterParser(this._edm) .parse(this._tokenizer, referencedType, this._crossjoinEntitySetNames, this._aliases); this._tokenizer.requireNext(TokenKind.CLOSE); return new FilterTransformation().setFilter(filter); } else if (this._tokenizer.next(TokenKind.GroupByTrafo)) { return this._parseGroupByTrafo(referencedType); } else if (this._tokenizer.next(TokenKind.OrderByTrafo)) { const orderBy = new OrderByParser(this._edm) .parse(this._tokenizer, referencedType, this._crossjoinEntitySetNames, this._aliases); this._tokenizer.requireNext(TokenKind.CLOSE); return new OrderByTransformation().setOrderBy(orderBy); } else if (this._tokenizer.next(TokenKind.SearchTrafo)) { const search = new SearchParser().parse(this._tokenizer); this._tokenizer.requireNext(TokenKind.CLOSE); return new SearchTransformation().setSearch(search); } else if (this._tokenizer.next(TokenKind.SkipTrafo)) { this._tokenizer.requireNext(TokenKind.UnsignedIntegerValue); const skip = Number.parseInt(this._tokenizer.getText(), 10); this._tokenizer.requireNext(TokenKind.CLOSE); if (!Number.isSafeInteger(skip)) { throw new UriSyntaxError(UriSyntaxError.Message.OPTION_NON_NEGATIVE_INTEGER, 'skip'); } return new SkipTransformation().setSkip(skip); } else if (this._tokenizer.next(TokenKind.TopTrafo)) { this._tokenizer.requireNext(TokenKind.UnsignedIntegerValue); const top = Number.parseInt(this._tokenizer.getText(), 10); this._tokenizer.requireNext(TokenKind.CLOSE); if (!Number.isSafeInteger(top)) { throw new UriSyntaxError(UriSyntaxError.Message.OPTION_NON_NEGATIVE_INTEGER, 'top'); } return new TopTransformation().setTop(top); } else if (this._tokenizer.next(TokenKind.QualifiedName)) { return this._parseCustomFunction(referencedType); } else { // eslint-disable-line no-else-return return this._parseBottomTop(referencedType); } } /** * Parse an aggregate transformation. * @param {?TransientStructuredType} referencedType type that the transformation references * @returns {AggregateTransformation} the parsed transformation * @throws {UriSyntaxError} * @throws {UriQueryOptionSemanticError} * @private */ _parseAggregateTrafo(referencedType) { let aggregate = new AggregateTransformation(); let properties = new Map(); do { let type = new TransientStructuredType(referencedType); aggregate.addExpression(this._parseAggregateExpr(type)); for (const [name, property] of type.getProperties()) { if (property === referencedType.getProperty(name)) continue; if (properties.has(name)) { throw new UriQueryOptionSemanticError(UriQueryOptionSemanticError.Message.IS_PROPERTY, name); } properties.set(name, property); } } while (this._tokenizer.next(TokenKind.COMMA)); this._tokenizer.requireNext(TokenKind.CLOSE); referencedType.deleteProperties(); for (const property of properties.values()) referencedType.addProperty(property); return aggregate; } /** * Parse an aggregate expression. * @param {?TransientStructuredType} referencedType type that the expression references * @returns {AggregateExpression} the parsed expression * @throws {UriSyntaxError} * @throws {UriQueryOptionSemanticError} * @private */ _parseAggregateExpr(referencedType) { this._tokenizer.saveState(); let aggregateExpression; let error; // First try is checking for a common expression. try { aggregateExpression = new AggregateExpression(); const expression = this._expressionParser.parse( this._tokenizer, referencedType, this._crossjoinEntitySetNames, this._aliases); aggregateExpression.setExpression(expression); const customMethods = referencedType.getCustomAggregationMethods(); this._parseAggregateWith(aggregateExpression, customMethods); switch (aggregateExpression.getStandardMethod()) { case null: if (aggregateExpression.getCustomMethod() === null) { throw new UriSyntaxError( UriSyntaxError.Message.WRONG_AGGREGATE_EXPRESSION_SYNTAX, this._tokenizer.getPosition()); } break; case AggregateExpression.StandardMethod.MIN: case AggregateExpression.StandardMethod.MAX: this._expressionParser.checkNoCollection(expression); if (expression.getType().getKind() !== EdmTypeKind.PRIMITIVE && expression.getType().getKind() !== EdmTypeKind.ENUM && expression.getType().getKind() !== EdmTypeKind.DEFINITION) { throw new UriQueryOptionSemanticError( UriQueryOptionSemanticError.Message.ONLY_FOR_PRIMITIVE_TYPES, 'aggregate'); } break; case AggregateExpression.StandardMethod.SUM: case AggregateExpression.StandardMethod.AVERAGE: this._expressionParser.checkNumericType(expression); break; default: } const alias = this._parseAsAlias(referencedType, true); aggregateExpression.setAlias(alias); referencedType.addProperty( this._createDynamicAggregationProperty(alias, aggregateExpression.getStandardMethod(), aggregateExpression.getCustomMethod() ? customMethods.get(aggregateExpression.getCustomMethod().toString()) : expression.getType())); this._parseAggregateFrom(aggregateExpression, referencedType); return aggregateExpression; } catch (err) { error = err; } // No legitimate continuation of a common expression has been found. // Second try is checking for a (potentially empty) path prefix and the things that could follow it. this._tokenizer.returnToSavedState(); aggregateExpression = new AggregateExpression(); const pathSegments = this._parsePathPrefix(referencedType); const identifierLeft = pathSegments && pathSegments.length && !pathSegments[pathSegments.length - 1].getKind(); let type = referencedType; if (identifierLeft && pathSegments.length > 1) type = pathSegments[pathSegments.length - 2].getEdmType(); if (!identifierLeft && pathSegments.length) type = pathSegments[pathSegments.length - 1].getEdmType(); const slashLeft = this._tokenizer.getText() === '/'; // A custom aggregate (an OData identifier) is defined in the // CustomAggregate EDM annotation (in namespace Org.OData.Aggregation.V1) // of the structured type or of the entity container. // Instead of looking into annotations, we expect the custom aggregate // to be declared in the service configuration. // Its name could be a property name, too, and the specification says // "the name refers to the custom aggregate within an aggregate expression // without a with clause, and to the property in all other cases." if (identifierLeft && type.getCustomAggregates().has(this._tokenizer.getText()) && !this._tokenizer.next(TokenKind.WithOperator)) { const customAggregate = this._tokenizer.getText(); const property = this._createDynamicProperty(customAggregate, type.getCustomAggregates().get(customAggregate)); pathSegments[pathSegments.length - 1].setKind(ResourceKind.PRIMITIVE_PROPERTY).setProperty(property); aggregateExpression.setPathSegments(pathSegments); const alias = this._parseAsAlias(referencedType, false); if (alias) { aggregateExpression.setAlias(alias); referencedType.addProperty(this._createDynamicProperty(alias, null)); } else { // TODO: Add property to related types. if (type.addProperty) type.addProperty(property); } this._parseAggregateFrom(aggregateExpression, referencedType); } else if (!identifierLeft && !slashLeft && type.getKind() === EdmTypeKind.ENTITY && this._tokenizer.next(TokenKind.OPEN)) { if (!pathSegments.length) { throw new UriSyntaxError( UriSyntaxError.Message.WRONG_AGGREGATE_EXPRESSION_SYNTAX, this._tokenizer.getPosition()); } aggregateExpression.setPathSegments(pathSegments); let inlineType = new TransientStructuredType(type); aggregateExpression.setInlineAggregateExpression(this._parseAggregateExpr(inlineType)); this._tokenizer.requireNext(TokenKind.CLOSE); } else if (!identifierLeft && (slashLeft || !pathSegments.length) && this._tokenizer.next(TokenKind.COUNT)) { pathSegments.push(new UriResource().setKind(ResourceKind.COUNT)); aggregateExpression.setPathSegments(pathSegments); const alias = this._parseAsAlias(referencedType, true); aggregateExpression.setAlias(alias); referencedType.addProperty( this._createDynamicProperty(alias, // The OData standard mandates Edm.Decimal (with no decimals), although counts are always integer. EdmPrimitiveTypeKind.Decimal, null, 0)); } else { // If there is still no success, we throw the error from the first try, // assuming it to be the case the user intended. throw error; } return aggregateExpression; } /** * Parse the "with" part of an aggregate expression. * @param {AggregateExpression} aggregateExpression the aggregate expression * @param {Map.<string, EdmPrimitiveType|EdmTypeDefinition>} customAggregationMethods the defined custom aggregation methods * @throws {UriSyntaxError} * @private */ _parseAggregateWith(aggregateExpression, customAggregationMethods) { if (this._tokenizer.next(TokenKind.WithOperator)) { if (this._tokenizer.next(TokenKind.QualifiedName)) { const customMethod = this._tokenizer.getText(); // A custom aggregation method is announced in the CustomAggregationMethods // EDM annotation (in namespace Org.OData.Aggregation.V1) of the structured type // or of the entity container. // Instead of looking into annotations, we expect the custom aggregation methods // to be declared in the service configuration. if (customAggregationMethods.has(customMethod)) { aggregateExpression.setCustomMethod(FullQualifiedName.createFromNameSpaceAndName(customMethod)); } else { throw new UriQueryOptionSemanticError( UriQueryOptionSemanticError.Message.CUSTOM_AGGREGATION_METHOD_NOT_FOUND, customMethod); } } else if (this._tokenizer.next(TokenKind.SUM)) { aggregateExpression.setStandardMethod(AggregateExpression.StandardMethod.SUM); } else if (this._tokenizer.next(TokenKind.MIN)) { aggregateExpression.setStandardMethod(AggregateExpression.StandardMethod.MIN); } else if (this._tokenizer.next(TokenKind.MAX)) { aggregateExpression.setStandardMethod(AggregateExpression.StandardMethod.MAX); } else if (this._tokenizer.next(TokenKind.AVERAGE)) { aggregateExpression.setStandardMethod(AggregateExpression.StandardMethod.AVERAGE); } else if (this._tokenizer.next(TokenKind.COUNTDISTINCT)) { aggregateExpression.setStandardMethod(AggregateExpression.StandardMethod.COUNT_DISTINCT); } else { throw new UriSyntaxError(UriSyntaxError.Message.WRONG_WITH_SYNTAX, this._tokenizer.getPosition()); } } } /** * Parse the alias part of an aggregate expression. * @param {?(TransientStructuredType|EdmEntityType|EdmComplexType)} referencedType type that the expression references * @param {boolean} isRequired whether the alias is required * @returns {?string} the alias name * @throws {UriSyntaxError} * @throws {UriQueryOptionSemanticError} * @private */ _parseAsAlias(referencedType, isRequired) { if (this._tokenizer.next(TokenKind.AsOperator)) { this._tokenizer.requireNext(TokenKind.ODataIdentifier); const name = this._tokenizer.getText(); if (referencedType.getProperty(name)) { throw new UriQueryOptionSemanticError(UriQueryOptionSemanticError.Message.IS_PROPERTY, name); } return name; } else if (isRequired) { throw new UriSyntaxError(UriSyntaxError.Message.ALIAS_EXPECTED, this._tokenizer.getPosition()); } return null; } /** * Parse the "from" part of an aggregate expression. * @param {AggregateExpression} aggregateExpression the aggregate expression * @param {?(TransientStructuredType|EdmEntityType|EdmComplexType)} referencedType type that the expression references * @throws {UriSyntaxError} * @throws {UriQueryOptionSemanticError} * @private */ _parseAggregateFrom(aggregateExpression, referencedType) { while (this._tokenizer.next(TokenKind.FromOperator)) { const expression = new MemberExpression(this._parseGroupingProperty(referencedType), referencedType); let from = new AggregateExpression().setExpression(expression); this._parseAggregateWith(from, referencedType.getCustomAggregationMethods()); if (from.getStandardMethod() === AggregateExpression.StandardMethod.SUM || from.getStandardMethod() === AggregateExpression.StandardMethod.AVERAGE) { this._expressionParser.checkNumericType(expression); } aggregateExpression.addFrom(from); } } /** * Create a dynamic structural property. * @param {string} name the name of the property * @param {?EdmPrimitiveType} type the type of the property * @param {?number} [precision] the precision facet of the property * @param {?(number|string)} [scale] the scale facet of the property * @returns {Object} the dynamic property as look-alike of EdmProperty * @private */ _createDynamicProperty(name, type, precision, scale) { return { getName: () => name, getType: () => type, isCollection: () => false, isNullable: () => true, getMaxLength: () => null, getPrecision: () => precision === undefined ? null : precision, getScale: () => scale === undefined ? 'variable' : scale, getSrid: () => 'variable', isUnicode: () => true, getDefaultValue: () => null, isPrimitive: () => true, getAnnotations: () => [] }; } /** * Create a dynamic aggregation property and set its type according to the result type of the aggregation method. * @param {string} name the name of the property * @param {?AggregateExpression.StandardMethod} method the aggregation method * @param {EdmPrimitiveType} type type the method acts on * @returns {Object} the dynamic property as look-alike of EdmProperty * @private */ _createDynamicAggregationProperty(name, method, type) { const resultType = method === AggregateExpression.StandardMethod.COUNT_DISTINCT || method === AggregateExpression.StandardMethod.SUM || method === AggregateExpression.StandardMethod.AVERAGE ? EdmPrimitiveTypeKind.Decimal : type; const precision = resultType === EdmPrimitiveTypeKind.DateTimeOffset || resultType === EdmPrimitiveTypeKind.Duration || resultType === EdmPrimitiveTypeKind.TimeOfDay ? 12 : null; const scale = method === AggregateExpression.StandardMethod.COUNT_DISTINCT ? 0 : 'variable'; return this._createDynamicProperty(name, resultType, precision, scale); } /** * Parse a compute transformation. * @param {?TransientStructuredType} referencedType type that the transformation references * @returns {ComputeTransformation} the parsed transformation * @throws {UriSyntaxError} * @throws {UriQueryOptionSemanticError} * @private */ _parseComputeTrafo(referencedType) { let compute = new ComputeTransformation(); do { const expression = this._expressionParser.parse( this._tokenizer, referencedType, this._crossjoinEntitySetNames, this._aliases); this._expressionParser.checkNoCollection(expression); const expressionTypeKind = expression.getType().getKind(); if (expressionTypeKind !== EdmTypeKind.PRIMITIVE && expressionTypeKind !== EdmTypeKind.ENUM && expressionTypeKind !== EdmTypeKind.DEFINITION) { throw new UriQueryOptionSemanticError(UriQueryOptionSemanticError.Message.ONLY_FOR_PRIMITIVE_TYPES, 'compute'); } const alias = this._parseAsAlias(referencedType, true); referencedType.addProperty(this._createDynamicProperty(alias, expression.getType())); compute.addExpression(new ComputeExpression() .setExpression(expression) .setAlias(alias)); } while (this._tokenizer.next(TokenKind.COMMA)); this._tokenizer.requireNext(TokenKind.CLOSE); return compute; } /** * Parse a concat transformation. * @param {?TransientEdmStructuredType} referencedType type that the transformation references * @returns {ConcatTransformation} the parsed transformation * @throws {UriSyntaxError} * @throws {UriQueryOptionSemanticError} * @private */ _parseConcatTrafo(referencedType) { let concat = new ConcatTransformation(); // Each sub-transformation could aggregate properties away, // so it has to start with the original referenced type to avoid // unintended consequences for subsequent sub-transformations. // Each sub-transformation adds its properties to the resulting properties. let types = []; do { let type = new TransientStructuredType(referencedType); concat.addSequence(this._parseApply(type)); types.push(type); } while (this._tokenizer.next(TokenKind.COMMA)); if (concat.getSequences().length < 2) this._tokenizer.requireNext(TokenKind.COMMA); // for the error message this._tokenizer.requireNext(TokenKind.CLOSE); let properties = new Map(); for (const type of types) { for (const [name, property] of type.getProperties()) { if (properties.has(name)) { if (this._expressionParser.isCompatible(properties.get(name).getType(), property.getType())) { continue; } if (!this._expressionParser.isCompatible(property.getType(), properties.get(name).getType())) { throw new UriQueryOptionSemanticError(UriSemanticError.Message.INCOMPATIBLE_TYPE, property.getType().getFullQualifiedName(), properties.get(name).getType().getFullQualifiedName()); } } properties.set(name, property); } } for (const [name, property] of properties) { referencedType.addProperty(property, types.some(type => !type.getProperty(name))); } referencedType.deleteProperties(); return concat; } /** * Parse an expand transformation. * @param {?TransientStructuredType} referencedType type that the transformation references * @returns {ExpandItem} the parsed expand item * @throws {UriSyntaxError} * @throws {UriQueryOptionSemanticError} * @private */ _parseExpandTrafo(referencedType) { let item = new ExpandItem(); const pathSegments = new ExpandParser(this._edm) .parseExpandPath(this._tokenizer, referencedType, this._crossjoinEntitySetNames); if (!pathSegments.length) { throw new UriSyntaxError(UriSyntaxError.Message.EXPAND_NO_VALID_PATH, this._tokenizer.getParseString(), this._tokenizer.getPosition()); } item.setPathSegments(pathSegments); const type = pathSegments[pathSegments.length - 1].getEdmType(); if (this._tokenizer.next(TokenKind.COMMA)) { if (this._tokenizer.next(TokenKind.FilterTrafo)) { item.setOption(QueryOption.FILTER, new FilterParser(this._edm) .parse(this._tokenizer, type, this._crossjoinEntitySetNames, this._aliases)); this._tokenizer.requireNext(TokenKind.CLOSE); } else { this._tokenizer.requireNext(TokenKind.ExpandTrafo); item.setOption(QueryOption.EXPAND, [this._parseExpandTrafo(type)]); } } while (this._tokenizer.next(TokenKind.COMMA)) { this._tokenizer.requireNext(TokenKind.ExpandTrafo); let nestedExpands = (item.getOption(QueryOption.EXPAND) || []); nestedExpands.push(this._parseExpandTrafo(type)); item.setOption(QueryOption.EXPAND, nestedExpands); } this._tokenizer.requireNext(TokenKind.CLOSE); return item; } /** * Parse a group-by transformation. * @param {?TransientStructuredType} referencedType type that the transformation references * @returns {GroupByTransformation} the parsed transformation * @throws {UriSyntaxError} * @throws {UriQueryOptionSemanticError} * @private */ _parseGroupByTrafo(referencedType) { let groupBy = new GroupByTransformation(); this._parseGroupByList(groupBy, referencedType); if (this._tokenizer.next(TokenKind.COMMA)) { groupBy.setTransformations(this._parseApply(referencedType)); } this._tokenizer.requireNext(TokenKind.CLOSE); referencedType.deleteProperties(); referencedType.unprotectProperties(); return groupBy; } /** * Parse the list of groups. * @param {GroupByItem} groupBy the current group-by item * @param {?(TransientStructuredType|EdmEntityType|EdmComplexType)} referencedType type that the group-by references * @private */ _parseGroupByList(groupBy, referencedType) { this._tokenizer.requireNext(TokenKind.OPEN); do { groupBy.addGroupByItem(this._parseGroupByElement(referencedType)); } while (this._tokenizer.next(TokenKind.COMMA)); this._tokenizer.requireNext(TokenKind.CLOSE); } /** * Parse the group-by element. * @param {?(TransientStructuredType|EdmEntityType|EdmComplexType)} referencedType type that the group-by references * @returns {GroupByItem} the group-by item object * @private */ _parseGroupByElement(referencedType) { return this._tokenizer.next(TokenKind.RollUpSpec) ? this._parseRollUpSpec(referencedType) : new GroupByItem().setPathSegments(this._parseGroupingProperty(referencedType)); } /** * Parse the rollup. * @param {?(TransientStructuredType|EdmEntityType|EdmComplexType)} referencedType type that the rollup references * @returns {GroupByItem} the rollup * @private */ _parseRollUpSpec(referencedType) { let item = new GroupByItem(); if (this._tokenizer.next(TokenKind.ALL)) { item.setIsRollupAll(); } else { item.addRollupItem(new GroupByItem().setPathSegments(this._parseGroupingProperty(referencedType))); } this._tokenizer.requireNext(TokenKind.COMMA); do { item.addRollupItem(new GroupByItem().setPathSegments(this._parseGroupingProperty(referencedType))); } while (this._tokenizer.next(TokenKind.COMMA)); this._tokenizer.requireNext(TokenKind.CLOSE); return item; } /** * Parse the path to the grouping property. * @param {?(TransientStructuredType|EdmEntityType|EdmComplexType)} referencedType type that the path references * @returns {UriResource[]} path segments * @private */ _parseGroupingProperty(referencedType) { let pathSegments = this._parsePathPrefix(referencedType); const leftOver = pathSegments.length && !pathSegments[pathSegments.length - 1].getKind() || this._tokenizer.getText() === '/'; if (pathSegments.length && !pathSegments[pathSegments.length - 1].getKind()) pathSegments.pop(); if (this._tokenizer.getText() === '/') this._tokenizer.requireNext(TokenKind.ODataIdentifier); if (leftOver) { const type = pathSegments.length ? pathSegments[pathSegments.length - 1].getEdmType() : referencedType; const property = type.getProperty(this._tokenizer.getText()); if (!property) { throw new UriQueryOptionSemanticError(UriSemanticError.Message.PROPERTY_NOT_FOUND, this._tokenizer.getText(), type.getFullQualifiedName()); } pathSegments.push(this._createPropertyResource(property)); } if (pathSegments[pathSegments.length - 1].isCollection()) { throw new UriQueryOptionSemanticError(UriQueryOptionSemanticError.Message.COLLECTION); } if (!pathSegments.length) this._tokenizer.requireNext(TokenKind.ODataIdentifier); // for the error message // TODO: Generalize to more than one segment and to other than structural properties. if (pathSegments.length === 1 && pathSegments[0].getProperty()) { referencedType.protectProperty(pathSegments[0].getProperty().getName()); } return pathSegments; } /** * Parse the path prefix and a following OData identifier as one path, deviating from the ABNF. * @param {?(TransientStructuredType|EdmEntityType|EdmComplexType)} referencedType type that the path references * @returns {UriResource[]} path segments * @private */ _parsePathPrefix(referencedType) { let pathSegments = []; let type = referencedType; if (this._tokenizer.next(TokenKind.QualifiedName)) { const typeCast = this._parseTypeCast(type); pathSegments.push(new UriResource().setKind(ResourceKind.TYPE_CAST).setTypeCast(typeCast)); this._tokenizer.requireNext(TokenKind.SLASH); type = typeCast; } let hasSlash; do { hasSlash = false; if (this._tokenizer.next(TokenKind.ODataIdentifier)) { const property = type.getProperty(this._tokenizer.getText()); if (property && (property.getEntityType || property.getType().getKind() === EdmTypeKind.COMPLEX)) { pathSegments.push(this._createPropertyResource(property)); type = property.getType ? property.getType() : property.getEntityType(); if (this._tokenizer.next(TokenKind.SLASH)) { hasSlash = true; if (this._tokenizer.next(TokenKind.QualifiedName)) { type = this._parseTypeCast(type); pathSegments.push(new UriResource().setKind(ResourceKind.TYPE_CAST).setTypeCast(type)); hasSlash = false; } } } else { pathSegments.push(new UriResource()); break; } } else { break; } } while (hasSlash || this._tokenizer.next(TokenKind.SLASH)); return pathSegments; } /** * Parse type cast. * @param {?(TransientStructuredType|EdmEntityType|EdmComplexType)} referencedType type that the type cast references * @returns {?(EdmEntityType|EdmComplexType)} the type of the cast * @private */ _parseTypeCast(referencedType) { const qualifiedName = this._tokenizer.getText(); const fqn = FullQualifiedName.createFromNameSpaceAndName(qualifiedName); const compareType = referencedType instanceof TransientStructuredType ? referencedType.getBaseType() : referencedType; const isEntityType = compareType.getKind() === EdmTypeKind.ENTITY; const type = isEntityType ? this._edm.getEntityType(fqn) : this._edm.getComplexType(fqn); if (!type) { throw new UriQueryOptionSemanticError( isEntityType ? UriSemanticError.Message.ENTITY_TYPE_NOT_FOUND : UriQueryOptionSemanticError.Message.COMPLEX_TYPE_NOT_FOUND, qualifiedName); } if (!type.compatibleTo(compareType)) { throw new UriQueryOptionSemanticError(UriSemanticError.Message.INCOMPATIBLE_TYPE, qualifiedName, referencedType.getFullQualifiedName()); } // Type casts are explicitly not supported (although the parser can parse them). FeatureSupport.failUnsupported(FeatureSupport.features.TypeCast, qualifiedName, this._tokenizer.getPosition()); return type; } /** * Create a property-resource segment. * @param {EdmProperty|EdmNavigationProperty} property the structural or navigation property * @returns {UriResource} the property resource segment * @private */ _createPropertyResource(property) { const isCollection = property.isCollection(); const isNavigation = property.getEntityType !== undefined; let kind; if (isNavigation) { kind = isCollection ? ResourceKind.NAVIGATION_TO_MANY : ResourceKind.NAVIGATION_TO_ONE; } else if (property.getType().getKind() === EdmTypeKind.COMPLEX) { kind = isCollection ? ResourceKind.COMPLEX_COLLECTION_PROPERTY : ResourceKind.COMPLEX_PROPERTY; } else { kind = isCollection ? ResourceKind.PRIMITIVE_COLLECTION_PROPERTY : ResourceKind.PRIMITIVE_PROPERTY; } return new UriResource() .setProperty(isNavigation ? null : property) .setNavigationProperty(isNavigation ? property : null) .setIsCollection(isCollection) .setKind(kind); } /** * Parse a custom-function transformation. * @param {?TransientStructuredType} referencedType type that the transformation references * @returns {CustomFunctionTransformation} the parsed transformation * @throws {UriSyntaxError} * @throws {UriQueryOptionSemanticError} * @private */ _parseCustomFunction(referencedType) { const fullQualifiedName = FullQualifiedName.createFromNameSpaceAndName(this._tokenizer.getText()); const bindingParameterType = referencedType.getBaseType(); let visitedParameters = new Map(); let names = []; this._tokenizer.requireNext(TokenKind.OPEN); if (!this._tokenizer.next(TokenKind.CLOSE)) { do { this._tokenizer.requireNext(TokenKind.ODataIdentifier); const name = this._tokenizer.getText(); if (visitedParameters.has(name)) { throw new UriQueryOptionSemanticError(UriQueryOptionSemanticError.Message.DUPLICATE_PARAMETER, name); } this._tokenizer.requireNext(TokenKind.EQ); const expression = this._expressionParser.parse( this._tokenizer, referencedType, this._crossjoinEntitySets, this._aliases); visitedParameters.set(name, expression); names.push(name); } while (this._tokenizer.next(TokenKind.COMMA)); this._tokenizer.requireNext(TokenKind.CLOSE); } const func = this._edm.getBoundFunction( fullQualifiedName, bindingParameterType.getFullQualifiedName(), true, names); if (!func) { throw new UriQueryOptionSemanticError(UriSemanticError.Message.FUNCTION_NOT_FOUND, fullQualifiedName.toString(), names.join(', ')); } // The parameters can only be validated after determining which of the overloaded functions we have. const parameters = this._expressionParser.getValidatedParameters(func, visitedParameters); // The binding parameter and the return type must be of type complex or entity collection. const bindingParameter = func.getParameters().values().next().value; const returnType = func.getReturnType(); if (bindingParameter.getType().getKind() !== EdmTypeKind.ENTITY && bindingParameter.getType().getKind() !== EdmTypeKind.COMPLEX || !bindingParameter.isCollection() || returnType.getType().getKind() !== EdmTypeKind.ENTITY && returnType.getType().getKind() !== EdmTypeKind.COMPLEX || !returnType.isCollection()) { throw new UriQueryOptionSemanticError(UriQueryOptionSemanticError.Message.FUNCTION_MUST_USE_COLLECTIONS, fullQualifiedName.toString()); } // TODO: What if the referenced type has been changed by previous transformations? // Set referenced type to result type of the function. referencedType.deleteProperties(); for (const property of returnType.getType().getProperties().values()) referencedType.addProperty(property); return new CustomFunctionTransformation().setFunction(func).setParameters(parameters); } /** * Parse a partial-aggregation transformation. * @param {?(TransientStructuredType|EdmEntityType|EdmComplexType)} referencedType type that the transformation references * @returns {BottomTopTransformation} the parsed transformation * @throws {UriSyntaxError} * @throws {UriQueryOptionSemanticError} * @private */ _parseBottomTop(referencedType) { let bottomTop = new BottomTopTransformation(); if (this._tokenizer.next(TokenKind.BottomCountTrafo)) { bottomTop.setMethod(BottomTopTransformation.Method.BOTTOM_COUNT); } else if (this._tokenizer.next(TokenKind.BottomPercentTrafo)) { bottomTop.setMethod(BottomTopTransformation.Method.BOTTOM_PERCENT); } else if (this._tokenizer.next(TokenKind.BottomSumTrafo)) { bottomTop.setMethod(BottomTopTransformation.Method.BOTTOM_SUM); } else if (this._tokenizer.next(TokenKind.TopCountTrafo)) { bottomTop.setMethod(BottomTopTransformation.Method.TOP_COUNT); } else if (this._tokenizer.next(TokenKind.TopPercentTrafo)) { bottomTop.setMethod(BottomTopTransformation.Method.TOP_PERCENT); } else if (this._tokenizer.next(TokenKind.TopSumTrafo)) { bottomTop.setMethod(BottomTopTransformation.Method.TOP_SUM); } else { throw new UriSyntaxError(UriSyntaxError.Message.WRONG_OPTION_VALUE, QueryOption.APPLY); } const number = this._expressionParser.parse( this._tokenizer, referencedType, this._crossjoinEntitySetNames, this._aliases); this._expressionParser.checkIntegerType(number); bottomTop.setNumber(number); this._tokenizer.requireNext(TokenKind.COMMA); const value = this._expressionParser.parse( this._tokenizer, referencedType, this._crossjoinEntitySetNames, this._aliases); this._expressionParser.checkNumericType(value); bottomTop.setValue(value); this._tokenizer.requireNext(TokenKind.CLOSE); return bottomTop; } } module.exports = ApplyParser;