@sap/xsodata
Version:
Expose data from a HANA database as OData V2 service with help of .xsodata files.
866 lines (788 loc) • 28.2 kB
JavaScript
'use strict';
const dbSegments = require('./../db/dbSegment');
const sel = require('./expandSelectTreeBuilder');
const Http404_NotFound = require('./../utils/errors/http/notFound');
const Http400_BadRequest = require('./../utils/errors/http/badRequest');
const associationUtils = require('./../utils/associations');
const model = require('../model/model.js');
const EntityType = require('../model/entityType.js');
/**
* Class for reading a OData resource path URI
*
* @param context
* @param {Array} segments Uri Segments
* @constructor
*/
const ResourcePathReader = function (context, segments) {
this.context = context;
this.gModel = context.gModel;
// used to moved segments as they are beeing processed
this.reduceSegments = segments.slice();
this.firstDBSegment = null;
this.lastDBSegment = null;
//processing data
this.tableCount = 1;
};
/**
* Consume and evaluates the first resource path segment
*
* @returns {null|dbSegments.DbSegment}
*/
ResourcePathReader.prototype.consumeFirstSegment =
function consumeFirstSegment() {
let segment = this.reduceSegments.shift(); // of type uriSegmentTypes.js#ResourceSegment
// Resolve entity type
let entityType = this.gModel.getEntityType(segment.identifier);
if (entityType === undefined) {
throw new Http404_NotFound(
"Resource not found for the segment '" +
segment.identifier +
"' at position " +
segment.position +
'.'
);
}
// Compute initial state: input parameters, keys, collection flag; may advance segment/entityType
const state = this._computeFirstSegmentState(entityType, segment);
entityType = state.entityType;
segment = state.segment;
const inputParameters = state.inputParameters;
const keys = state.keys;
const isCollection = state.isCollection;
// Create a dbSegment
this.firstDBSegment = new dbSegments.DbSegment(
dbSegments.DBS_Entity,
entityType,
this.tableCount++
);
this.firstDBSegment._urisegment = segment;
// Set input parameters in case of a calcview
if (
inputParameters &&
(this.context.request.method === 'GET' ||
inputParameters.length > 0)
) {
inputParameters.forEach(function (kv) {
this.firstDBSegment.addInputParameter(
this.context,
kv.name,
kv.value
);
}, this);
this.firstDBSegment.validateInputParameters();
}
// Set the key predicates of the first segments or the calcview segments
if (keys) {
this.firstDBSegment.setKeyValues(
segment.keys,
entityType.kind === model.entityKind.calculationView &&
this.context.request.method === 'POST'
);
}
this.firstDBSegment.isCollection = isCollection;
return this.firstDBSegment;
};
ResourcePathReader.prototype._computeFirstSegmentState = function (
entityType,
segment
) {
if (
!(
entityType.kind === model.entityKind.inputParameters ||
entityType.kind === model.entityKind.calculationView
)
) {
return {
entityType: entityType,
segment: segment,
inputParameters: null,
keys: segment.keys,
isCollection: segment.keys ? false : true,
};
}
if (
entityType._entityType &&
entityType._entityType.parameters &&
entityType._entityType.parameters.viaKey === true
) {
const split = this._splitKeysForViaKey(entityType, segment);
return {
entityType: entityType,
segment: segment,
inputParameters: split.inputParameters,
keys: split.keys,
isCollection: split.isCollection,
};
}
return this._adoptCalcViewSegmentOrThrow(entityType, segment);
};
ResourcePathReader.prototype._splitKeysForViaKey = function (
entityType,
segment
) {
let inputParameters = [];
let keys = null;
let isCollection = true;
if (segment.keys) {
keys = [];
segment.keys.forEach(function (kv) {
if (entityType.inputParameters[kv.name]) {
inputParameters.push(kv);
} else if (entityType.keys.names.includes(kv.name)) {
keys.push(kv);
} else {
throw new Http400_BadRequest(
'Inputparameter/Key ' + kv.name + ' is unknown'
);
}
}, this);
isCollection = keys.length === 0;
}
return {
inputParameters: inputParameters,
keys: keys,
isCollection: isCollection,
};
};
ResourcePathReader.prototype._adoptCalcViewSegmentOrThrow = function (
entityType,
segment
) {
const inputParameters = segment.keys;
let keys = null;
let nextSeg = this.reduceSegments.shift();
if (nextSeg) {
const navigation = entityType.getNavigation(nextSeg.identifier);
const association = this.gModel.getAssociationByName(
navigation.association
);
const endTo = this.getTargetEnd(navigation, association, entityType);
const newEntityType = this.gModel.getEntityType(endTo.type);
if (!newEntityType) {
throw new Http404_NotFound(
"Resource not found for the segment '" +
nextSeg.identifier +
"' at position " +
nextSeg.position +
'.'
);
}
keys = nextSeg.keys;
const isCollection = nextSeg.keys ? false : true;
return {
entityType: newEntityType,
segment: nextSeg,
inputParameters: inputParameters,
keys: keys,
isCollection: isCollection,
};
}
if (
!entityType._entityType ||
!entityType._entityType.parameters ||
entityType._entityType.parameters.viaKey !== true
) {
throw new Http400_BadRequest(
'Navigation to calc view expected after an Input path segment 1'
);
}
return {
entityType: entityType,
segment: segment,
inputParameters: inputParameters,
keys: keys,
isCollection: true,
};
};
ResourcePathReader.prototype.consumeFollowerSegment =
function consumeFollowerSegment(prevDbSegment) {
let segment = this.reduceSegments.shift();
this._validateFollowerPreconditions(prevDbSegment, segment);
const special = this._handleSpecialSegments(prevDbSegment, segment);
if (special !== null) {
return special;
}
if (prevDbSegment.isCollection === true) {
throw new Http400_BadRequest(
`The segment '${segment.identifier}' in the request URI is not valid. Since the previous segment refers to a collection, the only supported value for the next segment is '$count'.`
);
}
//check for primitive property
// ### segement.identifier is nav
const property = prevDbSegment.entityType.__metadata.getProperty(
segment.identifier
);
if (property) {
prevDbSegment.kind = dbSegments.DBS_Property;
prevDbSegment.singleProperty = property.COLUMN_NAME;
return prevDbSegment;
}
const navInfo = this._resolveNavigation(prevDbSegment, segment);
const dbSegment = this._createNavigationDbSegment(
prevDbSegment,
segment,
navInfo
);
return this._finalizeNavigationDbSegment(dbSegment, segment, navInfo);
};
ResourcePathReader.prototype._validateFollowerPreconditions = function (
prevDbSegment,
segment
) {
if (
prevDbSegment.kind === dbSegments.DBS_Property &&
segment.identifier !== '$value'
) {
throw new Http400_BadRequest(
"The segment '" +
segment.identifier +
"' in the request URI is not valid. Since the previous segment refers to a primitive type property, the only supported value for the next segment is '$value'."
);
}
if (prevDbSegment.restriction.onlyValue === true) {
throw new Http400_BadRequest(
"The segment '" +
segment.identifier +
"' in the request URI is not valid. $batch, $value, $metadata or a service operation which returns void must be the last segment."
);
}
if (prevDbSegment.restriction.onlyCount === true) {
throw new Http400_BadRequest(
"The segment '" +
prevDbSegment._urisegment.identifier +
"'in the request URI is not valid. $batch, $value, $metadata or a service operation which returns void must be the last segment."
);
}
};
ResourcePathReader.prototype._handleSpecialSegments = function (
prevDbSegment,
segment
) {
if (segment.identifier === '$count') {
if (prevDbSegment.kind === dbSegments.DBS_Property) {
throw new Http400_BadRequest(
"The segment '$count' in the request URI is not valid. Since the previous segment refers to a primitive type property, the only supported value for the next segment is '$value'."
);
}
prevDbSegment.restriction.onlyCount = true;
return prevDbSegment;
}
if (segment.identifier === '$links') {
return this.consumeLinksSegment(prevDbSegment);
}
if (segment.identifier === '$value') {
if (prevDbSegment.kind === dbSegments.DBS_Property) {
prevDbSegment.restriction.onlyValue = true;
return prevDbSegment;
}
throw new Http400_BadRequest(
"Resource not found for the segment '$value'"
);
}
return null;
};
ResourcePathReader.prototype._resolveNavigation = function (
prevDbSegment,
segment
) {
const navigation = prevDbSegment.entityType.getNavigation(
segment.identifier
);
// ### association is first_to_second
if (!navigation) {
throw new Http400_BadRequest(
"Resource not found for the segment '" + segment.identifier + "'"
);
}
const association = this.getAssociation(navigation);
const endFrom = this.getFromEnd(
navigation,
association,
prevDbSegment.entityType
);
const endTo = this.getTargetEnd(
navigation,
association,
prevDbSegment.entityType
);
const targetEntity = this.gModel.getEntityType(endTo.type);
return {
navigation: navigation,
association: association,
endFrom: endFrom,
endTo: endTo,
targetEntity: targetEntity,
};
};
ResourcePathReader.prototype._createNavigationDbSegment = function (
prevDbSegment,
segment,
navInfo
) {
const dbSegment = new dbSegments.DbSegment(
dbSegments.DBS_ResourceNavigation,
navInfo.targetEntity,
this.tableCount++
);
prevDbSegment.nextDBSegment = dbSegment;
dbSegment.previousDBSegment = prevDbSegment;
dbSegment.association = navInfo.association;
dbSegment._urisegment = segment;
dbSegment.setFrom(navInfo.endFrom);
dbSegment.setOver(navInfo.association.over);
dbSegment.setTo(navInfo.endTo);
return dbSegment;
};
ResourcePathReader.prototype._finalizeNavigationDbSegment = function (
dbSegment,
segment,
navInfo
) {
const endTo = navInfo.endTo;
const targetEntity = navInfo.targetEntity;
const navigation = navInfo.navigation;
if (targetEntity.kind === EntityType.entityKind.calculationView) {
if (
targetEntity._entityType.parameters &&
targetEntity._entityType.parameters.viaKey === true
) {
let keys = [];
let inputs = [];
if (!segment.keys) {
throw new Http400_BadRequest(
'Navigation to calc view: Input parameters for target calc view are missing'
);
}
segment.keys.forEach(function (kv) {
if (targetEntity.inputParameters[kv.name]) {
inputs.push(kv);
} else {
keys.push(kv);
}
}, this);
inputs.forEach(function (kv) {
dbSegment.addInputParameter(this.context, kv.name, kv.value);
}, this);
dbSegment.setKeyValues(keys);
dbSegment.isCollection = keys.length === 0;
return dbSegment;
}
segment.keys.forEach(function (kv) {
dbSegment.addInputParameter(this.context, kv.name, kv.value);
}, this);
segment = this.reduceSegments.shift();
const targetEntityParams = this.gModel.getEntityType(
endTo.type + 'Parameters'
);
if (!segment) {
throw new Http400_BadRequest(
'Navigation to calc view expected after an Input path segment 2'
);
}
const navigation2 = targetEntityParams.getNavigation(
segment.identifier
);
const association2 = this.gModel.getAssociationByName(
navigation2.association
);
const endTo2 = this.getTargetEnd(
navigation,
association2,
targetEntityParams
);
const entityType2 = this.gModel.getEntityType(endTo2.type);
if (!entityType2) {
throw new Http404_NotFound(
"Resource not found for the segment '" +
segment.identifier +
"' at position " +
segment.position +
'.'
);
}
if (endTo2.multiplicity === '*') {
if (segment.keys) {
dbSegment.isCollection = false;
dbSegment.setKeyValues(segment.keys);
} else {
dbSegment.isCollection = true;
}
} else {
dbSegment.isCollection = false;
}
return dbSegment;
}
if (endTo.multiplicity === '*') {
if (segment.keys) {
dbSegment.isCollection = false;
dbSegment.setKeyValues(segment.keys);
} else {
dbSegment.isCollection = true;
}
} else {
dbSegment.isCollection = false;
}
return dbSegment;
};
/**
* Consume and the segments behind the "$links" segment
*
* @returns {null|dbSegments.DbSegment}
*/
ResourcePathReader.prototype.consumeLinksSegment = function (prevDbSegment) {
const segment = this.reduceSegments.shift();
if (!segment || !segment.identifier) {
// E.g. /Teams('1')/$links
throw new Http400_BadRequest(
"The request URI is not valid. There must be a segment specified after the '$links' segment and the segment must refer to a entity resource."
);
}
if (this.reduceSegments.length !== 0) {
// E.g. /Teams('1')/$links/ne_Manager/nm_Employees
throw new Http400_BadRequest(
"The request URI is not valid. There must be only one segment after the '$links' segment."
);
}
// A key for the last segment is only allowed for GET & DELETE requests
if (
segment.keys &&
this.context.request.method !== 'GET' &&
this.context.request.method !== 'DELETE'
) {
// E.g. /Teams('1')/$links/np_Players('2')
throw new Http400_BadRequest(
"The request URI is not valid. The segment specified after the '$links' segment must have no key."
);
}
if (prevDbSegment.kind === dbSegments.DBS_Property) {
// E.g. /Teams('1')/Name/$links
throw new Http400_BadRequest(
"The segment '$links' in the request URI is not valid. Since the previous segment refers to a primitive type property, the only supported value for the next segment is '$value'."
);
}
if (prevDbSegment.isCollection === true) {
// E.g. /Teams/$links
throw new Http400_BadRequest(
"The segment '$links' at position 6 in the request URI is not valid. Since the previous segment refers to a collection, the only supported value for the next segment is '$count'."
);
}
// If this request is not a GET request, then prevent multiple navigation properties before the $links request
if (
this.context.request.method !== 'GET' &&
prevDbSegment.previousDBSegment
) {
throw new Http400_BadRequest(
'Multiple navigation properties are not supported for ' +
this.context.request.method +
' requests'
);
}
// Check for navigation property
const navigation = prevDbSegment.entityType.getNavigation(
segment.identifier
);
if (!navigation) {
//E.g. /Teams('1')/$links/invalid
throw new Http404_NotFound(
"The request URI is not valid. The segment '" +
segment.identifier +
"' must refer to a navigation property since the previous segment identifier is '$links'."
);
}
const association = this.getAssociation(navigation);
const endTo = this.getTargetEnd(
navigation,
association,
prevDbSegment.entityType
);
const endFrom = this.getFromEnd(
navigation,
association,
prevDbSegment.entityType
);
const targetEntity = this.gModel.getEntityType(endTo.type);
const dbSegment = new dbSegments.DbSegment(
dbSegments.DBS_ResourceNavigation,
targetEntity,
this.tableCount++
);
prevDbSegment.nextDBSegment = dbSegment;
dbSegment.previousDBSegment = prevDbSegment;
dbSegment.association = association;
prevDbSegment.setLinks(); // set $links=true to the 1st DBSeg
dbSegment.restriction.onlyRefs = true; //show only links on this navigation property
dbSegment._urisegment = segment;
// Set the key predicates of the segment
if (segment.keys) {
dbSegment.setKeyValues(segment.keys);
}
if (endTo.multiplicity === '*') {
if (segment.keys) {
dbSegment.isCollection = false;
} else {
dbSegment.isCollection = true;
}
dbSegment.multiplicity = '*';
} else {
dbSegment.isCollection = false;
dbSegment.multiplicity = '1';
}
//set used association ends
dbSegment.setFrom(endFrom);
dbSegment.setTo(endTo);
if (association.over) {
dbSegment.setOver(association.over);
prevDbSegment.setOver(association.over);
}
return dbSegment;
};
ResourcePathReader.prototype.processResourcePath = function () {
let lastDBSegment = null;
while (this.reduceSegments.length > 0) {
if (lastDBSegment === null) {
lastDBSegment = this.consumeFirstSegment();
} else {
//the previousDBSegment may not change, e.g. in case if the next segment is a property segment
lastDBSegment = this.consumeFollowerSegment(lastDBSegment);
}
}
return lastDBSegment;
};
/**
* Add a new navigation dbSegment using the expanded navigation property and attach it as child to the
* root segment (dbSeg parameter).
* Then use the expandInformation to process nested expands
*
* @param dbSeg Which is the root of the new segment
* @param expandNavName Name of expanded the navigation property
* @param expandInformation Expand information containing nested expands
*/
ResourcePathReader.prototype.addExpand = function (
dbSeg,
expandNavName,
expandInformation
) {
if (dbSeg.isLinks) {
return;
}
const dbSegNew = this._createNavigationSegment(dbSeg, expandNavName);
dbSeg.addExpandDbSegment(expandNavName, dbSegNew);
// recurse
this.addSelExpTree(dbSegNew, expandInformation);
};
/**
* Create a new navigation dbSegment based on the provided dbSegment and the navigation property name
*
* @param dbSeg
* @param expandNavName
* @returns {*|DbSegment}
* @private
*/
ResourcePathReader.prototype._createNavigationSegment = function (
dbSeg,
expandNavName
) {
const navigation = dbSeg.entityType.getNavigation(expandNavName);
if (!navigation) {
throw new Http400_BadRequest(
`Unknown navigation property '${expandNavName}' in entity type '${dbSeg.entityType.name}'`
);
}
const association = this.getAssociation(navigation);
const endTo = this.getTargetEnd(navigation, association, dbSeg);
const endFrom = this.getFromEnd(navigation, association, dbSeg);
const targetEntity = this.gModel.getEntityType(endTo.type);
const dbSegNew = new dbSegments.DbSegment(
dbSegments.DBS_Navigation,
targetEntity,
this.tableCount++
);
dbSegNew.previousDBSegment = dbSeg;
dbSegNew.setFrom(endFrom);
dbSegNew.setTo(endTo);
dbSegNew.setOver(association.over);
dbSegNew.isCollection = endTo.multiplicity === '*';
return dbSegNew;
};
/**
* Starts traversing the raw select-expand tree from the expandSelectTreeBuilder and adds
* new dbSegments into the dbSegment expand tree.
*
* @param dbSeg
* @param tree Tree with expand information (see {@link test/test_select_expand_tree.js} for samples)
*/
ResourcePathReader.prototype.addSelExpTree = function (dbSeg, tree) {
//first add all known expanded navigation properties
if (tree.expandAllAvailExpands === true) {
for (const expandNavName in tree.availableExpands) {
if (tree.availableExpands.hasOwnProperty(expandNavName)) {
this.addExpand(
dbSeg,
expandNavName,
tree.availableExpands[expandNavName]
);
}
}
} else {
for (const name of tree.expandNavigations) {
this.addExpand(dbSeg, name, tree.availableExpands[name]);
}
}
//now add properties
if (tree.isStar || tree.showAllProperties) {
this.checkOtherProperties(dbSeg, tree.properties);
dbSeg.addAllProperties();
} else {
for (const selectedProperty of tree.properties) {
dbSeg.addSelectProperties(selectedProperty);
if (
dbSeg.isNavigationProperty(selectedProperty) &&
!dbSeg.getRelevantNavigationSegments()[selectedProperty]
) {
const relevantNavigationSegment = this._createNavigationSegment(
dbSeg,
selectedProperty
);
dbSeg.addRelevantNavigationSegment(
selectedProperty,
relevantNavigationSegment
);
}
}
}
};
ResourcePathReader.prototype.checkOtherProperties = function (
dbSeg,
properties
) {
const entityType = dbSeg.entityType;
for (const propertyName of properties) {
if (
!(
entityType.propertiesMap[propertyName] ||
entityType.navPropertiesMap[propertyName]
) &&
entityType.keys.generatedKey !== propertyName
) {
throw new Http400_BadRequest(
'property "' + propertyName + '" not allowed'
);
}
}
};
ResourcePathReader.prototype.buildODataTreeTrunk = function () {
this.lastDBSegment = this.processResourcePath();
};
ResourcePathReader.prototype.insertODataNavTreeCrone = function (
queryParameters
) {
//first check for the $expend and $select parameters.
const expands = queryParameters.expand;
const selects = queryParameters.select;
const expSelTree = sel.processExpandSelectTree(expands, selects);
this.addSelExpTree(this.lastDBSegment, expSelTree);
if (this.firstDBSegment.isLinks) {
this.addSelExpTree(this.firstDBSegment, expSelTree);
}
};
ResourcePathReader.prototype.insertQueryParameters = function (
queryParameters
) {
//first check for the $expend and $select parameters.
for (const name in queryParameters) {
if (queryParameters.hasOwnProperty(name)) {
//do checks here
this.lastDBSegment.systemQueryParameter[name] =
queryParameters[name];
}
}
};
exports.parseResourcePath = function (context, odata) {
const resourcePathReader = new ResourcePathReader(context, odata.segments);
// Creates the dbSegment line from the URI up to the "?"
resourcePathReader.buildODataTreeTrunk();
// Evaluate the $select and $expand system query parameter and build a tree
resourcePathReader.insertODataNavTreeCrone(odata.systemQueryParameters);
resourcePathReader.insertQueryParameters(odata.systemQueryParameters);
odata.dbSegment = resourcePathReader.firstDBSegment;
odata.dbSegmentLast = resourcePathReader.lastDBSegment;
// process only if the uri is in the pattern segment/$links/segment
if (
odata.dbSegment.isLinks === true &&
odata.dbSegmentLast.restriction.onlyRefs === true
) {
//if (odata.dbSegmentLast.isCollection) {
// can not use isCollection for the case of: DELETE Managers('3')/$links/nm_Employees('1')
if (odata.dbSegmentLast.multiplicity === '*') {
odata.links = {
//e.g. /Manager(1)/$links/nm_Employees
// Uri : Employees(2)
//only POST/Create is allowed (vid_Manager is changed in employee record set)
toBeUpdated: odata.dbSegmentLast,
keySource: odata.dbSegment,
};
} else {
odata.links = {
//e.g. /Employees(1)/$links/nm_Manager
// Uri : Manager(1)
//only Put/Update is allowed (vid_Manager is changed in employee record set)
toBeUpdated: odata.dbSegment,
keySource: odata.dbSegmentLast,
};
}
// The corresponding association for the given request will be needed many times for custom exits processing
const association = odata.dbSegmentLast.association;
odata.links.association = association;
if (
odata.dbSegmentLast.entityType.name === association.principal.type
) {
odata.links.principal = odata.dbSegmentLast;
odata.links.dependent = odata.dbSegment;
} else if (
odata.dbSegmentLast.entityType.name === association.dependent.type
) {
odata.links.principal = odata.dbSegment;
odata.links.dependent = odata.dbSegmentLast;
}
}
};
/**
* Calculates the target end depending on navigation, association an source entityType
*
* @param navigation
* @param association
* @param entityType
* @returns {*}
*/
ResourcePathReader.prototype.getTargetEnd = function (
navigation,
association,
entityType
) {
return associationUtils.getTargetEnd(navigation, association, entityType);
};
/**
* Calculates the from end depending on navigation, association an source entityType
*
* @param navigation
* @param association
* @param entityType
* @returns {*}
*/
ResourcePathReader.prototype.getFromEnd = function (
navigation,
association,
entityType
) {
return associationUtils.getFromEnd(navigation, association, entityType);
};
/**
* Load a association object from the model
*
* @param navigation Navigation whose association should be loaded
* @returns {*}
*/
ResourcePathReader.prototype.getAssociation = function (navigation) {
return this.gModel.getAssociation(navigation.association);
};
exports.ResourcePathReader = ResourcePathReader;