Source: operations/messages.js

'use strict';
/**
 * @module lib/operations/messages
 * @summary Whiteflag API messages endpoints handler module
 * @description Module with api messages endpoint handlers
 * @tutorial modules
 * @tutorial openapi
 */
module.exports = {
    getMessages,
    getMessage,
    sendMessage,
    receiveMessage,
    getReferences,
    getSequence
};

/* Common internal functions and classes */
const arr = require('../_common/arrays');
const { ProcessingError } = require('../_common/errors');
const { createBody,
        addRelatedResource,
        addMessageData,
        sendImperative,
        sendIndicative } = require('./_common/response');

/* Whiteflag modules */
const wfRetrieve = require('../protocol/retrieve');
const wfRxEvent = require('../protocol/events').rxEvent;
const wfTxEvent = require('../protocol/events').txEvent;

/* Module constants */
const R_MESSAGE = 'message';

/* MAIN MODULE FUNCTIONS */
/**
 * Retrieves all messages from the database
 * @function getMessages
 * @alias module:lib/operations/messages.getMessages
 * @param {Object} req the http request
 * @param {Object} res the http response
 * @param {string} operationId the operation id as defined in the openapi definition
 * @param {logEndpointEventCb} callback
 */
function getMessages(req, res, operationId, callback) {
    let wfQuery = {};
    let resBody = createBody(req, operationId, R_MESSAGE, req.query);

    // Put parameters in internal query
    let parameters = Object.keys(req.query);
    if (parameters.length !== 0) {
        parameters.forEach(parameter => {
            switch (parameter) {
                // Integers
                case 'blockNumber':
                case 'blockDepth': {
                    wfQuery[`MetaHeader.${parameter}`] = parseInt(req.query[parameter]);
                    break;
                }
                // Booleans
                case 'autoGenerated':
                case 'transmissionSuccess':
                case 'confirmed':
                case 'originatorValid':
                case 'referenceValid':
                case 'formatValid': {
                    wfQuery[`MetaHeader.${parameter}`] = (req.query[parameter] === 'true');
                    break;
                }
                // Message Header fields
                case 'Version':
                case 'EncryptionIndicator':
                case 'MessageCode':
                case 'DuressIndicator':
                case 'ReferenceIndicator':
                case 'ReferencedMessage': {
                    wfQuery[`MessageHeader.${parameter}`] = req.query[parameter];
                    break;
                }
                // Metaheader strings
                default: {
                    wfQuery[`MetaHeader.${parameter}`] = req.query[parameter];
                }
            }
        });
    }
    wfRetrieve.getQuery(wfQuery, function opsGetMessagesDbCb(err, wfMessages) {
        return returnMessages(err, res, resBody, wfMessages, callback);
    });
}

/**
 * Transmits messages to the blockchain through the tx event chain
 * @function sendMessage
 * @alias module:lib/operations/messages.sendMessage
 * @param {Object} req the http request
 * @param {Object} res the http response
 * @param {string} operationId the operation id as defined in the openapi definition
 * @param {logEndpointEventCb} callback
 */
function sendMessage(req, res, operationId, callback) {
    const wfMessage = req.body;
    if (wfMessage.MetaHeader?.autoGenerated) wfMessage.MetaHeader.autoGenerated = false;
    wfTxEvent.emit('messageCommitted', wfMessage, function opsSendMessageCb(err, wfMessage) {
        // Create response body and preserve information before responding
        let resBody = createBody(req, operationId, R_MESSAGE);
        if (!err) resBody.meta = addResourceData(wfMessage, resBody.meta.request.url, resBody.meta);

        // Send response using common endpoint response function
        return sendImperative(res, err, resBody, wfMessage, callback);
    });
}

/**
 * Gets message by transaction hash
 * @function getMessage
 * @alias module:lib/operations/messages.getMessage
 * @param {Object} req the http request
 * @param {Object} res the http response
 * @param {string} operationId the operation id as defined in the openapi definition
 * @param {logEndpointEventCb} callback
 */
function getMessage(req, res, operationId, callback) {
    const transactionHash = req.params.transactionHash;
    wfRetrieve.getMessage(transactionHash, null, function opsGetMessageCb(err, wfMessages = []) {
        // Create response body and process result
        let resBody = createBody(req, operationId, R_MESSAGE);
        let resData = wfMessages;
        if (!err) {
            if (wfMessages.length < 1) {
                err = new ProcessingError(`Message could not be found`, null, 'WF_API_NO_RESOURCE');
            } else if (wfMessages.length > 1) {
                err = new ProcessingError(`Multiple messages with the same transaction hash found ${transactionHash}`, null, 'WF_API_RESOURCE_CONFLICT');
            } else {
                resBody.meta = addResourceData(wfMessages[0], resBody.meta.request.url, resBody.meta);
                resData = wfMessages[0];
            }
        }
        // Add message info to meta data and send response using common endpoint response function
        return sendIndicative(res, err, resBody, resData, callback);
    });
}

/**
 * Receives messages by triggering the rx event chain
 * @function receiveMessage
 * @alias module:lib/operations/messages.receiveMessage
 * @param {Object} req the http request
 * @param {Object} res the http response
 * @param {string} operationId the operation id as defined in the openapi definition
 * @param {logEndpointEventCb} callback
 */
function receiveMessage(req, res, operationId, callback) {
    // Get information and create response body
    const transactionHash = req.params.transactionHash;
    let resBody = createBody(req, operationId, R_MESSAGE);

    // Do not complain if plain metaheader provided
    let wfMessage = {};
    if (Object.hasOwn(req.body, 'MetaHeader')) {
        wfMessage = req.body;
    } else if (Object.hasOwn(req.body, 'MessageHeader') || Object.hasOwn(req.body, 'MessageBody')) {
        wfMessage = req.body;
        wfMessage.MetaHeader = {};
    } else {
        wfMessage.MetaHeader = req.body;
    }
    // Check transaction hash
    if (wfMessage.MetaHeader.transactionHash) {
        if (wfMessage.MetaHeader.transactionHash !== transactionHash) {
            const err = new ProcessingError(`Provided message data has a different transaction hash in its metaheader: ${wfMessage.MetaHeader.transactionHash}`, null, 'WF_API_BAD_REQUEST');
            return sendImperative(res, err, resBody, null, callback);
        }
    } else {
        wfMessage.MetaHeader.transactionHash = transactionHash;
    }
    wfRxEvent.emit('messageReceived', wfMessage, function opsReceiveMessageCb(err, wfMessage) {
        // Add message info to meta data and send response using common endpoint response function
        if (!err) resBody.meta = addResourceData(wfMessage, resBody.meta.request.url, resBody.meta);
        return sendImperative(res, err, resBody, wfMessage, callback);
    });
}

/**
 * Retrieves message references from the database
 * @function getReferences
 * @alias module:lib/operations/messages.getReferences
 * @param {Object} req the http request
 * @param {Object} res the http response
 * @param {string} operationId the operation id as defined in the openapi definition
 * @param {logEndpointEventCb} callback
 */
function getReferences(req, res, operationId, callback) {
    const query = req.query || {};
    let resBody = createBody(req, operationId, R_MESSAGE, query);

    // Retrieve references if transaction hash is specified as query parameter
    if (query.transactionHash && query.blockchain) {
        return wfRetrieve.getReferences(query.transactionHash, query.blockchain,
            function opsGetReferencesBcCb(err, wfMessages) {
                if (!err) resBody.meta.info = arr.addItem(resBody.meta.info, `Known Whiteflag messages on ${query.blockchain} referencing message ${query.transactionHash}`);
                returnMessages(err, res, resBody, wfMessages, callback);
            }
        );
    }
    if (query.transactionHash) {
        return wfRetrieve.getReferences(query.transactionHash, null,
            function opsGetReferencesCb(err, wfMessages) {
                if (!err) resBody.meta.info = arr.addItem(resBody.meta.info, `Known Whiteflag messages referencing message ${query.transactionHash}`);
                returnMessages(err, res, resBody, wfMessages, callback);
            }
        );
    }
    // Cannot retrieve message references without transaction hash
    let err = new ProcessingError('Query does not contain transactionHash of referencing message', null, 'WF_API_BAD_REQUEST');
    return returnMessages(err, res, resBody, null, callback);
}

/**
 * Retrieves message sequences from the database
 * @function getSequence
 * @alias module:lib/operations/messages.getSequence
 * @param {Object} req the http request
 * @param {Object} res the http response
 * @param {string} operationId the operation id as defined in the openapi definition
 * @param {logEndpointEventCb} callback
 */
function getSequence(req, res, operationId, callback) {
    const query = req.query || {};
    let resBody = createBody(req, operationId, R_MESSAGE, query);

    // Retrieve sequence if transaction hash is specified as query parameter
    if (query.transactionHash && query.blockchain) {
        return wfRetrieve.getSequence(query.transactionHash, query.blockchain,
            function opsGetSequenceBcCb(err, wfMessages) {
                if (!err) resBody.meta.info = arr.addItem(resBody.meta.info, `Known Whiteflag message sequence on ${query.blockchain} starting with message ${query.transactionHash}`);
                returnMessages(err, res, resBody, wfMessages, callback);
            }
        );
    }
    if (query.transactionHash) {
        return wfRetrieve.getSequence(query.transactionHash, null,
            function opsGetSequenceCb(err, wfMessages) {
                if (!err) resBody.meta.info = arr.addItem(resBody.meta.info, `Known Whiteflag message sequence starting with message ${query.transactionHash}`);
                returnMessages(err, res, resBody, wfMessages, callback);
            }
        );
    }
    // Cannot retrieve message sequence without trabsaction hash
    let err = new ProcessingError('Query does not contain transactionHash of first message in sequence', null, 'WF_API_BAD_REQUEST');
    return returnMessages(err, res, resBody, null, callback);
}

/* PRIVATE MODULE FUNCTIONS */
/**
 * Returns messages from queries on resource endpoints
 * @private
 * @param {Object} req the http request
 * @param {Object} res the http response
 * @param {Object} resBody the response body
 * @param {Array} wfMessages Whiteflag messages
 * @param {logEndpointEventCb} callback
 */
function returnMessages(err, res, resBody, wfMessages, callback) {
    let resData = [];
    if (!err) {
        // Ensure message are in an array
        if (Array.isArray(wfMessages)) resData = wfMessages;
            else resData = [ wfMessages ];

        // Check query for related resources
        if (resBody.meta?.query) {
            let blockchain = null;
            let originator = null;
            let parameters = Object.keys(resBody.meta.query);
            parameters.forEach(parameter => {
                if (parameter === 'blockchain') blockchain = resBody.meta.query[parameter]
                if (parameter === 'originatorAddress') originator = resBody.meta.query[parameter]
            });
            resBody.meta = addRelatedResource(blockchain, null, originator, resBody.meta);
        }
    }
    // Send response using common endpoint response function
    return sendIndicative(res, err, resBody, resData, callback);
}

/**
 * Adds information of a single Whiteflag message to meta data
 * @param {wfMessage} wfMessage
 * @param {string} reqPath
 * @param {Object} [resMeta] the meta data object to add message details to 
 * @returns {Object} the updated meta data object
 */
function addResourceData(wfMessage, reqPath, resMeta = {}) {
    // Add resource data
    if (!Object.hasOwn(resMeta, 'resource')) {
        resMeta.resource = { type: R_MESSAGE };
    }
    if (wfMessage.MetaHeader?.transactionHash) {
        resMeta.resource.id = wfMessage.MetaHeader.transactionHash;
        if (reqPath) {
            if (!reqPath.includes(wfMessage.MetaHeader.transactionHash)) {
                reqPath += `/${wfMessage.MetaHeader.transactionHash}`
            }
            resMeta.resource.location = reqPath;
        }
    }
    resMeta = addMessageData(wfMessage, resMeta);

    // Add related resource data
    let blockchain = wfMessage.MetaHeader?.blockchain || null;
    let account = null;
    let originator = null;
    if (wfMessage.MetaHeader?.transceiveDirection === 'TX') {
        account = wfMessage.MetaHeader?.originatorAddress || null;
    } else {
        originator = wfMessage.MetaHeader?.originatorAddress || null;
    }
    resMeta = addRelatedResource(blockchain, account, originator, resMeta);

    // Return result
    return resMeta;
}