Exposing redis Through Forest Admin
3 min readNov 23, 2020
We’re using Forest Admin on a project and had a need to also administer our redis instance. We’re using Azure redis but this should work on any redis provider.
First, install the redis NPM package
npm install redis
Second, add a service for wrapping the redis calls. I’ve added REDIS_CONNECTION_STRING as an environmental variable, formatted like rediss://<key>@<instance>.redis.cache.windows.net:6380.
const { promisify } = require("util");
const redis = require("redis");function getClient() {
const client = redis.createClient(process.env.REDIS_CONNECTION_STRING);
const delAsync = promisify(client.del).bind(client);
const getAsync = promisify(client.get).bind(client);
const flushDbAsync = promisify(client.flushdb).bind(client);
const scanAsync = promisify(client.scan).bind(client);
const ttlAsync = promisify(client.ttl).bind(client);client.on("error", console.error);return {
delAsync,
getAsync,
flushDbAsync,
scanAsync,
ttlAsync,
};
}const redisCacheService = {
deleteAsync: async (key) => getClient().delAsync(key),getAsync: async (key) => getClient().getAsync(key),flushDbAsync: async () => getClient().flushDbAsync(),scanAsync: async(offset, limit) => {
const res = await getClient().scanAsync(
offset,
// 'MATCH', 'q:*',
'COUNT', limit
);
const cursor = res[0];
const keys = res[1];return {
keys,
cursor,
};
},ttlAsync: async (key) => getClient().ttlAsync(key),
};module.exports = redisCacheService;
Third, create a Forest entry for the cache. Note that I’ve also created a custom action to flush the cache.
const Liana = require('forest-express-sequelize');Liana.collection('cache_entry', {
actions: [{
name: 'Flush',
type: 'global',
}],
fields: [{
field: 'id',
type: 'String',
}, {
field: 'value',
type: 'String',
}, {
field: 'ttl',
type: 'Integer',
}],
});
Last, create a Route for the cache
const express = require('express');
const { PermissionMiddlewareCreator } = require('forest-express-sequelize');
const redisCacheService = require("../services/redis-cache-service");
const JSONAPISerializer = require('jsonapi-serializer').Serializerconst router = express.Router();
const permissionMiddlewareCreator = new PermissionMiddlewareCreator('cache_entry');// Delete a User
router.delete('/cache_entry/:recordId', permissionMiddlewareCreator.delete(), async (request, response, next) => {
try {
await redisCacheService.deleteAsync(request.params.recordId);await response.status(204).send();} catch (e) {
next(e);
}
});// Get a list of keys from the redis cache
router.get('/cache_entry', permissionMiddlewareCreator.list(), async (request, response, next) => {
try {
const limit = parseInt(request.query.page.size, 10) || 20;
const offset = (parseInt(request.query.page.number, 10) - 1) * limit;
const res = await redisCacheService.scanAsync(offset, limit);
const { cursor, keys } = res;
const cacheEntryList = [];for (const key of keys) {
const value = await redisCacheService.getAsync(key);
const ttl = await redisCacheService.ttlAsync(key);
cacheEntryList.push({ id: key, value, ttl });
}const count = cursor === limit ? limit + 1 : cursor;
const cacheEntriesSerializer = new JSONAPISerializer('cache_entry', {
attributes: ['id', 'value'],
keyForAttribute: 'underscore_case',
nullIfMissing: true,
pluralizeType: false,
});
const cacheEntries = cacheEntriesSerializer.serialize(cacheEntryList);response.send({ ...cacheEntries, meta: { count }});} catch (e) {
next(e);
}
});// Get a redis cache entry
router.get('/cache_entry/:recordId', permissionMiddlewareCreator.details(), async (request, response, next) => {
try {
const value = await redisCacheService.getAsync(request.params.recordId);
const ttl = await redisCacheService.ttlAsync(request.params.recordId);
const cacheEntryItem = { id: request.params.recordId, value, ttl };
const cacheEntriesSerializer = new JSONAPISerializer('cache_entry', {
attributes: ['id', 'value', 'ttl'],
keyForAttribute: 'underscore_case',
nullIfMissing: true,
pluralizeType: false,
});
const cacheEntry = cacheEntriesSerializer.serialize(cacheEntryItem);
response.send({ ...cacheEntry });} catch (e) {
next(e);
}
});// Delete a list of redis cache entries
router.delete('/cache_entry', permissionMiddlewareCreator.delete(), async (request, response, next) => {
try {
for (const key of request.body.data.attributes.ids) {
await redisCacheService.deleteAsync(key);
}} catch (e) {
next(e);
}
});router.post('/actions/flush', permissionMiddlewareCreator.smartAction(), async (req, res) => {
try {
await redisCacheService.flushDbAsync();
res.send({ success: 'Cache Flushed' });} catch (e) {
console.error(e);
res.status(500).send('Cache Flush Failed');
}
});module.exports = router;