UNPKG

@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
'use strict'; 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); } ); };