@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
JavaScript
;
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);
}
}