UNPKG

@sap/xsodata

Version:

Expose data from a HANA database as OData V2 service with help of .xsodata files.

429 lines (381 loc) 14 kB
"use strict"; var StateMaschine = require('./../utils/stateMaschine'); var PreconditionFailedError = require('./../utils/errors/http/preconditionFailed'); var PreconditionRequiredError = require('./../utils/errors/http/preconditionRequired'); var NotModifiedError = require('./../utils/errors/http/notModified'); var BadRequestError = require('./../utils/errors/http/badRequest'); var NotFoundError = require('./../utils/errors/http/notFound'); var InternalError = require('./../utils/errors/internalError'); var uriTypes = require('./../uri/uriType'); function debug(context, text, arg1) { if (context && context.logger && context.logger.debug) { context.logger.debug('conditionalHttpHandler',[text, arg1].join(" ")); } } module.exports = ConditionalHttpHandler; /** *Constructor. Creates the internal states also. * *@param {object} context Current xsodata context object */ function ConditionalHttpHandler(context) { var states = createStates(); this._stateMaschine = new StateMaschine(states, context); this.on = this._stateMaschine.on.bind(this._stateMaschine); } /** *Static helper method for usage in async.waterfall() to enable conditional http handling. * *@param {object} context Current xsodata context object *@param {function} asyncDone Callback called on finish or error. Expected signature is * function(error, context){} */ ConditionalHttpHandler.processConditionalRequest = function processConditionalRequest(context, asyncDone) { var handler = new ConditionalHttpHandler(context); handler.on("error", function (error) { asyncDone(error, context); }); handler.on("final", function () { asyncDone(null, context); }); handler.initialize(); }; /** * Initializes and starts the inner statemaschine. This triggers the conditional http handling. */ ConditionalHttpHandler.prototype.initialize = function initialize() { return this._stateMaschine.initialize(); }; /** * Returns the current statemaschine. The statemaschine handles the conditional http handling by * transitioning from state to state. */ ConditionalHttpHandler.prototype.getStateMaschine = function getStateMaschine() { return this._stateMaschine; }; /** * Creates the internal states needed by the internal statemaschine. These states represent * the conditional http handling. * For state documentation see Diagramm in folder "internal/documentation/ * BLI70 - BLI70 - Conditional http handling/Activity etag handling Process request v5.pdf". */ function createStates() { return { initializeWith: "Start", states: { "Start": { action: function () { debug(this.getExternalContext(),"Starting conditional http handling..."); debug(this.getExternalContext(), "Processing request method: ", this.getExternalContext().request.method); debug(this.getExternalContext(), "Start"); this.next("If-Match exists"); } }, "If-Match exists": { action: function () { if (getHeader(this.getExternalContext(), "If-Match")){ debug(this.getExternalContext(),"If-Match exists: ", true); this.next("Is conditional request supported 1"); } else { debug(this.getExternalContext(),"If-Match exists: ", false); this.next("Is modification request"); } } }, "Is conditional request supported 1": { // We need more than one kind of this state because there will be different outcomes action: function () { var isSupported = isConditionalRequestSupported(this.getExternalContext()); debug(this.getExternalContext(),"Is conditional request supported 1: ", isSupported); if (isSupported === true) { this.next("Is concurrent entity 1"); } else { this.next("400 Bad request"); } } }, "Is conditional request supported 2": { action: function () { // We need more than one kind of this state because there will be different outcomes var isSupported = isConditionalRequestSupported(this.getExternalContext()); debug(this.getExternalContext(),"Is conditional request supported 2: ", isSupported); if (isSupported) { this.next("428 Precondition required"); } else { this.next("process request"); } } }, "Is conditional request supported 3": { action: function () { // We need more than one kind of this state because there will be different outcomes var isSupported = isConditionalRequestSupported(this.getExternalContext()); debug(this.getExternalContext(),"Is conditional request supported 3: ", isSupported); if (isSupported) { this.next("Is concurrent entity 3"); } else { this.next("400 Bad request"); } } }, "Is concurrent entity 1": { action: function () { // We need more than one kind of this state because there will be different outcomes var hasToken = hasConcurrencyToken(this.getExternalContext()); debug(this.getExternalContext(),"Is concurrent entity 1: ", hasToken); if (hasToken === true) { this.next("Evaluate If-Match"); } else { this.next("400 Bad request"); } } }, "Is concurrent entity 2": { action: function () { // We need more than one kind of this state because there will be different outcomes var hasToken = hasConcurrencyToken(this.getExternalContext()); debug(this.getExternalContext(),"Is concurrent entity 2: ", hasToken); if (hasToken === true) { this.next("Is conditional request supported 2"); } else { this.next("If-None-Match exists"); } } }, "Is concurrent entity 3": { action: function () { // We need more than one kind of this state because there will be different outcomes var hasToken = hasConcurrencyToken(this.getExternalContext()); debug(this.getExternalContext(),"Is concurrent entity 3: ", hasToken); if (hasToken === true) { this.next("Evaluate If-None-Match"); } else { this.next("400 Bad request"); } } }, "Evaluate If-Match": { action: function () { var etagTemp, ifMatchHeader = getHeader(this.getExternalContext(), "If-Match"); debug(this.getExternalContext(),"Evaluate If-Match"); debug(this.getExternalContext()," If-Match header: ", ifMatchHeader); if(ifMatchHeader === "*"){ return this.next("If-None-Match exists"); } else { getEtag.call(this, this.getExternalContext(), function (error, etag) { if (error) { return this.next("UnexpectedError", error); } if (etag === undefined) { return this.next("404 Not found"); } etagTemp = buildEtagHeaderValue(etag); debug(this.getExternalContext()," entity etag: ", etagTemp); if (etagTemp === ifMatchHeader) { this.next("If-None-Match exists"); } else { this.next("412 Precondition failed"); } }.bind(this)); } } }, "Evaluate If-None-Match": { action: function () { var etagTemp, ifNoneMatchHeader = getHeader(this.getExternalContext(), "If-None-Match"); debug(this.getExternalContext(),"Evaluate If-None-Match"); debug(this.getExternalContext()," If-None-Match header: ", ifNoneMatchHeader); if(ifNoneMatchHeader === "*"){ if (this.getExternalContext().request.method === "GET") { this.next("304 Not modified"); } else { this.next("412 Precondition failed"); } } else { getEtag.call(this, this.getExternalContext(), function (error, etag) { if (error) { return this.next("UnexpectedError", error); } if (etag === undefined) { return this.next("404 Not found"); } etagTemp = buildEtagHeaderValue(etag); debug(this.getExternalContext()," entity etag: ", etagTemp); if (etagTemp === ifNoneMatchHeader) { if (this.getExternalContext().request.method === "GET") { this.next("304 Not modified"); } else { this.next("412 Precondition failed"); } } else { this.next("process request"); } }.bind(this)); } } }, "If-None-Match exists": { action: function () { if (getHeader(this.getExternalContext(), "If-None-Match")){ debug(this.getExternalContext(),"If-None-Match exists: ", true); this.next("Is conditional request supported 3"); } else { debug(this.getExternalContext(),"If-None-Match exists: ", false); this.next("process request"); } } }, "Is modification request": { action: function () { var isModification = isModificationRequest(this.getExternalContext()); debug(this.getExternalContext(),"Is modification request: ", isModification); if (isModification){ this.next("Is concurrent entity 2"); } else { this.next("If-None-Match exists"); } } }, "process request": { action: function () { debug(this.getExternalContext(),"process request"); debug(this.getExternalContext(),"... leaving conditional http handling"); this.setFinal(true); } }, "400 Bad request": { action: function () { debug(this.getExternalContext(),"400 Bad request"); throw new BadRequestError("Bad request", this.getExternalContext()); } }, "412 Precondition failed": { action: function () { debug(this.getExternalContext(),"412 Precondition failed"); throw new PreconditionFailedError("Precondition failed", this.getExternalContext()); } }, "428 Precondition required": { action: function () { debug(this.getExternalContext(),"428 Precondition required"); throw new PreconditionRequiredError("Precondition required", this.getExternalContext()); } }, "304 Not modified": { action: function () { debug(this.getExternalContext(),"304 Not modified"); throw new NotModifiedError("Not modified", this.getExternalContext()); } }, "404 Not found": { action: function () { debug(this.getExternalContext(),"404 Not found"); throw new NotFoundError("404 Not found", this.getExternalContext()); } }, "UnexpectedError": { action: function (source, error) { debug(this.getExternalContext(),"UnexpectedError occured", error); var context = this.getExternalContext(); throw new InternalError("Unexpected error", context, error); } } } }; } /** * Returns the http header value found in context for given name parameter. * *@param {object} context Current xsodata context object *@param {string} name The name of the header field to get the value for *@return {string} The value of the header or null */ function getHeader(context, name) { if (!context.request.headers) { return null; } if (context.request.headers[name]) { return context.request.headers[name]; } if (context.request.headers[name.toLowerCase()]) { return context.request.headers[name.toLowerCase()]; } return null; } /** * Determines if the http request is any kind of modification request. A modification request * is one of MERGE, DELETE, or PUT. POST isn't because it is not allowed in this context of * conditional http hanndling. * *@param {object} context Current xsodata context object *@return {boolean} true if the request is kind of modification else false */ function isModificationRequest(context) { var method = context.request.method; return method === "PUT" || method === "DELETE"; } /** * Determines if the requested entity resource is a current entity or not. If the requested * entity does does not support currcurrent mode by definition this method returns false. * *@param {object} context Current xsodata context object *@return {boolean} true if the entity is a concurrent entity else false */ function hasConcurrencyToken(context) { // if (!context.oData.dbSegment || !context.oData.dbSegment.entityType) { // return false; // } return context.oData.dbSegment.entityType.hasConcurrencyToken(); } /** * Returns the generated etag for the requested entity. The etag value is generated on-the-fly * and can be used to evalute If-Match oder If-None-Match header. Etag has the form of "ABCDEF....". * *@param {object} context Current xsodata context object *@param {function} callback Callback called on etag generation. Expected signature is * function(error, etag){} */ function getEtag(context, callback) { /*jshint validthis:true */ var self = this; if (self._cachedEtag) { callback(null, self._cachedEtag); } else { context.oData.dbSegmentLast.getETag(context, function (error, etag) { self._cachedEtag = etag; callback(error, etag); }); } } /** * Determines if the current request URI is one of the allowed odata URI's for conditional http * handling and if the current http request is not a POST request. * *@param {object} context Current xsodata context object *@return {boolean} true if the request is supported else false */ function isConditionalRequestSupported(context) { var uriType = context.uriTree.uriType; var isSupported = uriType === uriTypes.URI2 || uriType === uriTypes.URI5A || uriType === uriTypes.URI5B || uriType === uriTypes.URI6A; isSupported = context.request.method !== "POST" && isSupported; return isSupported; } /** * Builds the header equivalent ETag http header value. Meaning that if an etag 'abc' is provided * this method returns 'W/"abc"' which is equivalent to the http weak ETag header specification. * * @param etag {String} The raw etag value to wrap * @returns {string} The http header equivalent value */ function buildEtagHeaderValue(etag) { return 'W/"' + etag + '"'; }