Exposing redis Through Forest Admin

Andrew Varnon
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').Serializer
const 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;

--

--

Andrew Varnon
Andrew Varnon

Written by Andrew Varnon

I am a full stack developer and architect, specializing in .Net and Azure.

Responses (1)