@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
JavaScript
'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 + '"';
}