@sap/xsodata
Version:
Expose data from a HANA database as OData V2 service with help of .xsodata files.
333 lines (285 loc) • 11.1 kB
JavaScript
;
const async = require('async');
const connect = require('../db/connect');
const batchParser = require('./../utils/batch/batchParser');
const batchExecutor = require('./../utils/batch/batchExecutor');
const simpleRequest = require('./../http/simpleHttpRequest');
const simpleResponse = require('./../http/simpleHttpResponse');
const batchRunState = require('./../utils/batch/batchConst').runState;
const utils = require('./../utils/utils');
function parseBatchBody(batchContext, asyncDone) {
batchContext.logger.silly('batchProcessor', 'parseBody');
const req = batchContext.parentContext.request;
const boundary = batchParser.getBoundary(req.headers['content-type']);
return req.getBodyAsString(function (err, body) {
if (err) {
return asyncDone(err, batchContext);
}
try {
batchContext.logger.silly(body); // log batch payload
batchContext.batchParsed = batchParser.convertBatch(body, boundary);
batchContext.logger.silly('batchProcessor', 'parseBody OK');
return asyncDone(null, batchContext);
} catch (err) {
return asyncDone(err, batchContext);
}
});
}
function processApp(batchContext, app, resultDone) {
batchContext.logger.silly('batchProcessor', 'processApp');
if (batchContext.status === batchRunState.createTables) {
app.request = simpleRequest.createRequestFromAppHttp(app);
app.response = simpleResponse.createResponse();
}
if (app.errorResponseWritten) {
return resultDone(null, app);
}
if (batchContext.status === batchRunState.createTables) {
batchExecutor.processCreateTables(
app,
app.request,
app.response,
batchContext,
function (err) {
return resultDone(batchContext.inChangeSet ? err : null, app);
}
);
} else if (batchContext.status === batchRunState.execution) {
batchExecutor.process(
app.request,
app.response,
batchContext,
app.context,
function (err) {
return resultDone(batchContext.inChangeSet ? err : null, app);
}
);
} else if (
batchContext.status === batchRunState.preCommitRun ||
batchContext.status === batchRunState.postCommitRun
) {
batchExecutor.processPrePostCommitRun(
app.request,
app.response,
batchContext,
app.context,
function (err) {
return resultDone(batchContext.inChangeSet ? err : null, app);
}
);
}
}
function processParts(batchContext, asyncDone) {
const parts = batchContext.processedPart.parts;
async.mapSeries(
parts,
function (part, cb) {
try {
processApp(batchContext, part, cb);
} catch (ex) {
cb(ex);
}
},
function (err) {
batchContext.logger.silly('Batch', 'processChangeSet finished');
return asyncDone(err, batchContext);
}
);
}
function collectContentIds(batchContext, asyncDone) {
const part = batchContext.processedPart;
const contentIds = (part.contentIds = { list: [], map: {} });
part.parts.forEach(function (app) {
const id = app.rawData.headers['content-id'];
if (id) {
const idTriple = { id: id, app: app, value: null, key_nv: {} };
app.contentId = idTriple;
contentIds.list.push(idTriple);
contentIds.map[id] = idTriple;
}
});
return asyncDone(null, batchContext);
}
function processChangeSetPartsCreateTable(batchContext, asyncDone) {
batchContext.status = batchRunState.createTables;
processParts(batchContext, asyncDone);
}
function processChangeSetParts(batchContext, asyncDone) {
batchContext.status = batchRunState.execution;
processParts(batchContext, asyncDone);
}
function processChangeSetPartsPreCommit(batchContext, asyncDone) {
batchContext.status = batchRunState.preCommitRun;
processParts(batchContext, asyncDone);
}
function processChangeSetPartsCommit(batchContext, asyncDone) {
const parentContext = batchContext.parentContext;
const client = parentContext.db.client;
batchContext.logger.debug(
'processChangeSetPartsCommit',
'commit changeset operations'
);
client.commit(function (err) {
if (err) {
batchContext.logger.info(
'SQL Exec',
'Commit Error: \n' + JSON.stringify(err)
);
return asyncDone(err, batchContext);
}
return asyncDone(null, batchContext);
});
}
function processChangeSetPartsPostCommit(batchContext, asyncDone) {
batchContext.status = batchRunState.postCommitRun;
processParts(batchContext, asyncDone);
}
function processChangeSet(batchContext, part, asyncDone) {
try {
batchContext.logger.silly('Batch', 'processChangeSet');
batchContext.inChangeSet = true;
batchContext.processedPart = part;
async.waterfall(
[
utils.injectContext(batchContext),
utils.try(collectContentIds),
utils.try(processChangeSetPartsCreateTable), // for *ALL* operations of current changeset: create all temp. tables once at beginning
utils.try(processChangeSetParts), // execute all operations
utils.try(processChangeSetPartsPreCommit), // mainly custom-exit processing (pre-commit exit)
utils.try(processChangeSetPartsCommit), // all changes of operations are committed
utils.try(processChangeSetPartsPostCommit), // mainly custom-exit processing (post-commit exit)
],
function (err, batchContext) {
if (!err) {
batchContext.status = batchRunState.executed;
batchContext.inChangeSet = false;
batchContext.processedPart = null;
return asyncDone(null, batchContext);
} else {
const dbClient = batchContext.parentContext.db.client;
if (dbClient) {
//ROLLBack the changes
return connect.dbRollback(
batchContext.parentContext,
dbClient,
function (errDB) {
if (errDB) {
//Error in rollback ends whole request
return asyncDone(errDB, batchContext);
}
//inside one of the changeset part an error occured
updateBatchContext(batchContext, part, err);
//Error is set back with changeSetErrorResponse, proceed with further batch parts
return asyncDone(null, batchContext);
}
);
} else {
updateBatchContext(batchContext, part, err);
return asyncDone(null, batchContext);
}
}
}
);
} catch (err) {
return asyncDone(err, batchContext);
}
}
function updateBatchContext(batchContext, part, err) {
batchContext.status = batchRunState.executedWithError;
batchContext.inChangeSet = false;
part.changeSetError = err;
part.changeSetErrorResponse = err._changeSetErrorResponse;
batchContext.processedPart = null;
}
function processAppWithState(app, state, batchContext, asyncDone) {
batchContext.status = state;
processApp(batchContext, app, function (err) {
asyncDone(err, batchContext);
});
}
function processPart(batchContext, part, asyncDone) {
batchContext.logger.silly('batchProcessor', 'processPart');
if (part.type === 'app') {
// processing a batch single operation (not in changeset)
async.waterfall(
[
utils.injectContext(batchContext),
utils.try(
processAppWithState,
part,
batchRunState.createTables
),
utils.try(processAppWithState, part, batchRunState.execution),
],
function (err) {
return asyncDone(err, batchContext);
}
);
} else {
if (batchContext.inChangeSet === false) {
processChangeSet(batchContext, part, asyncDone);
} else {
return asyncDone(
new Error('Nested changesets not allowed'),
batchContext
);
}
}
}
function processBatch(batchContext, asyncDone) {
batchContext.logger.silly('batchProcessor', 'processBatch');
const batchParts = batchContext.batchParsed.parts;
async.mapSeries(
batchParts,
function (item, cb) {
try {
processPart(batchContext, item, cb);
} catch (ex) {
cb(ex);
}
},
function (err) {
return asyncDone(err, batchContext);
}
);
}
function sendBatchResponse(batchContext, asyncDone) {
const context = batchContext.parentContext;
batchContext.batchParsed.write(context, context.response);
// We send http 202 because of OData v2 specification 2.2.7.6.6 Batch Responses:
//
// "If a data service receives Batch Request (section 2.2.7.6) with a valid set
// of HTTP request headers,it MUST respond with a 202 Accepted HTTP response code
// to indicate that the request has been accepted for processing, but that the processing
// has not yet been completed."
if (context.response.statusCode === 200) {
// The status code is only set to 202 if no other implementation has already set to another
// status code than 200 which is the default
context.response.statusCode = 202;
}
return asyncDone(null, batchContext);
}
exports.process = function (context, asyncDone) {
context.logger.silly('batchProcessor', 'started');
const batchContext = {
parentContext: context,
logger: context.logger,
callRegisteredStep: context.callRegisteredStep, // pass to inner context
inChangeSet: false,
batchData: null,
batchParsed: null,
status: null,
};
async.waterfall(
[
utils.injectContext(batchContext),
utils.tryAndMeasure(parseBatchBody, 'parseBatchBody'),
utils.tryAndMeasure(processBatch, 'processBatch'),
utils.tryAndMeasure(sendBatchResponse, 'sendBatchResponse'),
],
function (err) {
context.logger.silly('batchProcessor', 'finished');
return asyncDone(err, context);
}
);
};