UNPKG

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