@sap/xsodata
Version:
Expose data from a HANA database as OData V2 service with help of .xsodata files.
1,000 lines (908 loc) • 34.5 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
);
const 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');
const 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 {
const 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) {
const overwrittenColumn =
entityType.propertiesMap[
entityType.keys.generatedKey
];
if (overwrittenColumn) {
// Provided generated key overwrites an existing column
let 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"';
const processRows = function (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);
}
const 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 (const entityProperty of entityType.properties) {
if (entityProperty.COLUMN_NAME === property.DESC_NAME) {
entityProperty.suppressAnnotationDimension = true;
}
}
}
}
});
const 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 {
const paramEntityType = loadInputParametersEntity(
parameters,
entityType,
context,
rows
);
if (parameters.viaKey) {
/*this is called */
loadInputParametersViaKey(entityType, paramEntityType);
}
} catch (err_process) {
err = err_process;
}
return cb(err);
}
);
}
function _getLength(columnSqlType) {
const n = columnSqlType.match(/^\w+\((\d+)\)$/);
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 loadInputParametersEntity(parameters, entityType, context, rows) {
let paramEntityName = parameters.entity;
if (!paramEntityName || paramEntityName.trim() === '') {
//No name, use default naming
paramEntityName = entityType.name + 'Parameters';
}
const resultsProp = parameters.resultProperty || 'Results';
let assocName = paramEntityName + '_to_' + entityType.name;
const _et = {
name: paramEntityName,
properties: {},
navigates: {},
keys: rows.map(function (row) {
return row.VARIABLE_NAME;
}),
};
_et.navigates[resultsProp] = {
name: resultsProp,
association: assocName,
from: {
principal: true,
},
};
const paramEntityType = new EntityType(_et);
const mrows = rows.map(function (row) {
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;
const 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) {
const 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) {
let 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) {
const 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();
}
const load = function (err, context) {
if (err) {
release();
return cbErr(err);
}
return loadEntityType(context, entityType, function (err) {
release();
return cbErr(err);
});
};
try {
if (Measurement.isActive()) {
const 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');
const entityTypes = context.gModel.getEntityTypes();
let parent;
if (Measurement.isActive()) {
// perform an explicit measurement
parent = Measurement.getRunningMeasurement();
}
async.mapSeries(
entityTypes,
function (entityType, cbMapLimit) {
let 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);
}
);
};