UNPKG

@sap/xsodata

Version:

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

524 lines (489 loc) 19.6 kB
'use strict'; const StateMaschine = require('./../utils/stateMaschine'); const PreconditionFailedError = require('./../utils/errors/http/preconditionFailed'); const PreconditionRequiredError = require('./../utils/errors/http/preconditionRequired'); const NotModifiedError = require('./../utils/errors/http/notModified'); const BadRequestError = require('./../utils/errors/http/badRequest'); const NotFoundError = require('./../utils/errors/http/notFound'); const InternalError = require('./../utils/errors/internalError'); const 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) { const 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) { const 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() { function handleConcurrentEntity( entityNumber, hasTokenMessage, errorMessage ) { // We need more than one kind of this state because there will be different outcomes return function () { const hasToken = hasConcurrencyToken(this.getExternalContext()); debug( this.getExternalContext(), `Is concurrent entity ${entityNumber}: `, hasToken ); if (hasToken === true) { this.next(hasTokenMessage); } else { this.next(errorMessage); } }; } 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 () { const 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 const 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 const 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: handleConcurrentEntity( 1, 'Evaluate If-Match', '400 Bad request' ), }, 'Is concurrent entity 2': { action: handleConcurrentEntity( 2, 'Is conditional request supported 2', 'If-None-Match exists' ), }, 'Is concurrent entity 3': { action: handleConcurrentEntity( 3, 'Evaluate If-None-Match', '400 Bad request' ), }, 'Evaluate If-Match': { action: function () { let etagTemp; const 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 () { let etagTemp; const 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 () { const 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 ); const 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) { const 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) { 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 */ const 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) { const uriType = context.uriTree.uriType; const isSupported = context.request.method !== 'POST' && (uriType === uriTypes.URI2 || uriType === uriTypes.URI5A || uriType === uriTypes.URI5B || uriType === uriTypes.URI6A); 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 + '"'; }