mastercard-api-core
Version:
Core functionality for MasterCard API
609 lines (507 loc) • 19.5 kB
JavaScript
/*
* Copyright 2016 MasterCard International.
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of
* conditions and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
* Neither the name of the MasterCard International Incorporated nor the names of its
* contributors may be used to endorse or promote products derived from this software
* without specific prior written permission.
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
* SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
* IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
* IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
*/
/**
* This module exposes the different domain object on which different API calls can be invoked upon.
*
* @class MasterCardAPI
* @static
*/
var MasterCardAPI = {};
// Imports
var mastercardError = require('./lib/error');
var constants = require('./lib/constants');
var oauth = require("./lib/security/oauth/oauth");
var operationConfig = require("./lib/operation-config");
var operationMetaData = require("./lib/operation-metadata");
var MESCryptography = require("./lib/security/mpqrmes/mes-cryptography");
var utils = require("./lib/utils");
var request = require('request');
var url = require('url');
// Variables
var authentication = null;
var initialized = false;
var registeredInstances = {};
var cryptographyInterceptors = [];
var proxy = null;
var debug = false;
var test = false;
var environment = constants.Environment.SANDBOX;
/**
* Initialize the Core MasterCard API SDK
*
* @param opts
* {Boolean} sandbox - true (use sandbox), false (use production)
* {Boolean} authentication - authentication object e.g. MasterCardAPI.OAuth
*/
MasterCardAPI.init = function (opts) {
//arizzini: backward compatible
if (utils.isSet(opts.sandbox)) {
if (opts.sandbox == true) {
environment = constants.Environment.SANDBOX;
} else {
environment = constants.Environment.PRODUCTION;
}
} else if (utils.isSet(opts.environment)) {
environment = opts.environment;
} else {
environment = constants.Environment.SANDBOX;
}
MasterCardAPI.setEnvironment(environment);
if (utils.isSet(opts.debug)) {
debug = opts.debug;
} else {
debug = false;
}
if (utils.isSet(opts.proxy)) {
proxy = opts.proxy;
}
if (utils.isSet(opts.authentication)) {
authentication = opts.authentication;
}
else {
throw new Error("Authentication must be set");
}
initialized = true;
};
MasterCardAPI.setEnvironment = function(env) {
for (var resourceConfigKey in registeredInstances) {
registeredInstances[resourceConfigKey].setEnvironment(env);
}
environment = env;
}
MasterCardAPI.setProxy = function(proxyHost) {
if (utils.isSet(proxyHost)) {
proxy = proxyHost;
} else {
proxy = null;
}
}
MasterCardAPI.registerResourceConfig = function(instance) {
// console.log("registerResourceConfig.... "+instance.getName());
if (! (instance.getName() in registeredInstances)) {
// console.log("registered.... ");
registeredInstances[instance.getName()] = instance;
}
};
MasterCardAPI.getEnvironment = function() {
return environment;
};
MasterCardAPI.addCryptographyInterceptor = function(cryptographyInterceptor) {
if (cryptographyInterceptor == null) {
return;
}
cryptographyInterceptors.push(cryptographyInterceptor);
}
/**
* Function to execute an API call.
*
* @param {Object} opts
* {String} path - The path for the API
* {String} action - The type of action being invoked on the domain object
* {Object} params - The request parameters
* @param {Function} callback - A callback function to handle an success/error responses from the API
*/
MasterCardAPI.execute = function (opts, callback) {
try {
// Check SDK has been correctly initialized
_checkState();
var operationConfig = opts.operationConfig;
var operationMetaData = opts.operationMetaData;
var params = opts.params;
var requestOptions = _getRequestOptions(params, operationConfig, operationMetaData);
if (debug) {
console.log( "---- Request ----");
console.log( "URL");
console.log( requestOptions.method+"="+requestOptions.uri.href);
console.log( "");
console.log( "Headers");
console.log( JSON.stringify(requestOptions.headers) );
console.log( "");
console.log( "Body" );
console.log( requestOptions.body );
console.log( "------------------");
console.log( "" );
}
request(requestOptions, function (err, res, body) {
if (err) {
// Catch our timeout error thrown below
if (err.code === "ECONNRESET") {
callback(new mastercardError.APIError('The API request has timed out', err, null), null, (res) ? res.headers : null);
} else if (err.code === "ECONNREFUSED") {
callback(new mastercardError.APIError('The API server refused the connection', err, null), null, (res) ? res.headers : null);
} else if (err.code === 'ETIMEDOUT') {
callback(new mastercardError.APIError('The API server connection timeout', err, null), null, (res) ? res.headers : null);
} else {
callback(new mastercardError.APIError('Error executing API call', err, null), null, (res) ? res.headers : null);
}
} else {
var statusCode = res.statusCode
if (debug) {
console.log( "---- Response ----");
console.log( "Status");
console.log( statusCode);
console.log( "");
console.log( "Headers");
console.log( JSON.stringify(res.headers) );
console.log( "");
console.log( "Body" );
console.log( body );
console.log( "------------------");
console.log( "");
}
if (statusCode < 300) {
var parsedResponse = body;
try {
if (body && body.length > 0) {
parsedResponse = JSON.parse(body);
var interceptor = _getCryptographyInterceptor(requestOptions.uri.href);
if (interceptor != null && typeof interceptor.decrypt === 'function') {
parsedResponse = interceptor.decrypt(parsedResponse);
}
}
}
catch (e) {
console.error("Error parsing json response. Status: " + statusCode)
}
// NB: Don't want to call callback in catch above as if the callback function throws an error it
// will go back into the catch
callback(null, parsedResponse, (res) ? res.headers : null);
} else {
var parsedResponse = body;
try {
if (body && body.length > 0) {
parsedResponse = JSON.parse(body);
}
}
catch (e) {
console.error("Error parsing json-error response. Status: " + statusCode)
}
callback(new mastercardError.APIError('Error executing API call', parsedResponse, statusCode), null, (res) ? res.headers : null);
}
}
});
}
catch(e) {
callback(e, null, null);
}
};
/**
* Function to check whether a value/object is 'undefined', null, false or empty.
* Expose for use in SDKs
*
* @param {Object} value - Object to check
* @return {Boolean} Returns true if object's type is 'undefined'
*/
MasterCardAPI.isSet = function (value) {
return utils.isSet(value);
};
/**
* Function to check if the SDK has been initialized correctly.
*
* @private
* @return {Boolean} Returns true if both Consumer Key and Authentication are set
* @throws mastercardError.APIError
*/
function _checkState() {
// Check if initialized
if (!initialized) {
throw new mastercardError.SDKError('MasterCardAPI.init(opts) must be called');
}
if (test === false)
{
if (!utils.isSet(authentication)) {
throw new mastercardError.SDKError('Authentication must be set');
}
if (!utils.isSet(authentication.consumerKey)) {
throw new mastercardError.SDKError('Consumer Key is not set');
}
}
return true;
}
/**
* Function to build up the URI endpoint to use in the request.
*
* @private
* @param {Object} params - The request parameters
* @param {Object} operationConfig
* <pre>
* path - The path for the API<br>
* action - The type of action being invoked on the domain object<br>
* queryParams - List of additional query parameters to support where Query and Body parameters used for create / update
* </pre>
* @param {Object} operationMetaData<br>
* <pre>
* host - the host to send to if overridden
* </pre>
*
* @return {Object} Returns a URI object needed for a HTTP request
*/
function _getURI(params, operationConfig, operationMetaData) {
var uri = operationMetaData.host
var resourcePath = operationConfig.path;
if (resourcePath.indexOf("#env") > 0) {
//arizzini: we have found an envirment marker
var tmpContext = "";
if (utils.isSet(operationMetaData.context)) {
tmpContext = operationMetaData.context;
}
resourcePath = resourcePath.replace("#env", tmpContext);
//arizzini: just in case we passig empty in the env which causes to form
//double forward slash in the url.
resourcePath = resourcePath.replace("//", "/");
}
// Modify URI to point to the correct API path
uri = uri + resourcePath;
// replace the path parameters
uri = _replacePathParameters(uri, params);
// Add query params to URI if any
switch (operationConfig.action) {
case "read":
case "list":
case "delete":
case "query":
uri = _appendMapToQueryString(uri, params);
break;
}
// create and update may have Query and Body parameters as part of the request.
// Check additionalQueryParametersList is set
if (utils.isSet(operationConfig.queryParams)) {
switch (operationConfig.action) {
case "create":
case "update":
// Get the submap of query parameters which also removes the values from objectMap
var queryParams = utils.subMap(params, operationConfig.queryParams);
uri = _appendMapToQueryString(uri, queryParams);
break;
}
}
// Add Format=JSON
if (operationMetaData.jsonNative == false) {
uri = _appendMapToQueryString(uri, { Format: "JSON" });
}
// Use node js 'url' module to create URI object
return url.parse(uri);
};
var _replacePathParameters = function(path, map)
{
var pathParameterRegex = /(\{.*?\})/g;
var pathToReplace = path;
var match = pathParameterRegex.exec(path);
while (match != null) {
// get the first matching group
var group = match[0];
// extract the key value by removing {}
var key = group.slice(1, -1);
if (key in map) {
// replace {user_id} with id in the map
pathToReplace = pathToReplace.replace(group, map[key]);
// remove the replace value from the map
delete map[key];
} else {
// something went wrong.. p45
throw new mastercardError.SDKError("Required path parameter: '"+key+"' not found on input map");
}
// iterate to next
match = pathParameterRegex.exec(path);
}
return pathToReplace;
};
/**
* Append map as parameters to URL
*
* @param {Object} map
* @param {String} uri - the current uri
* @returns {String} uri
*/
var _appendMapToQueryString = function(uri, map) {
for (var key in map) {
uri = _appendQueryString(uri, key, map[key]);
}
return uri;
};
/**
* Append a key and value as a parameter to the uri
*
* @param {String} uri
* @param {String} key
* @param {Object} value
* @returns {string|*}
* @private
*/
var _appendQueryString = function(uri, key, value) {
if (uri.indexOf("?") == -1) {
uri += "?";
}
if (uri.indexOf("?") != uri.length - 1) {
uri += "&";
}
uri += utils.uriRfc3986Encode(key) + "=" + utils.uriRfc3986Encode(value);
return uri;
};
var _getCryptographyInterceptor = function(uri) {
var interceptorSize = cryptographyInterceptors.length;
for (var i = 0; i < interceptorSize; i++) {
var interceptor = cryptographyInterceptors[i];
if (interceptor == null || typeof interceptor.getTriggeringEndPath !== 'function') {
continue;
}
var triggerPaths = interceptor.getTriggeringEndPath();
var triggerPathsSize = triggerPaths.length;
for(var j = 0; j < triggerPathsSize; j++) {
var triggeringPath = triggerPaths[j];
if (uri === triggeringPath || uri.endsWith(triggeringPath) || RegExp(triggeringPath).test(uri)) {
return interceptor;
}
}
}
}
/**
* Function to construct the options map needed for the API request.
*
* @private
* @param {String} httpMethod - The type of HTTP request being made e.g. 'POST'
* @param {Object} uri - An object containing the properties of a URI
* @param {String} authHeader - String containing Authorization header data
* @param {Object} headerParam - An map containing the extra header parames which needs to be added
* @param {Object} operationMetaData - contains SDK version
*
* @returns request options map
*/
function _getRequestOptions(params, operationConfig, operationMetaData ) {
var headerParam = utils.subMap(params, operationConfig.headerParams);
var uri, httpMethod;
var returnObj = {};
returnObj.headers = {};
returnObj["encoding"]= "utf8"
var contentType = "application/json; charset=utf-8";
if (operationMetaData.contentTypeOverride != null) {
contentType = operationMetaData.contentTypeOverride+"; charset=utf-8";
}
uri = _getURI(params, operationConfig, operationMetaData);
returnObj["uri"] = uri;
httpMethod = _getHttpMethod(operationConfig.action);
returnObj["method"] = httpMethod;
var body = null;
if (httpMethod !== "GET" && httpMethod !== "DELETE" && httpMethod !== "HEAD") {
if (_isEmpty(params) === false) {
var interceptor = _getCryptographyInterceptor(uri.href);
if (interceptor != null && typeof interceptor.encrypt === 'function') {
params = interceptor.encrypt(params);
}
body = JSON.stringify(params);
} else {
body = "";
}
returnObj["body"] = body;
returnObj.headers["Content-Type"] = contentType;
}
returnObj.headers["Accept"] = contentType;
returnObj.headers["User-Agent"] = constants.getCoreVersion()+"/" + operationMetaData.version
// arizzini: need to add the additional headers
for (var key in headerParam) {
returnObj.headers[key] = headerParam[key];
}
if (authentication && (typeof authentication.sign === 'function')) {
var authHeader = authentication.sign(uri, httpMethod, body)
returnObj.headers["Authorization"] = authHeader
}
//arizzini: addding the proxy info
if (proxy) {
returnObj["proxy"] = proxy;
}
return returnObj;
}
function _isEmpty(obj) {
return Object.keys(obj).length === 0;
}
/**
* Function to choose the appropriate HTTP method based on the API action.
*
* @private
* @param {String} action - The type of action being invoked on the domain object
* @return {String} Returns a HTTP request type e.g. 'POST'
*/
function _getHttpMethod(action) {
var httpMethod;
if (action === "create") {
httpMethod = "POST";
}
else if (action === "delete") {
httpMethod = "DELETE";
}
else if (action === "update") {
httpMethod = "PUT";
}
else if (action === "read" || action === "list" || action === "query") {
httpMethod = "GET";
}
else {
throw new mastercardError.SDKError("Unknown action [" + action + "]");
}
return httpMethod;
}
// Expose classes
MasterCardAPI.OAuth = oauth.OAuth;
MasterCardAPI.OAuthWithFileContent = oauth.OAuthWithFileContent;
MasterCardAPI.OperationConfig = operationConfig;
MasterCardAPI.OperationMetaData = operationMetaData;
MasterCardAPI.MasterCardError = mastercardError;
MasterCardAPI.Cryptography = {
MESCryptography: MESCryptography,
}
//arizzini: if you need to expose private function only during unit testing use this
if (typeof global.it === 'function') {
// START EXPOSE PRIVATE FUNCTION FOR TESTING
var port = process.env.JENKINS_PORT ? process.env.JENKINS_PORT : 8080;
MasterCardAPI.getUri = function(params, operationConfig, operationMetaData) {
return _getURI(params, operationConfig, operationMetaData);
};
MasterCardAPI.getRegisteredResourceConfigCount = function() {
return Object.keys(registeredInstances).length;
};
MasterCardAPI.clearResourceConfig = function() {
registeredInstances = {};
};
MasterCardAPI.reset = function() {
environment = constants.Environment.SANDBOX;
}
MasterCardAPI.getRequestOptions = function (params, operationConfig, operationMetaData) {
return _getRequestOptions(params, operationConfig, operationMetaData);
};
MasterCardAPI.testInit = function (opts) {
test = true;
sandbox = true;
initialized = true;
authentication = opts.authentication
};
MasterCardAPI.getProxy = function() {
return proxy;
};
// END EXPOSE PRIVATE FUNCTION FOR TESTING
}
// Export our object for use.
module.exports = MasterCardAPI;