app/models/submission-model.js

/* global process, __dirname */

/**
 * @module submission-model
 */

const config = require( './config-model' ).server;
const client = require( 'redis' ).createClient( config.redis.main.port, config.redis.main.host, {
    auth_pass: config.redis.main.password
} );
const path = require( 'path' );
//var debug = require( 'debug' )( 'submission-model' );
let logger;

/**
 * Use a cron job and logrotate service, e.g.:
 * /usr/sbin/logrotate /home/enketo/logrotate.conf -s /home/enketo/enketo-express/logs/logrotate
 *
 * Example analyses of log files for form with enketo ID "YYYd":
 *
 * zgrep --no-filename "      YYYd    " submissions*.* | sort > YYYd-submissions.log
 * (you may need to enter CTRL-V to enter the literal TAB character),
 *
 * or (might be slower):
 * zgrep --no-filename -P "\tYYYd\t" submissions*.* > YYYp-submissions.log
 */

// only instantiate logger if required
if ( config.log.submissions ) {
    logger = require( 'bristol' );

    // for ephemeral file systems (e.g. Heroku) also use write a log to the console
    logger
        .addTarget( 'console' )
        .withFormatter( 'syslog' );

    // for non-ephemeral single-server installations, write to a dedicated easy-to-process submission log file
    logger
        .addTarget( 'file', {
            file: path.resolve( __dirname, '../../logs/submissions.log' )
        } )
        .withFormatter( _formatter );
}

// in test environment, switch to different db
if ( process.env.NODE_ENV === 'test' ) {
    client.select( 15 );
}


/**
 * Whether instanceID was submitted successfully before.
 *
 * To prevent large submissions that were divided into multiple batches from recording multiple times,
 * we use a redis capped list to store the latest 100 instanceIDs
 * This list can be queried to avoid double-counting instanceIDs
 *
 * Note that edited records are submitted multiple times with different instanceIDs.
 *
 * @static
 * @param { string } id - Enketo ID of survey
 * @param { string } instanceId - instance ID of record
 * @return {Promise<Error|boolean>} a Promis that resolves with a boolean
 */
function isNew( id, instanceId ) {
    if ( !id || !instanceId ) {
        const error = new Error( 'Cannot log instanceID: either enketo ID or instance ID not provided', id, instanceId );
        error.status = 400;

        return Promise.reject( error );
    }

    const key = `su:${id.trim()}`;

    return _getLatestSubmissionIds( key )
        .then( latest => _alreadyRecorded( instanceId, latest ) )
        .then( alreadyRecorded => {
            if ( !alreadyRecorded ) {
                client.lpush( key, instanceId, error => {
                    if ( error ) {
                        console.error( `Error pushing instanceID into: ${key}` );
                    } else {
                        // only store last 100 IDs
                        client.ltrim( key, 0, 99, error => {
                            if ( error ) {
                                console.error( `Error trimming: ${key}` );
                            }
                        } );
                    }
                } );

                return true;
            }

            return false;
        } );
}

/**
 * @static
 * @param { string } id - Enketo ID of survey
 * @param { string } instanceId - instance ID of record
 * @param { string } deprecatedId - deprecated ID of record
 */
function add( id, instanceId, deprecatedId ) {
    if ( logger ) {
        logger.info( instanceId, {
            enketoId: id,
            deprecatedId,
            submissionSuccess: true
        } );
    }
}

/**
 * @param { string } instanceId - instance ID of record
 * @param {Array<string>} [list] - List of IDs
 * @return { boolean } Whether instanceID already exists in the list
 */
function _alreadyRecorded( instanceId, list = [] ) {
    return list.indexOf( instanceId ) !== -1;
}

/**
 * @param { string } key - database key
 * @return { Promise } a Promise that resolves with a redis list of submission IDs
 */
function _getLatestSubmissionIds( key ) {
    return new Promise( ( resolve, reject ) => {
        client.lrange( key, 0, -1, ( error, res ) => {
            if ( error ) {
                reject( error );
            } else {
                resolve( res );
            }
        } );
    } );
}

/**
 * Formatter function for logger
 *
 * @param { object } options - logger formatting options
 * @param { object } severity - level of log message
 * @param { object } date - date
 * @param {Array<object>} elems - object to log
 */
function _formatter( options, severity, date, elems ) {
    let instanceId = '-';
    let enketoId = '-';
    let deprecatedId = '-';

    if ( Array.isArray( elems ) ) {
        instanceId = elems[ 0 ];
        if ( elems[ 1 ] && typeof elems[ 1 ] === 'object' ) {
            enketoId = elems[ 1 ].enketoId || enketoId;
            deprecatedId = elems[ 1 ].deprecatedId || deprecatedId;
        }
    }

    return [ date.toISOString(), enketoId, instanceId, deprecatedId ].join( '\t' );
}

module.exports = {
    isNew,
    add
};