UNPKG

@sap/xsodata

Version:

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

406 lines (365 loc) 13.9 kB
'use strict'; const async = require('async'); const applyUriChecks = require('./../../uri/applyChecks'); const authorizationProcessor = require('./../../processor/authorizationProcessor'); const batchConst = require('./batchConst'); const configuration = require('./../../configuration'); const errorProcessor = require('./../../processor/errorProcessor'); const json = require('./../../serializer/jsonSerializer'); const metadataDbReader = require('./../../model/metadataReader'); const oDataUriParser = require('./../../uri/oDataUriParser'); const oDataProcessor = require('./../../processor/processor'); const uriChecks = require('./../../uri/checks'); const uriKeyValueParser = require('./../../parsers/uriKeyValue'); const uriProcessor = require('./../../http/uriParser'); const utils = require('./../utils'); const xsodataFileReader = require('./../../model/xsodataReader'); const ConditionalHttpHandler = require('./../../http/conditionalHttpHandler'); const DebugInfo = require('./../errors/debugInfo'); const RequestContext = require('./../requestContext'); const TestError = require('./../errors/testError'); exports.processPrePostCommitRun = function ( request, response, batchContext, context, done ) { context.logger.silly('batchProcessor', 'processPrePostCommitRun'); try { async.waterfall( [ utils.injectContext(context), utils.try(oDataProcessor.processRequest), ], function (err /*, contextFinish*/) { if (err) { if (context.mode === configuration.modes.development) { if (err instanceof TestError) { context.logger.debug( 'xsodata', 'dev mode: is TestError' ); return done ? done(err, context) : null; } } if (err instanceof DebugInfo) { const serializer = new json.JsonSerializer( context, 65536, 200 ); serializer.write(err.message); serializer.flush(); } else { errorProcessor.process(context, err); } if (batchContext.inChangeSet) { err._changeSetErrorResponse = response; } finish(err, context, done); return; } finish(null, context, done); } ); } catch (exception) { context.logger.info( 'xsodata', 'process-exception: ' + JSON.stringify(exception) ); if (done) { done(exception); } else { throw exception; } } }; // STEP batch create tables --> after createNewContentIdFromRecordMapFromPayload // move the contentId information to app.contentID which is an link to the corresponding id in part.contentIds function fillContentID(context, asyncDone) { const batchContext = context.batchContext; const app = context.app; if (batchContext.inChangeSet) { if (app.contentId) { context.logger.silly( 'batchExecutor', 'set contentID ' + app.contentId.id + ' = ' + context.contendId ); app.contentId.value = context.contentId; app.contentId.key_nv = context.contentIdKeyObj; } } return asyncDone(null, context); } // STEP batch process --> before processConditionalRequest and processRequest // Because in the step create table not yet calculated auto key values may have been set to nil in the contentId, now // (in the process step) this key values values must be updated with the calculated values. // Updated are only the values in the dbsegment, not in the Uri, also reparsing the Uri MUST be avoided. function updateContextIdKeyValues(context, asyncDone) { const batchContext = context.batchContext; if (!batchContext.inChangeSet) { return asyncDone(null, context); } let updateDbSegment = null; const firstSegment = context.oData.dbSegment; if (firstSegment.isLinks === true) { // no error if there is only one segment or it is a $links segment and there is only one more segment after the $links segment if (firstSegment.nextDBSegment.nextDBSegment !== null) { return asyncDone( new Error( 'The usage of ContentIds is only supported for Uris with one odata segment' ), context ); } updateDbSegment = firstSegment; } else { if (firstSegment.nextDBSegment !== null) { return asyncDone( new Error( 'The usage of ContentIds is only supported for Uris with one odata segment' ), context ); } updateDbSegment = firstSegment; } const part = batchContext.processedPart; const contentIds = part.contentIds.list; const contentIdInfo = findContentIdInfo( context.request.urlOrig, contentIds ); if (!contentIdInfo) { return asyncDone(null, context); } const newKeyNV = contentIdInfo.key_nv; const newKeysNames = Object.keys(newKeyNV); // instead of rewriting the uri and reparsing the uri, the already parsed keys are directly modified if (newKeysNames.length !== updateDbSegment._KeyValues.length) { return asyncDone( new Error( 'The key list of the referenced ContentIDs must fit to the parsed key list' ), context ); } let keyName; let i; let newValue, newValueUri, newValueParsed; for (i = 0; i < updateDbSegment._KeyValues.length; i++) { keyName = updateDbSegment._KeyValues[i].name; newValue = newKeyNV[keyName]; newValueUri = newValue; if (newKeyNV[keyName] === undefined) { return asyncDone( new Error( 'The key ' + keyName + 'has not been set by the referenced request' ), context ); } newValueParsed = uriKeyValueParser.parse(newValueUri); updateDbSegment._KeyValues[i].value = newValueParsed; } return asyncDone(null, context); } function findContentIdInfo(urlOrig, contentIds) { let contentIdName; let contentIdIndex; for ( contentIdIndex = 0; contentIdIndex < contentIds.length; contentIdIndex++ ) { contentIdName = '$' + contentIds[contentIdIndex].id; if (urlOrig.indexOf(contentIdName) > -1) { return contentIds[contentIdIndex]; } } return null; } // STEP batch create tables --> before uri parsing // Check the own URI for '$' and replace them // Note the replacement may not be save since application data may still be missing (e.g. auto generated keys). // but it is save enough for tmp table creation of this request function replaceUriContendIds(context, asyncDone) { context.logger.silly('batchExecutor', 'check uri for contentID'); let uri = context.request.url; const batchContext = context.batchContext; if (!batchContext.inChangeSet) { return asyncDone(null, context); } const part = batchContext.processedPart; const contentIds = part.contentIds.list; let id; for (const contentId of contentIds) { id = '$' + contentId.id; if (uri.indexOf(id) > -1) { uri = uri.replace(id, contentId.value); } } //TODO add check if not all $ are replaced context.request.urlOrig = context.request.url; context.request.url = uri; context.logger.silly('batchExecutor', 'new uri:' + uri); return asyncDone(null, context); } exports.processCreateTables = function ( app, request, response, batchContext, done ) { batchContext.parentContext.logger.silly( 'batchExecutor', 'process step: ' + batchContext.status + ' ' + batchConst.runStateText[batchContext.status] ); const context = new RequestContext( batchContext.parentContext.networkContext, batchContext.parentContext.networkContext.requestOptions ); app.context = context; context.app = app; context.db = batchContext.parentContext.db; context.logger.silly('xsodata', 'process batch: ' + request.url); context.batchContext = batchContext; context.callRegisteredStep = batchContext.callRegisteredStep; //set req,res,db data context.request = request; context.response = response; //inject model into context context.modelData = batchContext.parentContext.modelData; context.userName = batchContext.parentContext.userName; try { async.waterfall( [ utils.injectContext(context), utils.try(replaceUriContendIds), utils.try(uriProcessor.prepareUri), utils.try(xsodataFileReader.loadXsodataConfiguration), utils.try(metadataDbReader.loadModelMetadata), utils.try(oDataUriParser.parseODataUri), utils.try(authorizationProcessor.processAuthorization), utils.try(applyUriChecks.bind(null, uriChecks)), utils.try(oDataProcessor.processRequest), utils.try(fillContentID), ], function (err) { if (err) { if (context.mode === configuration.modes.development) { if (err instanceof TestError) { context.logger.debug( 'xsodata', 'dev mode: is TestError' ); return done ? done(err, context) : null; } } if (err instanceof DebugInfo) { const serializer = new json.JsonSerializer( context, 65536, 200 ); serializer.write(err.message); serializer.flush(); } else { errorProcessor.process(context, err); } if (batchContext.inChangeSet) { err._changeSetErrorResponse = app.response; } // this request has and error and the response is already written, so don't process further step in // the processing chain. app.errorResponseWritten = true; return finish(err, context, done); } return finish(null, context, done); } ); } catch (exception) { context.logger.info( 'xsodata', 'process-exception: ' + JSON.stringify(exception) ); if (done) { done(exception); } else { throw exception; } } }; exports.process = function (request, response, batchContext, context, done) { context.logger.silly( 'batchExecutor', 'process step: ' + batchContext.status + ' ' + batchConst.runStateText[batchContext.status] ); const app = context.app; try { async.waterfall( [ utils.injectContext(context), utils.try(updateContextIdKeyValues), utils.try(ConditionalHttpHandler.processConditionalRequest), utils.try(oDataProcessor.processRequest), utils.try(fillContentID), ], function (err) { if (err) { if (context.mode === configuration.modes.development) { if (err instanceof TestError) { context.logger.debug( 'xsodata', 'dev mode: is TestError' ); return done ? done(err, context) : null; } } if (err instanceof DebugInfo) { const serializer = new json.JsonSerializer( context, 65536, 200 ); serializer.write(err.message); serializer.flush(); } else { errorProcessor.process(context, err); } if (batchContext.inChangeSet) { err._changeSetErrorResponse = app.response; } return finish(err, context, done); } return finish(null, context, done); } ); } catch (exception) { context.logger.info( 'xsodata', 'process-exception: ' + JSON.stringify(exception) ); if (done) { done(exception); } else { throw exception; } } }; function finish(err, context, done) { context.logger.info('xsodata', 'batchExecutor finish'); context.response.end(); if (done) { return done(err, context); } }