@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
JavaScript
;
/**
* 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;
}