@sap/xsodata
Version:
Expose data from a HANA database as OData V2 service with help of .xsodata files.
266 lines (234 loc) • 9.37 kB
JavaScript
;
const _ = require('lodash');
const HttpError = require('./../utils/errors/httpError');
const SqlError = require('../utils/errors/sqlError');
const dataCollector2 = require('./../sql/dataCollector2');
const EXIT_REGEXP = /^([\w\.\/\-]+)(:((\w+)\.(js|xsjslib)))?::(\w+)$/;
module.exports = {
ExitSpec: ExitSpec,
eventHandler: eventHandler,
executeExit: executeExit,
};
function ExitSpec(sExitSpec) {
const aMatch = EXIT_REGEXP.exec(sExitSpec);
function throwMalformedSpec() {
throw new Error(
'Invalid exit specification ' +
sExitSpec +
'. Expected format package.package:file.ext::function'
);
}
if (!aMatch) {
throwMalformedSpec();
}
this.package = aMatch[1];
this.file = aMatch[4];
this.ext = aMatch[5];
this.functionName = aMatch[6];
if (!this.functionName) {
throwMalformedSpec();
}
}
/**
* @param sEventType : string, one of: before, after, precommit, postcommit
* @param sOperation : string, one of: create, update, delete
* @returns {Function} ready to be plugged in the async waterfall
*/
function eventHandler(sEventType, sOperation) {
return function callEventHandler(context, asyncDone) {
let modifications; // The defined modifications (custom exits) of the given odata service entry (association | entityType)
let association, entityType;
let operation, modelExitSpec;
let foundEvent;
// if links, e.g. /Employees('1')/$links/ne_Team, modifications are given in the in-xsodata-defined association (TeamEmployees)
if (context.oData.dbSegment && context.oData.dbSegment.isLinks) {
// extract the corresponding association, then its modifications
association = context.oData.links.association;
modifications = association.modification || {};
}
// otherwise, modifications belong to the single given entityType
else {
// extract the corresponding entityType, then its modifications
entityType = context.oData.dbSegmentLast.entityType;
modifications = entityType.modifications || {};
}
operation = modifications[sOperation] || {};
if (operation.events) {
foundEvent = _.find(operation.events, function (event) {
return event.type === sEventType;
});
if (foundEvent) {
modelExitSpec = foundEvent;
}
}
if (modelExitSpec) {
executeExit(
modelExitSpec.action,
sEventType,
sOperation,
context,
asyncDone
);
} else {
return asyncDone(null, context);
}
};
}
/**
*
* @param sExitSpec is exit spec as string, for example my_package.my_subpackage:my_file.xsjslib::my_function
* @param sEventType is a string, one of 'using','before','after', 'precommit', 'postcommit'
* @param sOperation is a string, one of 'create','update','delete'
* @param context is the odata context
* @param asyncDone is the async framework callback
*/
function executeExit(sExitSpec, sEventType, sOperation, context, asyncDone) {
try {
logInfoSave(context, 'custom exit', 'executing ...');
logInfoSave(context, 'custom exit', 'exitName:' + sExitSpec);
logInfoSave(context, 'custom exit', 'eventType:' + sEventType);
logInfoSave(context, 'custom exit', 'operation:' + sOperation);
_executeExit(sExitSpec, sEventType, sOperation, context, asyncDone);
} catch (err) {
return asyncDone(err, context);
}
}
function logInfoSave(context, preFix, logText) {
if (context.logger) {
context.logger.info(preFix, logText);
}
}
function logErrorSave(context, preFix, logText) {
if (context.logger) {
context.logger.error(preFix, logText);
}
}
function _executeExit(sExitSpec, sEventType, sOperation, context, asyncDone) {
const exitSpec = new ExitSpec(sExitSpec);
exitSpec._dbClient = context.db ? context.db.client : null;
let dbSegment = context.oData.dbSegmentLast;
let attribute1name; // afterTableName | (if links) principalTableName
let table1name; // the corresponding table name for attrib.1
let attribute2name; // beforeTableName | (if links) dependentTableName
let table2name; // the corresponding table name for attrib.2
let param = {},
aParams = [];
if (context.oData.dbSegment && context.oData.dbSegment.isLinks) {
dbSegment = context.oData.links.toBeUpdated;
attribute1name = 'principalTableName';
table1name = dbSegment.sql.rIdPrincipal;
attribute2name = 'dependentTableName';
table2name = dbSegment.sql.rIdDependent;
logInfoSave(context, 'custom exit - principalTable', table1name);
logInfoSave(context, 'custom exit - dependentTable', table2name);
} else {
dbSegment = context.oData.dbSegmentLast;
attribute1name = 'afterTableName';
table1name = dbSegment.sql.rId || dbSegment.sql.rIdNew;
attribute2name = 'beforeTableName';
table2name = dbSegment.sql.rIdOld;
logInfoSave(context, 'custom exit - afterTableName', table1name);
logInfoSave(context, 'custom exit - beforeTableName', table2name);
}
if (exitSpec.file) {
//script reference
param[attribute1name] = table1name;
param[attribute2name] = table2name;
logInfoSave(context, 'custom exit - script-file', exitSpec.file);
context.functionExecutor(exitSpec, param, function (err, result) {
// Custom exits return errors of a specific format (DevGuid, SPS10, Table75, Page494)
// if so, returned error must be reformatted
if (err) {
logErrorSave(context, 'custom exit failed with error', err);
return asyncDone(
new HttpError(
err.ERROR_MESSAGE || 'Internal Server Error',
context,
null,
err.HTTP_STATUS_CODE || 500,
err
),
context
);
}
if (result && result !== true) {
logErrorSave(
context,
'custom exit returned invalid result',
result
);
return asyncDone(
new HttpError(
result.ERROR_MESSAGE || 'Internal Server Error',
context,
null,
result.HTTP_STATUS_CODE || 500,
result
),
context
);
}
logInfoSave(context, 'custom exit', '... execution done');
return asyncDone(null, context);
});
} else {
//sql procedure reference
table1name = '"' + table1name + '"';
table2name = '"' + table2name + '"';
logErrorSave(context, 'custom exit', 'SQL-procedure');
logErrorSave(context, 'operation', sOperation);
logErrorSave(context, 'custom exit - table-1', table1name);
logErrorSave(context, 'custom exit - table-2', table2name);
switch (sOperation) {
case 'create': {
aParams = [table1name];
break;
}
case 'update': {
aParams = [table1name, table2name];
break;
}
case 'delete': {
aParams = [table2name];
break;
}
}
if (sEventType !== 'postcommit') {
aParams.push('?'); // for the error out parameter of the stored procedure
}
const sParams = aParams.join(',');
const sql = 'call "' + sExitSpec + '"(' + sParams + ')';
logInfoSave(context, 'custom exit:SQL-procedure', sql);
dataCollector2.executeSqlAsPreparedStatement(
context,
sql,
[],
(err, outTable) => {
if (err) {
logErrorSave(
context,
'custom exit SQL-procedure failed',
err
);
return asyncDone(new SqlError(context, err), context);
} else {
if (outTable && isNaN(outTable) && outTable.length > 0) {
const appError = outTable[0];
const statusCode = appError.HTTP_STATUS_CODE;
const errorMessage = appError.ERROR_MESSAGE;
const innerError = JSON.stringify(appError);
err = new HttpError(
errorMessage,
context,
null,
statusCode,
innerError
);
}
logInfoSave(context, 'custom exit', '... execution done');
return asyncDone(err, context);
}
}
);
}
}