@sap/xsodata
Version:
Expose data from a HANA database as OData V2 service with help of .xsodata files.
739 lines (622 loc) • 28.8 kB
JavaScript
;
const logMetadata = process.env.XSODATA_METADATA >= 1;
/**
* Add <__metadata> and <__schema> to each entity in <context.xsodata>
* Uses <context.xsodata>
* @type {exports}
*/
const utils = require('./../utils/utils');
const async = require('async');
const Rwlock = require('rwlock');
const db = require('./../db/connect');
const locks = new Rwlock();
const model = require('./model');
const InternalError = require('./../utils/errors/internalError');
const NotFound = require('./../utils/errors/http/notFound');
const SqlError = require('../utils/errors/sqlError');
const EntityType = require('./entityType');
const Association = require('./association');
const _ = require('lodash');
const Measurement = require('./../utils/measurement');
const dataCollector2 = require('./../sql/dataCollector2');
function loadKeysForTable(context, entityType, cb) {
context.logger.silly('model', 'loadKeysForTable for entity ' + entityType.name);
var processRows = function (err, rows) {
if (logMetadata) {
context.logger.silly('model', 'rows\n' + JSON.stringify(rows, null, 4));
}
if (err) {
return cb(new SqlError(context, err));
}
if (rows.length === 0) {
return cb(new InternalError("Table object for entity " + entityType.name + " does not have a key. Add a primary key to the table or use a generated key.", context, null), context);
}
entityType.setKeyNames(rows.map(function toKeyNames(row) {
return row.COLUMN_NAME;
}));
return cb(null, entityType);
};
let sql = "";
if (entityType._isVirtual) {
sql =
"select * from " + (entityType._remoteSource ? "\"" + entityType._remoteSource + "\".\"SYS\".\"CONSTRAINTS\"" : "\"SYS\".\"CONSTRAINTS\" " ) +
"WHERE SCHEMA_NAME = '" + entityType._virtualObjectSchema + "' AND TABLE_NAME = '" + entityType._virtualObjectName + "' AND IS_PRIMARY_KEY = 'TRUE' ORDER BY position";
} else {
let schema = entityType.schema || context.defaultSchema;
sql = "SELECT * from CONSTRAINTS WHERE SCHEMA_NAME = '" + schema + "' AND TABLE_NAME = '" + entityType.tableName + "' AND IS_PRIMARY_KEY = 'TRUE' ORDER BY position";
}
dataCollector2.executeSqlDirectly(context, sql, processRows);
}
function loadTableInfo(context, entityType, tableStoreType, cb) {
context.logger.silly("model", "loadTableInfo");
var processRows = function (err, rows) {
try {
if (logMetadata) {
context.logger.silly('model', 'rows\n' + JSON.stringify(rows, null, 4));
}
if (err) {
return cb(new SqlError(context, err));
}
if (rows.length === 0) {
return cb(new NotFound('No data found for ' + entityType.tableName + '.', context, ''));
}
context.logger.silly('model', 'create Metadata for entity ' + entityType.name + ' with ' + rows.length + ' rows');
entityType.init(new model.Metadata(rows), rows[0].KIND, tableStoreType, context.logger);
if (entityType.kind === model.entityKind.table) {
if (entityType.keys.names.length > 0) {
return cb(new InternalError("Keys cannot be specified for source as it is a table object.", context, null), context);
} else {
return loadKeysForTable(context, entityType, cb);
}
} else {
var genKey = entityType.keys.generatedKey;
if (!genKey && entityType.keys.names.length === 0) {
return cb(new InternalError("Missing specification of keys for view.", context, null), context);
} else {
if (genKey) {
var overwrittenColumn = entityType.propertiesMap[entityType.keys.generatedKey];
if (!!overwrittenColumn) {
// Provided generated key overwrites an existing column
var msg = "Invalid generated key property '" + genKey + "'";
msg += " in definition '" + overwrittenColumn.TABLE_NAME + "'.";
msg += " The generated key would overwrite an existing property";
return cb(
new InternalError(msg, context, null),
context
);
}
}
return cb(null, entityType);
}
}
} catch (exception) {
return cb(new InternalError(exception.message));
}
};
let schema = "";
let parameters = [];
// entityType.table is either a real table or a SQL view;
// so we need to lookup both: sys.table_columns and sys.view_columns (if set: check via "remote source");
// sys.columns is not usable due to access restrictions
let sql =
"SELECT " +
" TO_BIGINT(" + model.entityKind.table + ") kind, " +
" table_name, " +
" column_name, " +
" position, " +
" data_type_name, " +
" is_nullable, " +
" length, " +
" scale, " +
" default_value " +
"FROM " + (entityType._remoteSource ? "\"" + entityType._remoteSource + "\"" + ".\"SYS\".\"TABLE_COLUMNS\"" : "TABLE_COLUMNS ") +
"WHERE schema_name = ? AND " +
" table_name = ? " +
"UNION ALL " +
"SELECT " +
" TO_BIGINT(" + model.entityKind.view + ") kind, " +
" view_name as table_name, " +
" column_name, " +
" position, " +
" data_type_name, " +
" is_nullable, " +
" length, " +
" scale, " +
" default_value " +
"FROM " + (entityType._remoteSource ? "\"" + entityType._remoteSource + "\"" + ".\"SYS\".\"VIEW_COLUMNS\"" : "VIEW_COLUMNS ") +
"WHERE schema_name = ? AND " +
" view_name = ? " +
"ORDER BY position";
if (entityType._isVirtual) {
parameters = [entityType._virtualObjectSchema, entityType._virtualObjectName, entityType._virtualObjectSchema, entityType._virtualObjectName];
} else {
schema = entityType.schema || context.defaultSchema;
parameters = [schema, entityType.tableName, schema, entityType.tableName];
}
context.logger.silly("model", "sql loadTableInfo: " + sql);
context.logger.silly("model", "sql parameters: " + parameters);
dataCollector2.executeSqlAsPreparedStatement(context, sql, parameters, processRows);
}
function loadCalcViewInfo(context, entityType, cb) {
/*
Remark : it is not possible(or at least i was not able to) to execute a JOIN in a MDX SELECT
*/
let schema = entityType.schema || context.defaultSchema;
let sql =
'select ' +
'SCHEMA_NAME,' +
'QUALIFIED_NAME,' +
'CATALOG_NAME,' +
'CUBE_NAME,' +
'COLUMN_NAME,' + // JOIN ON
'MEASURE_AGGREGATOR,' + // 1, 2, 3, 4, 5 in sync with entityType.CV_AGGREGATE_FUNCTIONS
'MEASURE_AGGREGATABLE, ' +
'SEMANTIC_TYPE, ' +
'COLUMN_CAPTION, ' +
'DIMENSION_TYPE, ' +
'UNIT_COLUMN_NAME, ' +
'DESC_NAME, ' +
'"ORDER" ' +
"from " + (entityType._remoteSource ? "\"" + entityType._remoteSource + "\"" + ".\"_SYS_BI\".\"BIMC_DIMENSION_VIEW_HDI\"" : "_SYS_BI.BIMC_DIMENSION_VIEW_HDI ") +
"where SCHEMA_NAME = ? AND QUALIFIED_NAME = ? " +
'order by "ORDER"';
function processRows(err, rows) {
if (logMetadata) {
context.logger.silly('metadataReader', 'rows\n' + JSON.stringify(rows, null, 4));
}
if (err) {
return cb(new SqlError(context, err));
}
if (!rows.length) {
return cb(null, null);
}
var cubeName = rows[0].CUBE_NAME;
entityType.setCalculationViewDimensionData(rows, { cube: cubeName }, context.logger); // e.g. sap:label is fill with calculation view dimension data
entityType.resolveAggregates();
entityType.properties.forEach((property) => {
// suppressing annotation "sap:aggregation-role" depends on xsodata-file "setting"
if (entityType._settings.noDimensionAnnoOnTextProperty && entityType._settings.noDimensionAnnoOnTextProperty === 'true') {
if (property.DESC_NAME && property.DESC_NAME !== property.COLUMN_NAME) {
for (let i = 0; i < entityType.properties.length; i++) {
if (entityType.properties[i].COLUMN_NAME === property.DESC_NAME) {
entityType.properties[i].suppressAnnotationDimension = true;
}
}
}
}
});
var parameters = entityType.getParameters();
if (!parameters) {
// entity not exposed as calcview
return cb(null, entityType);
}
entityType.makeCalcview();
loadInputParameters(parameters, entityType, context, function (errInp/*, paramEntity*/) {
if (errInp) {
return cb(errInp);
}
return cb(null, entityType);
});
}
/*
Example mapping :
calc view === sap.hana.democontent.epm.models::BUYER
SCHEMA_NAME === "container schema name".
QUALIFIED_NAME === "...::BUYER"
*/
/*
WARNING WARNING
The current HANA version has a bug, and prepared statements are not working in mdx select.
Hence i replaced prepared statement with direct sql statement.
*/
let parameters = [];
if (entityType._isVirtual) {
// CV information must be read from the remote database;
// no such concept of "virtual" CV data in local database as it is the case for tables
parameters = [entityType._virtualObjectSchema, entityType._virtualObjectName];
} else {
parameters = [schema, entityType.tableName];
}
dataCollector2.executeSqlAsPreparedStatement(context, sql, parameters, processRows);
}
function loadInputParameters(parameters, entityType, context, cb) {
let sql =
'select distinct ' +
'VARIABLE_NAME, ' +
'COLUMN_TYPE_D, ' +
'COLUMN_SQL_TYPE, ' +
'MANDATORY, ' +
'DESCRIPTION, ' +
'DEFAULT_VALUE, ' +
'SEMANTIC_TYPE, ' +
'SELECTION_TYPE, ' +
'MULTILINE, ' +
'"ORDER" ' +
'from ' + (entityType._remoteSource ? '"' + entityType._remoteSource + '"' + '"_SYS_BI"."BIMC_VARIABLE_VIEW_HDI"' : '_SYS_BI.BIMC_VARIABLE_VIEW_HDI ') +
'where QUALIFIED_NAME = ? order by "ORDER"';
context.logger.debug('metadataReader', 'loadInputParameters: ' + entityType.tableName);
context.logger.debug("metadataReader", "sql loadInputParameters: " + sql);
dataCollector2.executeSqlAsPreparedStatement(context, sql, [entityType.tableName], (err, rows) => {
if (logMetadata) {
context.logger.silly('metadataReader', 'rows\n' + JSON.stringify(rows, null, 4));
}
if (err) {
return cb(new SqlError(context, err));
}
entityType.inputParameters = {};
// if rows length==0 a calcview without inputparameter is exposed
// this is ok if used like
// "xsodata.test.calcview::calcview_without_input" as "CalcviewWithoutAsCalcview"
// keys generate local "CVID" parameters via entity "CalcviewWithoutAsCalcviewParam"
// results property "Results";
// and access url /calcview_without_input.xsodata/CalcviewWithoutAsCalcviewParam()/Results
rows.forEach(function (row) {
entityType.inputParameters[row.VARIABLE_NAME] = row;
});
try {
var paramEntityType = loadInputParametersEntity(parameters, entityType, context, rows);
if (parameters.viaKey) {
/*this is called */
loadInputParametersViaKey(entityType, paramEntityType, context);
}
} catch (err_process) {
err = err_process;
}
return cb(err);
});
}
function _getLength(columnSqlType) {
const n = columnSqlType.match(/^\w+\(([0-9]+)\)$/);
if (n) {
return Number.parseInt(n[1]);
}
return null;
}
exports._getLength = _getLength;
function _getType(type, defaultType = null) {
if (type) {
const i = type.indexOf('(');
if (i > -1) {
return type.substr(0, i);
} else {
return type;
}
}
return defaultType || 'NVARCHAR';
}
exports._getType = _getType;
/*
function _getTypeLengthScale(type, defaultType = null) {
if (type) {
const n = type.match(/^(\w+)(\(([0-9]+)(,([0-9]+))?\))?$/);
if (n) {
return {
type: n[1],
length: Number.parseInt(n[3]) || null,
scale : Number.parseInt(n[5]) || null
};
}
}
return {
type: defaultType|| 'NVARCHAR',
length: null,
scale: null
};
}
*/
function loadInputParametersEntity(parameters, entityType, context, rows) {
var paramEntityName = parameters.entity;
if (!paramEntityName || paramEntityName.trim() === '') {
//No name, use default naming
paramEntityName = entityType.name + 'Parameters';
}
var resultsProp = parameters.resultProperty || 'Results';
var assocName = paramEntityName + '_to_' + entityType.name;
var _et = {
name: paramEntityName,
properties: {},
navigates: {},
keys: rows.map(function (row) {
return row.VARIABLE_NAME;
})
};
_et.navigates[resultsProp] = {
name: resultsProp,
association: assocName,
from: {
principal: true
}
};
var paramEntityType = new EntityType(_et);
var mrows = rows.map(function (row) {
//const typeLengthScale = _getTypeLengthScale(row.COLUMN_SQL_TYPE, row.COLUMN_TYPE_D );
const type = _getType(row.COLUMN_SQL_TYPE, row.COLUMN_TYPE_D);
const metadata = {
KIND: model.entityKind.inputParameters,
TABLE_NAME: null,
COLUMN_NAME: row.VARIABLE_NAME,
POSITION: row.ORDER,
DATA_TYPE_NAME: type,
IS_NULLABLE: true,
LENGTH: _getLength(row.COLUMN_SQL_TYPE),
SCALE: null,
SEMANTIC_TYPE: row.SEMANTIC_TYPE,
MANDATORY: row.MANDATORY,
DESCRIPTION: row.DESCRIPTION,
SELECTION_TYPE: row.SELECTION_TYPE,
MULTILINE: row.MULTILINE
};
return metadata;
});
assocName = paramEntityName + '_to_' + entityType.name;
var association = new Association({
name: assocName,
principal: {
type: paramEntityName,
multiplicity: '1'
},
dependent: {
type: entityType.name,
multiplicity: '*'
},
referentialConstraint: false,
over: null,
isInputParametersAssociation: true
});
paramEntityType.init(new model.Metadata(mrows), model.entityKind.inputParameters, null, context.logger);
context.gModel.addEntity(paramEntityType);
context.gModel.addAssociation(association);
return paramEntityType;
}
function loadInputParametersViaKey(entityType, paramEntityType) {
var pvalues = _.values(paramEntityType.propertiesMap);
pvalues.forEach(function (pv) {
entityType.addProperty(pv, true);
});
}
function loadEntityType(context, entityType, cbErr) {
determineStoreType(context, entityType, function (error, tableStoreType) {
if (error) {
return cbErr(error);
}
return loadTableInfo(context, entityType, tableStoreType, function (err, entityType1) {
if (err) {
return cbErr(err, entityType1);
}
return loadCalcViewInfo(context, entityType1, function (errCv, entityTypeCV) {
if (errCv) {
return cbErr(errCv);
}
try {
entityType1.buildConcurrentPropertyList();
} catch (exception) {
return cbErr(new InternalError(exception.message));
}
return cbErr(null, entityTypeCV);
}
);
});
});
}
/**
* Determines HANA store type ("column" or "row") for the table / view, which is used for the specified
* <code>entityType</code>.
*
* @param {Object} context - OData context
* @param {Object} entityType - entity type, for which table the store type should be determined
* @param {Function} callback - callback function, which is called when the operation is completed.
*/
function determineStoreType(context, entityType, callback) {
let schema = entityType.schema || context.defaultSchema,
sql = 'select IS_COLUMN_TABLE as "IS_COLUMN_TYPE" from TABLES where SCHEMA_NAME = ? AND TABLE_NAME = ? ' +
'union select IS_COLUMN_VIEW as "IS_COLUMN_TYPE" from VIEWS where SCHEMA_NAME = ? AND VIEW_NAME = ?',
tableName = entityType.tableName,
parameters = [schema, tableName, schema, tableName];
context.logger.debug("model", "sql determineStoreType: " + sql);
context.logger.debug("model", "sql determineStoreType parameters: " + parameters);
dataCollector2.executeSqlAsPreparedStatement(context, sql, parameters, (error, rows) => {
if (error) {
return callback(new SqlError(context, error));
}
// if no error and no rows are returned: check for public synonym;
// if a virtual table is used (includes remote tables and remote views / CVs), we got a row back here!
if (rows.length === 0) {
context.logger.debug("model", `No table/view information found for ${tableName}. Try public synonym.`);
processSynonymTableType(context, entityType, tableName, (synonymErr, synonymFound, tableStoreType) => {
if (synonymErr) {
return callback(synonymErr);
}
if (synonymFound) {
return callback(null, tableStoreType);
}
return callback(new NotFound(`No table/view or synonym data found for table ${tableName}.`, context));
});
} else {
// check for virtual table; add VT data for entity type, if existing
let virtualTableSQL = "select * from SYS.VIRTUAL_TABLES where schema_name = ? and table_name = ?";
let virtualTableParameters = [schema, tableName];
context.logger.info("model", `Check for virtual table for ${tableName}`);
dataCollector2.executeSqlAsPreparedStatement(context, virtualTableSQL, virtualTableParameters, (sqlError, virtualTableRows) => {
if (sqlError) {
context.logger.error("model", `Failed read data for virtual table: ${tableName}` + sqlError);
return callback(new SqlError(context, sqlError));
}
if (virtualTableRows.length !== 0) {
entityType.setVirtualInfo(
virtualTableRows[0].REMOTE_OWNER_NAME,
virtualTableRows[0].REMOTE_OBJECT_NAME,
virtualTableRows[0].REMOTE_SOURCE_NAME );
context.logger.info("model", `(Remote) Virtual table information for table ${tableName}`);
context.logger.info("model", `Remote schema: ${virtualTableRows[0].REMOTE_OWNER_NAME}`);
context.logger.info("model", `Remote object: ${virtualTableRows[0].REMOTE_OBJECT_NAME}`);
context.logger.info("model", `Remote source: ${virtualTableRows[0].REMOTE_SOURCE_NAME}`);
} else {
context.logger.info("model", `No virtual table information exists for ${tableName}`);
}
// use data already found from TABLES / VIEWS for setting store type
return processTableType(error, rows, context, tableName, callback);
});
}
});
}
function processSynonymTableType(context, entityType, tableName, callback) {
const synonymSql = `select * from SYNONYMS where SCHEMA_NAME = 'PUBLIC' AND SYNONYM_NAME = '${tableName}'`;
dataCollector2.executeSqlDirectly(context, synonymSql, (synonymErr, synonymRows) => {
if (synonymErr) {
context.logger.error("model", "Failed to read synonym: " + synonymErr);
return callback(new SqlError(context, synonymErr), false, null);
}
if (synonymRows.length === 0) {
return callback(null, false, null); // not a synonym
}
if (synonymRows.length !== 1) {
context.logger.error("model", "No (unique) synonym data found for " + tableName + " table.");
return callback(new NotFound("No (unique) synonym data found for " + tableName + " table.", context), false, null);
}
// check for local or remote synonym
if (synonymRows[0].OBJECT_SCHEMA === '_SYS_LDB') { // remote synonym
const virtualTableName = synonymRows[0].OBJECT_NAME;
const virtualTableSql = `select * from VIRTUAL_TABLES where SCHEMA_NAME = '_SYS_LDB' and TABLE_NAME = '${virtualTableName}'`;
context.logger.info("model", `Checking virtual tables for remote synonym information for table ${virtualTableName}`);
// get remote source
dataCollector2.executeSqlDirectly(context, virtualTableSql, (virtualTableErr, virtualTableRows) => {
if (virtualTableErr) {
context.logger.error("model", `Failed to read virtual table ${virtualTableName} for synonym ${tableName}: ${virtualTableErr}`);
return callback(new SqlError(context, virtualTableErr), false, null);
}
context.logger.info("model", `(Remote) Virtual table information for synonym ${tableName} via table ${virtualTableName}`);
context.logger.info("model", `Remote schema: ${virtualTableRows[0].REMOTE_OWNER_NAME}`);
context.logger.info("model", `Remote object: ${virtualTableRows[0].REMOTE_OBJECT_NAME}`);
context.logger.info("model", `Remote source: ${virtualTableRows[0].REMOTE_SOURCE_NAME}`);
entityType.setVirtualInfo(
virtualTableRows[0].REMOTE_OWNER_NAME, // remote schema name
virtualTableRows[0].REMOTE_OBJECT_NAME, // remote object name
virtualTableRows[0].REMOTE_SOURCE_NAME); // remote source
// in SYNONYMS, each remote object has always the setting IS_COLUMN_OBJECT === FALSE (keep it as is)
let isColumnStoreType = synonymRows[0].IS_COLUMN_OBJECT;
let tableStoreType = isColumnStoreType === "TRUE" ? "column" : "row";
return callback(null, true, tableStoreType);
});
} else { // local synonym
context.logger.info("model", `(Local) Synonym information for ${synonymRows[0].SYNONYM_NAME}`);
context.logger.info("model", `Local schema: ${synonymRows[0].OBJECT_SCHEMA}`);
context.logger.info("model", `Local object: ${synonymRows[0].OBJECT_NAME}`);
entityType.setVirtualInfo(
synonymRows[0].OBJECT_SCHEMA, // local schema name
synonymRows[0].OBJECT_NAME); // local object name
// in SYNONYMS, a local object has the "correct" store type setting
let isColumnObjectType = synonymRows[0].IS_COLUMN_OBJECT;
let tableStoreType = isColumnObjectType === "TRUE" ? "column" : "row";
return callback(null, true, tableStoreType);
}
});
}
/**
* Callback, which is used to handle values, produced by determineStoreType function.
*
* @param {Object} error - error, produced by determineStoreType function
* @param {Object[]} dbRows - database entries, produced by determineStoreType function
* @param {Object} context - OData context
* @param {string} tableName - DB table name, for which store type should be determined
* @param {Function} callback - callback function, which is called when the operation is completed.
*/
function processTableType(error, dbRows, context, tableName, callback) {
var isColumnStoreType,
tableStoreType;
if (error) {
return (error instanceof SqlError) ? callback(error) : callback(new SqlError(context, error));
}
if (dbRows.length === 0) {
return callback(new NotFound("No data found for " + tableName + " table.", context));
}
if (logMetadata) {
context.logger.silly("processTableType", "rows:\n" + JSON.stringify(dbRows, null, 4));
}
// if found table is a virtual (remote) table, the IS_COLUMN_TYPE is always set to FALSE,
// but this setting does not affect SQL processing (joins on temp. tables)
isColumnStoreType = dbRows[0].IS_COLUMN_TYPE;
// HANA returns "TRUE"/"FALSE" as a string and not as a boolean value
tableStoreType = isColumnStoreType === "TRUE" ? "column" : "row";
return callback(null, tableStoreType);
}
exports._processTableType = processTableType;
function createLockeEntityID(context, entityType) {
return (context.uriTree.xsoFile || '') + '_' + entityType.name;
}
function loadEntityTypeOnce(context, entityType, cbErr) {
var lockEntityID = createLockeEntityID(context, entityType);
if (entityType.isInitialised) {
//access to __metadata is writelooked, so __metadata is definitively filled here
return cbErr();
}
context.logger.silly('model', 'entitytype load: ' + lockEntityID);
locks.writeLock(lockEntityID, function (release) {
context.logger.silly('model', 'got lock for: ' + lockEntityID);
//if loaded in the meantime
if (entityType.isInitialised) {
context.logger.silly('model', 'type already initialized: ' + lockEntityID);
release();
return cbErr();
}
var load = function (err, context) {
if (err) {
release();
return cbErr(err);
}
return loadEntityType(context, entityType, function (err) {
release();
return cbErr(err);
});
};
try {
if (Measurement.isActive()) {
var callConnect = utils.tryAndMeasure(db.connect, 'dbConnect');
return callConnect(context, load);
} else {
return db.connect(context, load);
}
} catch (err) {
release();
return cbErr(err);
}
});
}
exports.loadModelMetadata = function (context, asyncDone) {
context.logger.silly('model', 'load metadata');
var entityTypes = context.gModel.getEntityTypes();
if (Measurement.isActive()) { // perform an explicit measurement
var parent = Measurement.getRunningMeasurement();
}
async.mapSeries(
entityTypes,
function (entityType, cbMapLimit) {
var child;
try {
if (Measurement.isActive() && !context.batchContext) {
child = parent.newChild(entityType.name);
child.counterStart();
}
return loadEntityTypeOnce(context, entityType, function (err) {
//stop measurement
if (Measurement.isActive() && !context.batchContext) {
child.counterStop();
}
return cbMapLimit(err);
});
} catch (ex) {
//stop measurement
if (Measurement.isActive() && !context.batchContext) {
child.counterStop();
}
return cbMapLimit(ex);
}
},
function (err) {
if (err) {
context.logger.error('model', `Error on loadEntityTypes for ${context.uriTree.xsoFile}:` + err);
}
context.logger.silly('model', 'loadEntityTypes finished for ' + context.uriTree.xsoFile);
return asyncDone(err, context);
}
);
};