Source: datastores/rest.js

'use strict';
/**
 * @module lib/datastores/rest
 * @summary Whiteflag API rest datastore module
 * @description Module to use a database with a http rest interface
 * @tutorial modules
 */
module.exports = {
    // Rest datastore functions
    init: initRest,
    close: closeRest,
    storeMessage,
    getMessages,
    storeState,
    getState
};

// Node.js core and external modules //
const request = require('request');

// Whiteflag common functions and classes //
const log = require('../common/logger');
const { ignore } = require('../common/processing');

// Module constants //
const MESSAGECONTEXT = 'context=message';
const STATECONTEXT = 'context=state';
const HBCONTEXT = 'context=heartbeat';

// Module variables //
let _dbName = 'rest';
let _restURL = '';
let _restAuthHeader = '';

// MAIN MODULE FUNCTIONS //
/**
 * Initialises the database
 * @function initRest
 * @alias module:lib/datastores/rest.init
 * @param {Object} dbConfig datastore configuration parameters
 * @param {datastoreInitCb} callback function to be called after initialising the datastore
 */
function initRest(dbConfig, callback) {
    // Preserve name of the datastore
    _dbName = dbConfig.name;

    // Get rest URL parameters
    _restURL = getDatabaseURL(dbConfig);
    _restAuthHeader = getDatabaseAuthHeader(dbConfig);

    // Perform dummy call to assure config is correct
    log.trace(_dbName, `Checking http connection with ${_restURL}`);
    request(createRequestOptions(HBCONTEXT, null), function restCheckConnectionCb(err, response, body) {
        ignore(response);
        ignore(body);
        if (err) return callback(err, null);
        return callback(null, _dbName);
    });
}

/**
 * Closes the database
 * @function closeRest
 * @alias module:lib/datastores/rest.close
 * @param {datastoreCloseCb} callback function to be called after initialising the datastore
 */
function closeRest(callback) {
    log.trace(_dbName, 'Closing database connection');
    return callback();
}

/**
 * Stores a Whiteflag message in the database
 * @function storeMessage
 * @alias module:lib/datastores/rest.storeMessage
 * @param {wfMessage} wfMessage the whiteflag message to be stored
 * @param {datastoreStoreMessageCb} callback function to be called after storing the Whiteflag message
 */
function storeMessage(wfMessage, callback) {
    log.trace(_dbName, `Storing message: ${wfMessage.MetaHeader.transactionHash}`);

    let requestOptions = createRequestOptions(MESSAGECONTEXT, null);
    requestOptions.body = JSON.stringify(wfMessage);

    // Make http request to store message
    request.post(requestOptions, function restStoreMessageCb(err, response, body) {
        if (err) return callback(err, null);
        if (response.statusCode === 200) {
            return processResponseBody(body, callback);
        }
        return callback(new Error(JSON.stringify(response)));
    });
}

/**
 * Gets all Whiteflag messages from the database that match the query in an array
 * @function getMessages
 * @alias module:lib/datastores/rest.getMessages
 * @param {Object} wfQuery the properties of the messages to look up
 * @param {datastoreGetMessagesCb} callback function to be called after retrieving Whiteflag messages
 */
function getMessages(wfQuery, callback) {
    log.trace(_dbName, 'Performing message query: ' + JSON.stringify(wfQuery));

    // Make http request to retrieve messages
    request(createRequestOptions(MESSAGECONTEXT, wfQuery), function restCreateRequestCb(err, response, body) {
        if (err) return callback(err);
        if (response.statusCode === 200) {
            // return callback(null, JSON.parse(body));
            return processResponseBody(body, callback);
        }
        return callback(new Error(JSON.stringify(response)));
    });
}

/**
 * Stores Whiteflag state in the database
 * @function storeState
 * @alias module:lib/datastores/rest.storeState
 * @param {Object} stateObject state data enclosed in a storage / encryption container
 * @param {datastoreStoreStateCb} callback function to be called after storing the Whiteflag state
 */
function storeState(stateObject, callback) {
    log.trace(_dbName, 'Storing state');

    let requestOptions = createRequestOptions(STATECONTEXT, null);
    requestOptions.body = JSON.stringify(stateObject);

    // Make http request to store state
    request.post(requestOptions, function restUpsertStateCb(err, response, body) {
        if (err) return callback(err);
        if (response.statusCode === 200) {
            // return callback(null, JSON.parse(body));
            return processResponseBody(body, callback);
        }
        return callback(new Error(JSON.stringify(response)));
    });
}

/**
 * Gets Whiteflag state from the database
 * @function getState
 * @alias module:lib/datastores/.getState
 * @param {datastoreGetStateCb} callback function to be called after getting the Whiteflag state
 */
function getState(callback) {
    log.trace(_dbName, 'Retrieving state');

    // Make http request to retrieve messages
    request(createRequestOptions(STATECONTEXT, null), function restGetStateCb(err, response, body) {
        if (err) return callback(err);
        if (response.statusCode === 200) {
            return processResponseBody(body, callback);
        }
        return callback(new Error(JSON.stringify(response)));
    });
}

// PRIVATE MODULE FUNCTIONS //
/**
 * Gets URL for the REST datastore from the configuration
 * @private
 * @param {Object} dbConfig datastore configuration parameters
 * @returns {string} url
 */
function getDatabaseURL(dbConfig) {
    // Puts configuration parameters in a single url string
    const dbProtocol = (dbConfig.dbProtocol || 'http') + '://';
    const dbHost = dbConfig.dbHost || 'localhost';
    let dbPort = '';
    if (dbConfig.dbPort) dbPort = ':' + dbConfig.dbPort;
    const dbPath = dbConfig.dbPath || '';
    return (dbProtocol + dbHost + dbPort + dbPath);
}

/**
 * Gets http auth header for the REST datastore from the configuration
 * @private
 * @param {Object} dbConfig datastore configuration parameters
 * @returns {string} http auth header
 */
function getDatabaseAuthHeader(dbConfig) {
    // Creates basic http auth header
    if (dbConfig.username && dbConfig.password) {
        return ('Basic ' + Buffer.from(dbConfig.username + ':' + dbConfig.password).toString('base64'));
    }
    return '';
}

/**
 * Creates request options
 * @private
 * @param {string} context
 * @param {Object} parameters
 * @returns {Object} request options
 */
function createRequestOptions(context, parameters) {
    let requestURL = '';
    if (context) {
        requestURL = createContextURL(context, parameters);
    } else {
        requestURL = _restURL;
    }
    return {
        url : requestURL,
        headers : {
            'Authorization' : _restAuthHeader,
            'Content-type': 'application/json'
        }
    };
}

/**
 * Creates context URL
 * @private
 * @param {string} context
 * @param {Object} parameters
 * @returns {string} url with context
 */
function createContextURL(context, parameters) {
    let requestURL = _restURL + '?' + context;
    if (parameters && parameters['MetaHeader.transactionHash']) {
        requestURL += '&tx=' + parameters['MetaHeader.transactionHash'];
    } else {
        if (parameters && parameters.id) {
            requestURL += '&id=' + parameters.id;
        }
    }
    return requestURL;
}

/**
 * Creates context URL
 * @private
 * @param {Object} body request body
 * @param {function(Error, Object)} callback function to be called upon completion
 */
function processResponseBody(body, callback) {
    // Return null if empty response body
    if (body === '{}') return callback(null, null);

    // Parse response body
    let parsedBody = {};
    try {
        parsedBody = JSON.parse(body);
    } catch(err) {
        return callback(new Error(`Cannot parse data: ${err.message}: ${body}`));
    }
    return callback(null, parsedBody);
}