UNPKG

@sap/xsodata

Version:

Expose data from a HANA database as OData V2 service with help of .xsodata files.

490 lines (394 loc) 12.6 kB
'use strict'; /** * Atom xml to odata json serializer. * * This module converts the atom xml format into defined odata json we * use to handle 'modify' odata requests. * * Usage: * * var xmlStringData = getXmlStringSomeHow().toString('urf-8'); * * var atomSerializer = new AtomXmlToJsonSerializer(xmlStringData, null, { * logger:logger * }); * * atomSerializer.serialize(function(err, result){ * // handle err if != null * // handle json result * }); * */ var lodash = require('lodash'); var utils = require('./../utils/utils'); var XmlSerializer = require('./xmlToJsonSerializer'); var inherits = require('util').inherits; var typeConverter = require('./../utils/typeConverter'); var NAMESPACES = { m: "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata", d: "http://schemas.microsoft.com/ado/2007/08/dataservices" }; /** * Constructor function. Creates instance of AtomXmlToJsonSerializer. * * You can provide a logger through options function like * { * logger: { * silly: function(...){} * debug: function(...){} * trace: function(...){} * } * } * * The typeModelContext parameter must an object like * * { * "EmployeeId": { * "DATA_TYPE_NAME": "NVARCHAR", * }, * ... * } * * Each field found in this parameter will be validated. * * @param xmlString {String} Xml string to parse * @param typeModelContext {Object} Type model context to validate result model against * @param [options] {Object} Options * @constructor */ function AtomXmlToJsonSerializer(xmlString, typeModelContext, options) { if ((this instanceof AtomXmlToJsonSerializer) !== true) { return new AtomXmlToJsonSerializer(xmlString, typeModelContext, options); } XmlSerializer.call(this, xmlString, options); this .after(normalize) .after(validateNamespace) .after(toOdataJson(typeModelContext)); } inherits(AtomXmlToJsonSerializer, XmlSerializer); module.exports = AtomXmlToJsonSerializer; /** * This method starts the serialization process. * * The callback function has to be provided and expected signature is: * * function (err, resultJson) { * } * * err - Will be null if there is no error * resultJson - Json result after serialization * * @chainable * @param done {Function} Callback called on end * @returns {AtomXmlToJsonSerializer} Instance of AtomXmlToJsonSerializer; */ AtomXmlToJsonSerializer.prototype.toODataJson = function (done) { this.serialize(function (err, context) { done(err, context.result); }); return this; }; /** * Convinience method --> See #toODataJson method * @chainable */ AtomXmlToJsonSerializer.prototype.toJson = AtomXmlToJsonSerializer.prototype.toODataJson; /** * Normalizing the parsed json result and clean current context object. * * Build new context by attach the document and the namespaces to * a new context object: * * { * document:..., * namespaces:... * } * @private * @param context {Object} Result context object * @param asyncDone {Function} The async function call on finish */ function normalize(context, asyncDone) { var result = { document: context.result.document, namespaces: context.result.namespaces }; asyncDone(null, result); } /** * Converts the internal document json from parser into * odata json. * * @private * @param typeModelContext {Object} type model context */ function toOdataJson(typeModelContext) { return function (context, asyncDone) { var doc = context.document; var ns = context.namespaces; var nsMetadataPrefix = getNamespacePrefix(NAMESPACES.m, ns); var nsDataservicePrefix = getNamespacePrefix(NAMESPACES.d, ns); var propertiesKey = nsMetadataPrefix + ":properties"; var entries = findEntries(doc); var entryProperties = findEntryProperties(entries, propertiesKey); context.result = entryProperties .map(function (item) { return buildOdataJson(item, nsDataservicePrefix, nsMetadataPrefix, typeModelContext); }); asyncDone(null, context); }; } /** * * @throws {Error} Throws error when provided value is not edm type compatible * @param key * @param value * @param typeModelContext * @returns {string|number|boolean} */ function mapDataValueTypeToEdmType(key, value, typeModelContext) { /*jshint eqnull:true */ if (typeModelContext == null) { return value; } var modelTypeInfo = typeModelContext[key]; return typeConverter.serializeXmlValueToODataJsonValue(value, modelTypeInfo); } /** * Builds odata json (like http json payload) from provided entry. * * The odata json is the same as the http payload json which must be provided * in a http request. * * @private * @throws {Error} Throws error if any payload value is not a valid emd type * @param entry {Object} The m:properties entry object * @param nsDataservicePrefix {String} Namespace dataservice prefix * @param nsMetadataPrefix {String} Namespace metadataservice prefix * @param typeModelContext {Object} Object representing the model type context * @returns {Object} Odata representing json object */ function buildOdataJson(entry, nsDataservicePrefix, nsMetadataPrefix, typeModelContext) { var target = {}; var propertiesRegEx = new RegExp(nsMetadataPrefix + ":properties\.", "g"), dataServiceRegEx = new RegExp(nsDataservicePrefix + ":", "g"), childrenRegEx = new RegExp("\\$children\\.", "g"), valueRegEx = new RegExp("\\.\\$value$", "g"); utils.iterateObject(entry, function (key, obj, keyPath) { // keypath is an array containing the current recursive properties path // i.e. key looks like --> m:properties.$children.d:EmployeeId.$value key = keyPath.join('.'); var value = obj.$value; var targetKey = key .replace(propertiesRegEx, "") .replace(dataServiceRegEx, "") .replace(childrenRegEx, "") .replace(valueRegEx, ""); /*jshint eqnull:true */ if (value != null) { // Can throw error if provided value is not a valid edm type value = mapDataValueTypeToEdmType(targetKey, value, typeModelContext); target[targetKey] = value; } }, { filter: function (key /* obj, keyPath */) { // If key is '$value' we reached leaf of the object tree. // The $value contains the defined value we will use for odata json object return key === "$value"; }, recursive: true }); return target; } /** * Return the entries m:properties value. * * This method takes the parsed json data and runs through * all children properties to find the child with m:properties * object key. * * @private * @param entries the entries array * @param propertiesKey the propertiesKey to search for * @returns {Array} Result array containing all m:properties objects */ function findEntryProperties(entries, propertiesKey) { var properties = []; utils.iterateObject(entries, function validation(key, obj) { properties.push(obj); // Return false means do not recurse deeper return false; }, { filter: function filter(key /* , obj */) { // Call callback only if current key is propertiesKey (i.e. m:properties) return key === propertiesKey; }, recursive: true }); return properties; } /** * Returns the entries array from within the provided parsing result. * * The parsing result is parsed from atom xml format. The source * could be: * * <feed> * <entry> * ... * <m:properties> * ... * </m:properties> * ... * </entry> * <entry>...</entry> * </feed> * * but also * * <entry> * ... * <content type="application/xml"> * <m:properties> * ... * </m:properties> * </content> * ... * </entry> * * is allowed. This method checks which kind of * atom xml is used and returns the corresponding entries. * * @private * @param result {Array} Xml parsed result object * @returns {Array} Returns the corresponding properties */ function findEntries(result) { if (result.length === 0) { return result; } if (result[0].feed) { return lodash(result[0].feed.$children) .filter(function (item) { // We filter out all children which do not have entry property // There childrens are like <link> or <author>... /*jshint eqnull:true */ return (item.entry != null) === true; }).value(); } else { // No feed element found? --> We have default entries here return result; } } /** * Returns the namespace prefix for a provided namespace definition. * * This is like returning "m" for * "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" * namespace. * * @private * @param namespaceDef {String} namespace definition to seach prefix for * @param namespaces {Object} Available namespaces to seach in * @returns {String} Return namespace prefix or null if there is none */ function getNamespacePrefix(namespaceDef, namespaces) { var search = null; lodash.forOwn(namespaces, function (value, key) { if (namespaceDef === value) { search = key; return false; // Returning false in lodash.forOwn() stops further looping } }); return search; } /** * * Validates the parsed document against namespaces. * * Each namespace bounded property is validated against its namespace. * Throws an error if a bound namespace does not have a corresponding namespace. * * @private * @param context {Object} Main context object * @param asyncDone {Function} Function called when done */ function validateNamespace(context, asyncDone) { var target = context.document; var namespaces = context.namespaces; // Iterate over all object properties and corresponding // object child property values properties recursive utils.iterateObject(target, function validation(key /* , obj */) { var isValid = isNamespacedValue(key, namespaces); if (isValid !== true) { throw new Error("No valid namespace for key \"" + key + "\" available"); } }, { filter: function filter(key /*, obj */) { // Call validation function only if filter returns true return !isExceptionalNamespaceProperty(key); }, recursive: true }); asyncDone(null, context); } /** * Checks if the provided value parameter is namespaces bounded. * * This method validates if the provided value is a namespace bounded value * (like m:properties). If a namespace bounded value is found the * namespace prefix of the value is validated against list * of provided namespaces. Namespaces parameter is expected as: * * { * <prefix>:<namespace> * } * * The next function parameter can be used to inject your own type of * validation. * * @private * @param value {String} String to validate namespace * @param namespaces - List of namespaces to validate against * @param nextFn - If provided called at the end of validation * @returns {Boolean} true if valid, else false */ function isNamespacedValue(value, namespaces, nextFn) { var split = value.split(":"); if (split.length !== 2) { return true; } else { var prefix = split[0]; var namespace = namespaces[prefix]; if (nextFn && lodash.isFunction(nextFn)) { return nextFn(prefix, namespace, namespaces); } else { /*jshint eqnull:true */ return (namespace != null); } } } /** * Check if the provided value parameter contains special strings. * * Checks provided value for string occurencies which is not * a namespace usage itself but its definition like "xml:base" * or "xmlns" * * @private * @param value {String} The value to test * @returns {boolean} - true if special string was found, else false */ function isExceptionalNamespaceProperty(value) { /*jshint eqnull:true */ if (value == null || !lodash.isString(value)) { return true; } if (value.indexOf("xml:base") !== -1) { return true; } if (value.indexOf("xmlns") !== -1) { return true; } return false; }