@sap/odata-v4
Version:
OData V4.0 server library
857 lines (792 loc) • 41.7 kB
JavaScript
'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;