Source: datastores/embeddb.js

'use strict';
/**
 * @module lib/datastores/embeddb
 * @summary Whiteflag API embedded datastore module
 * @description Module to use an embedded datastore
 */
module.exports = {
    // Database functions
    init: initDatastore,
    close: closeDatastore,
    storeMessage,
    getMessages,
    storeState,
    getState
};

// Node.js core and external modules //
const fs = require('fs');
const SimpleDB = require('simpl.db');

// Whiteflag common functions and classes //
// eslint-disable-next-line no-unused-vars
const log = require('../common/logger');
const { ignore } = require('../common/processing');

// Module variables //
let _db;
let _dbName = 'embedded-db';
let _dbMessagesCollection;
let _dbStateCollection;

// Module constants //
const MESSAGECOLLECTION = 'wfMessages';
const STATECOLLECTION = 'wfState';
const DATAFILE = 'wfDatastore';

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

    // Check folder and file access
    if (!fs.existsSync(dbConfig.directory)) {
        try {
            fs.mkdirSync(dbConfig.directory, { recursive: true });
        } catch(err) {
            log.error(_dbName, `Error creating collections folder ${dbConfig.directory}: ${err.message}`);
            return callback(err, null);
        }
    }
    fs.access(dbConfig.directory, fs.constants.W_OK, err => {
        if (err) {
            log.error(_dbName, `Error writing to collections folder ${dbConfig.directory}: ${err.message}`);
            return callback(err, null);
        } else {
            // Database configuration
            const dbDataFile = dbConfig.directory + '/' + DATAFILE + '.json';
            const dbOptions = {
                autoSave: false,
                collectionTimestamps: true,
                collectionsFolder: dbConfig.directory,
                dataFile: dbDataFile
            };
            // Connect to datastore and preserve connector
            _db = new SimpleDB(dbOptions);
            log.trace(_dbName, `Openend embedded SimpleDB database version ${_db.version}`);

            // Collection for Whiteflag protocol state
            _dbStateCollection = _db.createCollection(STATECOLLECTION);
            _dbStateCollection.save();

            // Collection for Whiteflag messages
            _dbMessagesCollection = _db.createCollection(MESSAGECOLLECTION);
            _dbMessagesCollection.save();

            // All done
            log.info(_dbName, 'Collections in database: ' + JSON.stringify(_db.collections));
            _db.save();
            return callback(null, _dbName, _db);
        }
    });
}

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

/**
 * Stores a Whiteflag message in the database
 * @function storeMessage
 * @alias module:lib/datastores/embeddb.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, `Upserting message: ${wfMessage.MetaHeader.transactionHash}`);
    if (!_dbMessagesCollection) return callback(new Error(`Message collection in ${_dbName} not initialised`));

    // Insert if new else update message
    let result;
    if (_dbMessagesCollection.has(item => item.MetaHeader.transactionHash === wfMessage.MetaHeader.transactionHash)) {
        result = _dbMessagesCollection.update(
            message => {
                message.MetaHeader = wfMessage.MetaHeader;
                message.MessageHeader = wfMessage.MessageHeader;
                message.MessageBody = wfMessage.MessageBody;
            },
            item => item.MetaHeader.transactionHash === wfMessage.MetaHeader.transactionHash
        );
    } else {
        result = _dbMessagesCollection.create(wfMessage);
    }
    _dbMessagesCollection.save();
    return callback(null, result);
}

/**
 * Gets all Whiteflag messages from the database that match the query in an array
 * @function getMessages
 * @alias module:lib/datastores/embeddb.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) {
    let count = 0;
    let wfMessages = [];

    // Get an array of result with all messages that match all key values in query object
    log.trace(_dbName, 'Performing message query: ' + JSON.stringify(wfQuery));
    if (JSON.stringify(wfQuery) === '{}') {
        wfMessages = _dbMessagesCollection.getAll();
        count = wfMessages.length;
    } else {
        wfMessages = _dbMessagesCollection.getMany(item => {
            let match = false;
            for (const [key, value] of Object.entries(wfQuery)) {
                const property = key.split('.').pop();
                if (property in item.MetaHeader) {
                    if (item.MetaHeader[property] === value) match = true;
                    else return false;
                }
            }
            if (match) {
                count += 1;
                return true;
            }
            return false;
        });
    }
    log.trace(_dbName, `Found ${count} messages for query: ${JSON.stringify(wfQuery)}`);
    return callback(null, wfMessages, count);
}

/**
 * Stores Whiteflag state in the database
 * @function storeState
 * @alias module:lib/datastores/embeddb.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 in collection ${STATECOLLECTION}`);
    if (!_dbStateCollection) return callback(new Error(`State collection in ${_dbName} not initialised`));
    _dbStateCollection.remove();
    const result = _dbStateCollection.create(stateObject);
    _dbStateCollection.save();
    return callback(null, result);
}

/**
 * Gets Whiteflag state from the database
 * @function getState
 * @alias module:lib/datastores/embeddb.getState
 * @param {datastoreGetStateCb} callback function to be called after getting the Whiteflag state
 */
function getState(callback) {
    log.trace(_dbName, `Retrieving state from collection ${STATECOLLECTION}`);
    if (!_dbStateCollection) return callback(new Error(`State collection in ${_dbName} not initialised`));
    const stateObject = _dbStateCollection.fetch(
        state => {
            ignore(state); return true;
        }
    );
    return callback(null, stateObject);
}