/* global process*/
/**
* @module cache-model
*/
const utils = require( '../lib/utils' );
const transformer = require( 'enketo-transformer' );
const prefix = 'ca:';
const expiry = 30 * 24 * 60 * 60;
const config = require( './config-model' ).server;
const client = require( 'redis' ).createClient( config.redis.cache.port, config.redis.cache.host, {
auth_pass: config.redis.cache.password
} );
const debug = require( 'debug' )( 'cache-model' );
// in test environment, switch to different db
if ( process.env.NODE_ENV === 'test' ) {
client.select( 15 );
}
/**
* Gets an item from the cache.
*
* @static
* @name get
* @function
* @param {module:survey-model~SurveyObject} survey - survey object
* @return {Promise<Error|null|module:survey-model~SurveyObject>} Promise that resolves with cached {@link module:survey-model~SurveyObject|SurveyObject} or `null`
*/
function getSurvey( survey ) {
return new Promise( ( resolve, reject ) => {
if ( !survey || !survey.openRosaServer || !survey.openRosaId ) {
const error = new Error( 'Bad Request. Survey information to perform cache lookup is not complete.' );
error.status = 400;
reject( error );
} else {
const key = _getKey( survey );
client.hgetall( key, ( error, cacheObj ) => {
if ( error ) {
reject( error );
} else if ( !cacheObj ) {
resolve( null );
} else {
// form is 'actively used' so we're resetting the cache expiry
debug( 'cache is up to date and used, resetting expiry' );
client.expire( key, expiry );
survey.form = cacheObj.form;
survey.model = cacheObj.model;
survey.formHash = cacheObj.formHash;
survey.xslHash = cacheObj.xslHash;
survey.languageMap = JSON.parse( cacheObj.languageMap || '{}' );
resolve( survey );
}
} );
}
} );
}
/**
* Gets the hashes of an item from the cache.
*
* @static
* @name getHashes
* @function
* @param {module:survey-model~SurveyObject} survey - survey object
* @return {Promise<Error|module:survey-model~SurveyObject>} Promise that resolves with {@link module:survey-model~SurveyObject|SurveyObject} (updated with hash array if such exist)
*/
function getSurveyHashes( survey ) {
return new Promise( ( resolve, reject ) => {
if ( !survey || !survey.openRosaServer || !survey.openRosaId ) {
const error = new Error( 'Bad Request. Survey information to perform cache lookup is not complete.' );
error.status = 400;
reject( error );
} else {
const key = _getKey( survey );
client.hmget( key, [ 'formHash', 'xslHash' ], ( error, hashArr ) => {
if ( error ) {
reject( error );
} else if ( !hashArr || !hashArr[ 0 ] || !hashArr[ 1 ] ) {
resolve( survey );
} else {
survey.formHash = hashArr[ 0 ];
survey.xslHash = hashArr[ 1 ];
resolve( survey );
}
} );
}
} );
}
/**
* Checks if cache is present and up to date
*
* @static
* @name check
* @function
* @param {module:survey-model~SurveyObject} survey - survey object
* @return {Promise<Error|null|boolean>} a Promise that resolves with a boolean
*/
function isCacheUpToDate( survey ) {
return new Promise( ( resolve, reject ) => {
if ( !survey || !survey.openRosaServer || !survey.openRosaId || !survey.info.hash ) {
const error = new Error( 'Bad Request. Survey information to perform cache check is not complete.' );
error.status = 400;
reject( error );
} else {
// clean up the survey object to make sure no artefacts of cached survey are present
survey = {
openRosaServer: survey.openRosaServer,
openRosaId: survey.openRosaId,
info: {
hash: survey.info.hash
},
manifest: survey.manifest
};
const key = _getKey( survey );
client.hgetall( key, ( error, cacheObj ) => {
if ( error ) {
reject( error );
} else if ( !cacheObj ) {
debug( 'cache is missing' );
resolve( null );
} else {
// Adding the hashes to the referenced survey object can be efficient, since this object
// is passed around. The hashes may therefore already have been calculated
// when setting the cache later on.
// Note that this server cache only cares about media URLs, not media content.
// This allows the same cache to be used for a form for the OpenRosa server serves different media content,
// e.g. based on the user credentials.
_addHashes( survey );
if ( cacheObj.formHash !== survey.formHash || cacheObj.xslHash !== survey.xslHash || cacheObj.mediaUrlHash ) {
debug( 'cache is obsolete' );
resolve( false );
} else {
debug( 'cache is up to date' );
resolve( true );
}
}
} );
}
} );
}
/**
* Adds an item to the cache
*
* @static
*
* @name set
* @function
* @param {module:survey-model~SurveyObject} survey - survey object
* @return {Promise<Error|module:survey-model~SurveyObject>} a Promise that resolves with the survey object
*/
function setSurvey( survey ) {
return new Promise( ( resolve, reject ) => {
if ( !survey || !survey.openRosaServer || !survey.openRosaId || !survey.info.hash || !survey.form || !survey.model ) {
const error = new Error( 'Bad Request. Survey information to cache is not complete.' );
error.status = 400;
reject( error );
} else {
_addHashes( survey );
const obj = {
formHash: survey.formHash,
xslHash: survey.xslHash,
form: survey.form,
model: survey.model,
// The mediaUrlHash property is an artefact and no longer used.
// When hmset updates the database it would keep it in place, so we explicitly set it to empty.s
mediaUrlHash: '',
languageMap: JSON.stringify( survey.languageMap || {} )
};
const key = _getKey( survey );
client.hmset( key, obj, error => {
if ( error ) {
reject( error );
} else {
debug( 'cache has been updated' );
// expire in 30 days
client.expire( key, expiry );
resolve( survey );
}
} );
}
} );
}
/**
* Flushes the cache of a single survey
*
* @static
* @name flush
* @function
* @param {module:survey-model~SurveyObject} survey - survey object
* @return {Promise<Error|module:survey-model~SurveyObject>} Flushed {@link module:survey-model~SurveyObject|SurveyObject}
*/
function flushSurvey( survey ) {
return new Promise( ( resolve, reject ) => {
if ( !survey || !survey.openRosaServer || !survey.openRosaId ) {
const error = new Error( 'Bad Request. Survey information to cache is not complete.' );
error.status = 400;
reject( error );
} else {
const key = _getKey( survey );
client.hgetall( key, ( error, cacheObj ) => {
if ( error ) {
reject( error );
} else if ( !cacheObj ) {
error = new Error( 'Survey cache not found.' );
error.status = 404;
reject( error );
} else {
client.del( key, error => {
if ( error ) {
reject( error );
} else {
delete survey.form;
delete survey.model;
delete survey.formHash;
delete survey.xslHash;
delete survey.mediaHash;
delete survey.mediaUrlHash;
delete survey.languageMap;
resolve( survey );
}
} );
}
} );
}
} );
}
/**
* Completely empties the cache
*
* @static
* @return {Promise<Error|boolean>} Promise that resolves `true` after all cache is flushed
*/
function flushAll() {
return new Promise( ( resolve, reject ) => {
// TODO: "Don't use KEYS in your regular application code"
// (https://redis.io/commands/keys)
client.keys( `${prefix}*`, ( error, keys ) => {
if ( error ) {
reject( error );
}
keys.forEach( key => {
client.del( key, error => {
if ( error ) {
console.error( error );
}
} );
} );
// TODO: use Promise.All to resolve when all deletes have completed.
resolve( true );
} );
} );
}
/**
* Gets the key used for the cache item
*
* @param {module:survey-model~SurveyObject} survey - survey object
* @return {string|null} openRosaKey or `null`
*
*/
function _getKey( survey ) {
const openRosaKey = utils.getOpenRosaKey( survey, prefix );
return ( openRosaKey ) ? openRosaKey : null;
}
/**
* Adds the 3 relevant hashes to the survey object if they haven't been added already.
*
* @param {module:survey-model~SurveyObject} survey - survey object
*
*/
function _addHashes( survey ) {
survey.formHash = survey.formHash || survey.info.hash;
survey.xslHash = survey.xslHash || transformer.version;
}
module.exports = {
get: getSurvey,
getHashes: getSurveyHashes,
set: setSurvey,
check: isCacheUpToDate,
flush: flushSurvey,
flushAll
};