@sap/xsodata
Version:
Expose data from a HANA database as OData V2 service with help of .xsodata files.
510 lines (456 loc) • 15.2 kB
JavaScript
;
const fs = require('fs');
const sql = require('./../sql/sqlStatement');
const path = require('path');
//cache the css file
const styleCSS = fs.readFileSync(path.join(__dirname, '/style.css'));
/**
* Checks if query parameter "sap-ds-debug" exists, and if yes
* "context.debugView" attribute is set to object { "format" : <format> }
* @param context
* @param asyncDone
* @returns {*}
*/
exports.checkParameter = function (context, asyncDone) {
const showDebugViewFormat = context.uriTree.queryParameters['sap-ds-debug'];
if (showDebugViewFormat) {
context.debugView = { format: showDebugViewFormat };
}
return asyncDone(null, context);
};
exports.writeDebugInfo = function (context, rFrom, rTo) {
switch (context.debugView.format) {
case 'json':
rTo.writeHead(
rFrom.statusCode,
generateHead(context.response.headers, 'application/json')
);
rTo.write(JSON.stringify(generateViewJSON(context), null, 4));
break;
case 'html':
rTo.writeHead(
rFrom.statusCode,
generateHead(context.response.headers, 'text/html')
);
rTo.write(generateViewHTML(context));
break;
default:
rTo.writeHead(
rFrom.statusCode,
generateHead(context.response.headers, 'text/plain')
);
rTo.write('Unknown debug view format');
}
};
function generateHead(responseHeaders, contentType) {
// clone the response headers using JSON
const debugHeaders = JSON.parse(JSON.stringify(responseHeaders));
// change the application type according to query parameters: e.g. json, html, ..
debugHeaders['Content-Type'] = contentType + ';charset=utf-8';
return debugHeaders;
}
function generateViewJSON(context) {
const debView = {
request: {},
response: {},
server: {},
SQL: {},
xsodata: {},
};
try {
debView.request = createRequestInfo(context);
debView.response = createResponseInfo(context);
debView.server = createServerInfo(context);
debView.SQL = createSQLInfo(context);
debView.xsodata = getXSOData(context);
if (context.response.statusCode >= 400) {
debView.stacktrace = {};
debView.stacktrace = serializeStacktrace(context.response.data);
}
return debView;
} catch (exp) {
return exp;
}
}
function generateViewHTML(context) {
let debView = '<html>';
try {
debView += getHTMLHead();
debView += getHTMLBody(context);
} catch (exp) {
return null;
}
debView += '</html>';
return debView;
}
function getHTMLHead() {
return (
'<head>' + '<style type="text/css">' + styleCSS + '</style>' + '</head>'
);
}
function getHTMLBody(context) {
let body = '<body>';
body += objectToHTML(createRequestInfo(context), 'Request', true);
body += objectToHTML(createResponseInfo(context), 'Response', true);
body += objectToHTML(createServerInfo(context), 'Server', true);
body += objectToHTML(createSQLInfo(context), 'SQL', true);
body += objectToHTML(getXSOData(context), 'xsodata', true);
body += sectionDownload();
body += '</body>';
return body;
}
function createRequestInfo(context) {
const req = {};
let pr, obj;
req.method = context.request.method;
req.uri = context.request.url;
req.protocol = 'HTTP/' + context.request.httpVersion;
req.headers = {};
obj = context.request.headers;
for (pr in obj) {
if (obj.hasOwnProperty(pr) && typeof obj[pr] !== 'function') {
req.headers[pr] = obj[pr];
}
}
return req;
}
function createResponseInfo(context) {
const res = {};
let pr, obj;
res.status = context.response.statusCode;
res.headers = {};
obj = context.response.headers;
for (pr in obj) {
if (obj.hasOwnProperty(pr) && typeof obj[pr] !== 'function') {
res.headers[pr] = obj[pr];
}
}
res.body = context.response.data;
return res;
}
function createServerInfo(context) {
let pr, obj, prSub, segment;
const server = {
version: '',
dbVersion: '',
environment: {},
uri: {},
runtime: {},
};
//// Version
server.version = require('../../package.json').version;
server.dbVersion = context.gModel.getDbVersion();
//// Server.environment
server.environment.localAddr = context.httpResponse.connection.localAddress;
server.environment.localPort = context.httpResponse.connection.localPort;
server.environment.remoteAddr =
context.httpResponse.connection.remoteAddress;
server.environment.remotePort = context.httpResponse.connection.remotePort;
server.environment.remoteFamily =
context.httpResponse.connection.remoteFamily;
//// Server.uri
////// Query parameters
obj = context.uriTree.queryParameters;
for (pr in obj) {
if (
Object.prototype.hasOwnProperty.call(obj, pr) &&
pr.startsWith('$')
) {
// ignore non-property parameters
prSub = pr.substring(1, pr.length); // ignore $ sign in the output
server.uri[prSub] =
prSub === 'select' ? obj[pr].split(',') : obj[pr];
}
}
////// uriResourceParts
const segmentKind = [
'empty',
'',
'entity',
'resourceNavigation',
'property',
'count',
'navigation',
]; // read from dbSegment.js
server.uri.uriResourceParts = [];
segment = context.oData === null ? null : context.oData.dbSegment;
let segObj;
while (typeof segment !== 'undefined' && segment !== null) {
segObj = {};
segObj.segment = segment._AliasName || '';
segObj.uriResourceKind = segmentKind[segment.kind];
segObj.isCollection = segment.isCollection;
segObj.singleProperty = segment.singleProperty;
segObj.restriction = {};
obj = segment.restriction;
for (pr in obj) {
if (obj.hasOwnProperty(pr)) {
segObj.restriction[pr] = obj[pr];
}
}
server.uri.uriResourceParts.push(segObj);
segment = segment.nextDBSegment;
}
//// Runtime
server.runtime = measurementsToObject(context.measurements);
function measurementsToObject(measurementsArr) {
const measurementsObj = {};
measurementsArr.forEach(function (m) {
measurementsObj[m.name] = {};
measurementsObj[m.name].duration = m.time + ' microSec';
if (m.children.length !== 0) {
measurementsObj[m.name].children = measurementsToObject(
m.children
);
}
});
return measurementsObj;
}
return server;
}
/**
* Extracts SQL information from "context.sql.container" and returns a Java Script object containing
* all sql statements as string and their parameters.
* @param context
* @returns {{createTmp: Array, insertTmp: Array, selectTmp: Array, select: Array}}
*/
function createSQLInfo(context) {
if (!context.sql) {
return;
}
const container = context.sql.container;
let i;
let tmp;
const out = {
createTmp: [],
insertTmp: [],
selectTmp: [],
select: [],
};
for (i = 0; i < container.createTmp.length; i++) {
out.createTmp.push(stmToObject(container.createTmp[i].stm));
}
for (i = 0; i < container.insertTmp.length; i++) {
out.insertTmp.push(stmToObject(container.insertTmp[i].stm));
}
for (i = 0; i < container.selectTmp.length; i++) {
out.selectTmp.push(stmToObject(container.selectTmp[i].stm));
}
for (i = 0; i < container.select.length; i++) {
out.select.push(stmToObject(container.select[i].stm));
if (container.select[i].getFallbackStatement) {
tmp = container.select[i].getFallbackStatement();
if (tmp) {
out.select.push(stmToObject(tmp));
}
}
}
function stmToObject(stm) {
const sqlLocal = {
sql: null,
parameters: [],
};
sqlLocal.sql = stm.toSqlHana(
new sql.SqlBuildHanaContext(context),
sqlLocal.parameters
);
return sqlLocal;
}
return out;
}
function getXSOData(context) {
const xs = {};
let entity; // temporary variables
//// Database objects
xs['Database Objects'] = {};
context.gModel._entityTypes.forEach(function (elemEnt) {
entity = elemEnt.tableName;
xs['Database Objects'][entity] = {};
////// Name
xs['Database Objects'][entity].EntitySet = elemEnt.name;
////// Navigation
xs['Database Objects'][entity].navigates = [];
elemEnt.navProperties.forEach(function (elemNav) {
xs['Database Objects'][entity].navigates.push(
createNavigationText(elemNav)
);
});
////// Modifications
xs['Database Objects'][entity].modifications = {};
if (elemEnt.modifications.hasOwnProperty('create')) {
xs['Database Objects'][entity].modifications.create =
elemEnt.modifications.create;
}
if (elemEnt.modifications.hasOwnProperty('update')) {
xs['Database Objects'][entity].modifications.update =
elemEnt.modifications.update;
}
if (elemEnt.modifications.hasOwnProperty('delete')) {
xs['Database Objects'][entity].modifications.delete =
elemEnt.modifications.delete;
}
////// Properties
xs['Database Objects'][entity].properties = [];
elemEnt.properties.forEach(function (elemProp) {
xs['Database Objects'][entity].properties.push(
elemProp.COLUMN_NAME +
'(Type: ' +
elemProp.DATA_TYPE_NAME +
', ' +
(elemProp.IS_NULLABLE === 'TRUE'
? 'Nullable'
: 'Not nullable') +
')'
);
});
});
//// Associations
xs.Associations = {};
context.gModel._associations.forEach(function (elemAsc) {
xs.Associations[elemAsc.name] = {};
xs.Associations[elemAsc.name].principal = createAssociationEndText(
elemAsc,
'principal'
);
xs.Associations[elemAsc.name].dependent = createAssociationEndText(
elemAsc,
'dependent'
);
});
function createNavigationText(navigation) {
const principal = navigation.from.principal ? 'principal' : '-';
return (
navigation.association +
' as ' +
navigation.name +
' from ' +
(navigation.from.dependent ? 'dependent' : principal)
);
}
function createAssociationEndText(association, end) {
if (end !== 'principal' && end !== 'dependent') {
return;
}
return (
association[end].type +
' (' +
association[end].joinproperties[0] +
') ' +
'multiplicity ' +
"'" +
association[end].multiplicity +
"'"
);
}
return xs;
}
function serializeStacktrace(reponse) {
const error = JSON.parse(reponse).error;
let line = [];
const serliaized = {};
serliaized.exception = error.message.value;
serliaized.stack = error.message.stack
.split(' at ')
.map(function (value) {
// input example: ResourcePathReader.processResourcePath (C:\\git\\xsodata\\lib\\uri\\resourcePathParser.js:275:34)
//* cut out lines, where there is no "methodName (path)"
line = value.split(' (');
if (line.length === 1) {
return null;
}
return line;
})
.filter(function (value) {
// cut out returned nulls
return value !== null;
})
.map(function (value) {
try {
const node = {};
node.method = value[0];
node.path = value[1].split('.js')[0] + '.js'; // first segment before .js
node.line = value[1].split('.js')[1].split(':')[1]; // first segment after .js -> e.g. ':275:34'
return node;
} catch (ex) {
return {
method: '<unknown>',
path: '<unknown>',
line: '<unknown>',
};
}
});
return serliaized;
}
function objectToHTML(objIn, varName, firstLevel) {
let pr;
let html = '';
if (firstLevel) {
html +=
'<div class="header" id="sec_' +
varName +
'">' +
'<h1><a href="#sec_' +
varName +
'">' +
varName +
'</a></h1>' +
'</div>';
html += '<div class="section">' + '<table>';
for (pr in objIn) {
if (objIn.hasOwnProperty(pr) && typeof objIn[pr] !== 'function') {
html += objectToHTML(objIn[pr], pr, false);
}
}
html += '</table></div>';
} else if (typeof objIn === 'object' && !Array.isArray(objIn)) {
html +=
'<tr class="tabtr">' +
'<td class="name"><h2>' +
varName +
'</h2></td>' +
'<td class="value">' +
'<table class="objTab">';
for (pr in objIn) {
if (objIn.hasOwnProperty(pr) && typeof objIn[pr] !== 'function') {
html += objectToHTML(objIn[pr], pr, false);
}
}
html += '</table></td></tr>';
} else if (typeof objIn === 'object' && Array.isArray(objIn)) {
html +=
'<tr>' +
'<td class=""><h2>' +
varName +
'</h2>' +
//'<td class="value">' +
'<table>';
objIn.forEach(function (x) {
html += objectToHTML(x, '', false);
});
html += '</table></td></tr>';
} else {
html +=
'<tr>' +
'<td class="name"><h2>' +
varName +
'</h2></td>' +
'<td class="value">' +
objIn +
'</td>' +
'</tr>';
}
return html;
}
function sectionDownload() {
let html = '';
html +=
'<div class="header2" id="sec_download">' +
'<h1><a href="#sec_download"> Download </a></h1>' +
'</div>';
html +=
'<div class="section">' +
'<a onclick="this.href=,data:text/html;charset=UTF-8," href="#" download="page.html">Click here</a>' +
', to download this document as an HTML file!' +
'</div>';
return html;
}