@sap/xsodata
Version:
Expose data from a HANA database as OData V2 service with help of .xsodata files.
442 lines (351 loc) • 14.8 kB
JavaScript
;
/**
* Add <xsodata> attribute to <context> (for sample data see sampleXsodata)
* Uses <context.serviceDirectory>,<context.uriTree.xsoFile>
* For Sample output structure see /test/test_xsodata_file_parser/xs1testfiles/*
* @type {exports}
*/
//Include
var path = require('path');
var peg_parser = require('../parsers/peg_xsodata_parser');
var fs = require('fs');
var ModelFileError = require('./../utils/errors/modelFileError');
var Rwlock = require('rwlock');
var locks = new Rwlock();
var model = require('./model');
var async = require('async');
var utils = require('../utils/utils');
var InternalError = require('../utils/errors/internalError');
//Testing API
exports.parseContent = parseContent;
exports._validateModel = validateModel;
// external api
exports.loadXsodataConfiguration = loadXsodataConfiguration;
//Code
function parseContent(content, done) {
try {
var data = peg_parser.parse(content);
done(null, data);
} catch (ex) {
return done(ex);
}
}
/**
* Reads xsodata configuration.
*
* This method is a facade for calling the following internal methods:
*
* loadXsodataConfiguration()
* |
* +-- readXsodataConfigFromCache()
* |
* +-- readXsodataConfigFromFile()
* |
* +-- extendXsodataConfig()
* |
+-- extendNavigatesFromProperty()
-------------------------|
|
+-- createModelInstance()
* |
* +-- addConfigToCache()
*
* If reading the configuration model from internal cache succeeded the xsodata file won't be read.
* In this case the procesing stops and returns immediately. If not the xsodata will be read from file.
* All validations and default configuration extensions is done when the configuration is read from file storage
* once. After reading from file we can expect that the returning and cached configuration
* model is valid.
*
* @param context {Object} Current context object
* @param asyncDone {Function} Callback beeing called after finishing the function
* @since v0.2.0-beta.3
*/
function loadXsodataConfiguration(context, asyncDone) {
// context is request context
context.logger.silly('model', 'loadXsodataConfiguration');
async.waterfall([
utils.injectContext(context),
// First try to find configuration in cache
utils.tryAndMeasure(readXsodataConfigFromCache, 'readXsodataConfigFromCache'),
// Read from file if model was not already< found in cache
utils.tryAndMeasure(readXsodataConfigFromFile, 'readXsodataConfigFromFile'),
// Extends the current configuration by default or nesseccary properties
utils.tryAndMeasure(extendXsodataConfig, 'extendXsodataConfig'),
// Creates the internal xsodata abstraction configuration model
utils.tryAndMeasure(createModelInstance, 'createModelInstance'),
// If the configuration was red it will be add to internal cache to avoid I/O for next call
utils.tryAndMeasure(addConfigToCache, 'addConfigToCache')
], function (endErr, context) {
var id = getConfigurationId(context);
context.logger.info('model', 'using model id: ' + id);
return asyncDone(endErr, context);
});
}
/**
* Read the xsodata configuration from cache and assign to context.gModel property
*
* @param context {Object} Current context object
* @param asyncDone {Function} Callback beeing called after finishing the function
* @since v0.2.0-beta.3
*/
function readXsodataConfigFromCache(context, asyncDone) {
var locale = context.locale;
context.logger.silly('model', 'readXsodataConfigFromCache');
if (!context.modelData[locale]) {
context.modelData[locale] = {};
}
var id = getConfigurationId(context);
context.gModel = context.modelData[locale][id];
return asyncDone(null, context);
}
/**
* Extending the configuration model by default or optional properties.
*
* Here is the place for any kind of xsodata configuration extension.
* Just add a new async.waterfall(...) function to extend new functionality.
* This works like a chain of responsibility pattern.
*
* @param context {Object} Current context object
* @param asyncDone {Function} Callback beeing called after finishing the function
* @since v0.2.0-beta.3
*/
function extendXsodataConfig(context, asyncDone) {
if (context.gModel) {
// If configuration already exists we do not want to read from file
return asyncDone(null, context);
}
context.logger.silly('model', 'extendXsodataConfig');
async.waterfall([
utils.injectContext(context),
// Extends ... navigates ... from principal | dependent property
utils.try(extendNavigatesFromProperty),
utils.try(checkSchema)
], function (endErr, context) {
return asyncDone(endErr, context);
});
}
/**
* Check if a schema is defined on a entity set and if yes, check if this schema is allowed
*
* @param context {Object} Current context object
* @param asyncDone {Function} Callback beeing called after finishing the function
* @since v0.2.0-beta.3
*/
function checkSchema(context, asyncDone) {
var parsedXsodata = context._initialParsedXsodata;
var entityTypes;
var entityType;
var key;
context.logger.silly('model', 'checkSchema');
if (!parsedXsodata || !parsedXsodata.service) {
return asyncDone(null, context);
}
if (!parsedXsodata.service.entityTypes) {
return asyncDone(null, context);
}
entityTypes = parsedXsodata.service.entityTypes;
// Run through entityTypes and check if navigates property id provided
for (key in entityTypes) {
if (entityTypes.hasOwnProperty(key) === true) {
entityType = entityTypes[key];
if (entityType.schema) {
if (!utils.schemaAllowed(entityType.schema)) {
return asyncDone(new ModelFileError('Invalid schema', context), context);
}
}
}
}
return asyncDone(null, context);
}
/**
* Extending the configuration model by adding navigates --> from principal|dependent
*
* Referring SAP HANA Develper Guide for SPS 11, paragraph 7.1.6.6 OData Associations
* the navigates ... from principal | dependent is optional and must be abbreviated
* from configuration and corresponding callee entity.
*
* This is done by convention if the caller entity type is defined as
* the principal in the association referenced by its navigates property
* the corresponding "navigates from" type will be
* "navigates ... from principal"
* and vice versa.
*
* @param context {Object} Current context object
* @param asyncDone {Function} Callback beeing called after finishing the function
* @since v0.2.0-beta.3
*/
function extendNavigatesFromProperty(context, asyncDone) {
var parsedXsodata = context._initialParsedXsodata,
entityTypes,
associations,
key,
entityType,
navigates,
key2,
navigation,
associationName,
association;
context.logger.silly('model', 'extendNavigatesFromProperty');
if (!parsedXsodata || !parsedXsodata.service) {
return asyncDone(null, context);
}
if (!parsedXsodata.service.entityTypes || !parsedXsodata.service.associations) {
return asyncDone(null, context);
}
entityTypes = parsedXsodata.service.entityTypes;
associations = parsedXsodata.service.associations;
// Run through entityTypes and check if navigates property id provided
for (key in entityTypes) {
if (entityTypes.hasOwnProperty(key) === true) {
entityType = entityTypes[key];
navigates = entityType.navigates;
if (!navigates) {
continue;
}
// Run through navigates property and check if any corresponding association is available
for (key2 in navigates) {
if (navigates.hasOwnProperty(key2) === true) {
navigation = navigates[key2];
associationName = navigation.association;
if (!associationName) {
return asyncDone(new InternalError("Missing association after 'navigates' property", context), context);
}
association = associations[associationName];
if (!association) {
return asyncDone(new InternalError("Unknown association: " + associationName, context), context);
}
// If corresponding association and navigates hasn't any from property we assign
// a corresponding "from principal | dependent" by default
if (!navigation.hasOwnProperty('from')) {
navigation.from = {};
if (entityType.name === association.principal.type) {
navigation.from.principal = true;
} else if (entityType.name === association.dependent.type) {
navigation.from.dependent = true;
} else {
return asyncDone(new ModelFileError('Can not extend configuration model. Given entityType name does not match allowed associations', context), context);
}
}
}
}
}
}
return asyncDone(null, context);
}
/**
* Create the xsodata model instance and attach to context context.gModel property.
*
* @param context {Object} Current context object
* @param asyncDone {Function} Callback beeing called after finishing the function
* @since v0.2.0-beta.3
*/
function createModelInstance(context, asyncDone) {
context.logger.silly('model', 'createModelInstance');
if (!context || !context._initialParsedXsodata || !context._initialParsedXsodata.service) {
return asyncDone(null, context);
}
context.gModel = new model.Model(context._initialParsedXsodata);
return asyncDone(null, context);
}
/**
* Validates the model context in dependency of the registered validators.
*
* @param context {Object} Current context object
* @param asyncDone {Function} Callback beeing called after finishing the function
* @since v0.3.2
*/
function validateModel(context, asyncDone) {
context.logger.silly('model', 'validateModel');
async.waterfall([
utils.injectContext(context),
// Validate xsodata configuration concurrencyToken
require(__dirname + "/validator/xsoDataConcurrencyTokenValidator").validate
], asyncDone);
}
/**
* Adds the current xsodata configuratio to context cache. Att next service call
* the configuration will be loaded from internal cache and not read from file again.
*
* @param context {Object} Current context object
* @param asyncDone {Function} Callback beeing called after finishing the function
* @since v0.2.0-beta.3
*/
function addConfigToCache(context, asyncDone) {
var locale = context.locale;
context.logger.silly('model', 'addConfigToCache');
if (!context.gModel) {
return asyncDone(new ModelFileError("Xsodata configuration model does not exist. Can not add to internal cache.", context), context);
}
var id = getConfigurationId(context);
context.modelData[locale][id] = context.gModel;
return asyncDone(null, context);
}
/**
* Read the xsodata from configuration file and parse the content to json. The result configuration will be assigend
* to context.gModel property. If the current context already has a context.gModel
* property this method returns immediately.
*
* @param context {Object} Current context object
* @param asyncDone {Function} Callback beeing called after finishing the function
*/
function readXsodataConfigFromFile(context, asyncDone) {
if (context.gModel) {
// If configuration already exists we do not want to read from file
return asyncDone(null, context);
}
var id = getConfigurationId(context);
context.logger.silly('model', 'readXsodataConfigFromFile');
context.logger.silly('model', 'serviceConfiguration : ' + context.serviceConfiguration);
context.logger.silly('model', 'xsoFile: ' + context.uriTree.xsoFile);
var fullName;
if (context.uriTree.xsoFile) {
fullName = path.join(context.serviceConfiguration, context.uriTree.xsoFile);
if (fullName.indexOf(context.serviceConfiguration) !== 0) {
//SAP productstandard security, but avoid dos, so a warning
context.logger.warn('SECURITY illegal access to ' + fullName + 'prevented');
return asyncDone(new ModelFileError('File not found!', context), context);
}
} else {
fullName = context.serviceConfiguration;
}
context.logger.silly('model', 'full name: ' + fullName);
//lock on loading the content of fileName
locks.writeLock(id, function (release) {
context.logger.silly('model', 'loading model ' + fullName + ' file');
//load model
fs.readFile(fullName, 'utf-8', function (err, data) {
if (err) {
context.logger.info('model', 'file not found: ' + fullName + 'err: ' + JSON.stringify(err));
release();
return asyncDone(new ModelFileError('xsodata file not found!', context, err), context);
}
try {
//for sample more model see test/test_xsodat_file_parser/xs1testfiles/model.json
context._initialParsedXsodata = peg_parser.parse(data);
context.logger.info('model', 'loaded ' + fullName + ' file');
return asyncDone(null, context);
} catch (ex) {
context.logger.error('model', 'Error while parsing xsodata file: ' + fullName + '\nErr:\n' + JSON.stringify(ex, null, 4));
return asyncDone(new ModelFileError("Error while parsing xsodata file.", context, ex), context);
} finally {
release(); //other request may now use the metadata
}
});
});
}
/**
* Return the current configuration identifier. This is the xsodata file name by default or the
* context.serviceConfiguration property.
*
* @param context {Object} Current context object
* @return {String} Configuration identifier
* @since v0.2.0-beta.3
*/
function getConfigurationId(context) {
context.logger.silly('model', 'getConfigurationId');
if (context.uriTree.xsoFile) {
return context.uriTree.xsoFile;
} else {
return context.serviceConfiguration;
}
}