nsql-cache
Version:
Advanced cache layer for NoSQL databases
219 lines (186 loc) • 7.94 kB
JavaScript
;
const arrify = require('arrify');
const utils = require('./utils');
const { is } = utils;
const { getTTL } = utils.ttl;
/**
* gstore-node error code when entity is not found.
*/
const ERR_ENTITY_NOT_FOUND = 'ERR_ENTITY_NOT_FOUND';
module.exports = cache => {
const addCachePrefixKeys = key => cache.config.cachePrefix.keys + key;
const keyToString = key => addCachePrefixKeys(cache.db.keyToString(key));
/**
* Order a list of entities according to a list of keys.
* As some NoSQL database might not maintain the order of the keys passed
* in their response, this will garantee the order to save them in the cache
*/
const orderEntities = (entities, keys) => {
const entitiesByKey = {};
entities.forEach(entity => {
if (typeof entity === 'undefined') {
return;
}
entitiesByKey[keyToString(cache.db.getKeyFromEntity(entity))] = entity;
});
return keys.map(key => entitiesByKey[keyToString(key)] || undefined);
};
// Attach Key to cache result
const addKeyToEntity = (entities, keys) => {
if (Array.isArray(entities)) {
const _keys = arrify(keys);
return entities.map((entity, i) => cache.db.addKeyToEntity(_keys[i], entity));
}
return cache.db.addKeyToEntity(keys, entities);
};
const read = (keys, ...args) => {
let fetchHandler = args.length > 1 ? args[1] : args[0];
if (typeof fetchHandler !== 'function') {
/**
* If no fetchHandler is passed, defaults to the cache db getEntity() method
* unless we already wrapped the datastore.get and we have an _originalGet function attached
*/
fetchHandler = _keys => {
if (cache.db.getEntityUnWrapped) {
return cache.db.getEntityUnWrapped(_keys);
}
return cache.db.getEntity(_keys);
};
}
const options = is.object(args[0]) ? args[0] : {};
options.ttl = getTTL(cache, options, 'keys');
const isMultiple = Array.isArray(keys) && keys.length > 1;
/**
* Convert the keys to unique string id
*/
const stringKeys = isMultiple ? keys.map(keyToString) : keyToString(keys);
const _args = [...stringKeys, options];
if (isMultiple) {
return cache.cacheManager.mget(..._args).then(onResult);
}
return cache.cacheManager
.get(stringKeys, options)
.then(res => {
/**
* If the db adapter has a "postGetEntity" method to format
* the entities before returning them, we call that method
*/
if (cache.db.postGetEntity) {
return cache.db.postGetEntity(res);
}
return res;
})
.then(onResult);
function onResult(_cacheResult) {
const entitiesWithKeys = addKeyToEntity(arrify(_cacheResult), keys);
const cacheResult = entitiesWithKeys.filter(r => r !== undefined && r !== null);
const isEmptyResult = cacheResult.length === 0;
if (isEmptyResult) {
/**
* No cache we need to fetch the keys
*/
return fetchHandler(keys).then(_fetchResult => {
// We make sure the order of the entities returned by the fetchHandler
// is the same as the order of the keys provided.
const fetchResult = isMultiple ? orderEntities(_fetchResult, keys) : _fetchResult;
// Prime the cache
return cache.primeCache(stringKeys, fetchResult, options).then(() => {
/**
* If the db adapter has a "postGetEntity" method to format
* the entities before returning them, we call that method
*/
if (cache.db.postGetEntity) {
return cache.db.postGetEntity(fetchResult);
}
return fetchResult;
});
});
}
if (isMultiple && cacheResult.length !== keys.length) {
/**
* The cache returned some entities but not all of them
*/
const cached = {};
let strKey;
const addToCache = entity => {
if (!entity) {
return;
}
strKey = keyToString(cache.db.getKeyFromEntity(entity));
cached[strKey] = entity;
};
cacheResult.forEach(addToCache);
const keysNotFound = keys.filter(k => cached[keyToString(k)] === undefined);
return fetchHandler(keysNotFound)
.then(_fetchResult => {
// Make sure we the fetchResult is in the same order as the keys that we fetched
const fetchResult = orderEntities(_fetchResult, keysNotFound);
fetchResult.forEach(addToCache);
/**
* Prime the cache
*/
return cache.primeCache(keysNotFound.map(keyToString), fetchResult, options);
})
.catch(error => {
if (error.code === ERR_ENTITY_NOT_FOUND) {
// When we fetch *one* key and it is not found
// gstore.Model returns an error with 404 code.
strKey = keyToString(keysNotFound[0]);
cached[strKey] = null;
return;
}
throw new Error(error);
})
.then(() =>
// Map the keys to our cached map
// return "null" if no result
stringKeys.map(k => cached[k] || null)
);
}
return addKeyToEntity(_cacheResult, keys);
}
};
const mget = (..._keys) => {
const keys = _keys.map(k => keyToString(k));
if (keys.length === 1) {
return cache.get(keys[0]).then(_entity => {
if (typeof _entity === 'undefined') {
return _entity;
}
return addKeyToEntity([_entity], _keys)[0];
});
}
return cache.mget(...keys).then(entities => addKeyToEntity(entities, _keys));
};
const get = mget;
const mset = (..._keysValues) => {
let options = _keysValues.length % 2 > 0 ? _keysValues.pop() : {};
options = { ttl: getTTL(cache, options, 'keys') };
// Convert Datastore Keys to unique string id
const keysValues = _keysValues.map((kv, i) => {
if (i % 2 === 0) {
return addCachePrefixKeys(cache.db.keyToString(kv));
}
return kv;
});
const multi = keysValues.length > 2;
if (multi) {
return cache.mset(...keysValues, options).then(() => {
// The reponse is the odd index from the keysValues
const response = keysValues.filter((v, i) => i % 2 > 0);
return response;
});
}
return cache.set(keysValues[0], keysValues[1], options).then(() => keysValues[1]);
};
const set = mset;
const del = (...keys) => cache.del(keys.map(k => keyToString(k)));
return {
read,
get,
mget,
set,
mset,
del,
};
};