Skip to content

Instantly share code, notes, and snippets.

@noweh
Last active February 1, 2022 13:32
Show Gist options
  • Save noweh/c6894ead8f6cf5da8e35b38308f79a1f to your computer and use it in GitHub Desktop.
Save noweh/c6894ead8f6cf5da8e35b38308f79a1f to your computer and use it in GitHub Desktop.
Node.js and Sequelize: Redis-client and AbstractRepository files for database requests
const config = require('../config')
const RedisClient = require('./redis-client.js')
const sequelizeCache = require('./sequelize-cache')
class AbstractRepository {
/**
* Constructor, allows to use model directly in repository methods
* @param {Object} model
*/
constructor (model) {
if (this.constructor === AbstractRepository) {
throw new TypeError('Abstract class "AbstractRepository" cannot be instantiated directly')
}
if (model === undefined) {
throw new TypeError('Model cannot be empty')
}
this.model = model
}
/**
* Override Sequelize.model.findOne to add cache and logs
* @param {Object} options - existing options of Sequelize.model.findOne
* @param {Boolean} noCache
* @param {RedisClient} redisClient - sent if the connection should persist
* @returns {Promise<Object>}
*/
async findOne (options, noCache = false, redisClient = null) {
let item = ''
if (config.privateRuntimeConfig.cache.driver === 'redis' && !noCache) {
item = await sequelizeCache.request(this.model.name, 'findOne', (opts) => this.model.findOne(opts), options, this.getRedisClient(redisClient))
if (!redisClient) {
// Close RedisClient if not a bulkCall
await this.closeRedisClient()
}
} else {
try {
item = await this.model.findOne(options)
console.log(['FindOne request', { cacheHit: 'noCache', options, item }])
} catch (error) {
throw new TypeError('Error with findOne function')
}
}
return item
}
/**
* Override Sequelize.model.findAll to add cache and logs
* @param {Object} options : existing options of Sequelize.model.findAll
* @param {Boolean} noCache
* @param {RedisClient} redisClient - sent if the connection should persist
* @returns {Promise<Object>}
*/
async findAll (options = null, noCache = false, redisClient = null) {
let items = []
if (config.privateRuntimeConfig.cache.driver === 'redis' && !noCache) {
items = await sequelizeCache.request(this.model.name, 'findAll', (opts) => this.model.findAll(opts), options, this.getRedisClient(redisClient))
if (!redisClient) {
await this.closeRedisClient()
}
} else {
try {
items = await this.model.findAll(options)
console.log(['FindAll request', { cacheHit: 'noCache', options, items }])
} catch (error) {
throw new TypeError('Error with findAll function')
}
}
return items
}
/**
* Override Sequelize.model.create to add logs
* @param {Object} data
* @param {RedisClient} redisClient - sent if the connection should persist
* @param {Boolean} autoClearCache
* @returns {Promise<Object>}
*/
async create (data, redisClient = null, autoClearCache = true) {
try {
// Create function return item
const item = await this.model.create(data)
if (autoClearCache) {
// Clear redis cache for all data in this model when update
if (config.privateRuntimeConfig.cache.driver === 'redis') {
try {
wait this.getRedisClient(redisClient).client.flushAll()
console.log('Flush All Cache After Create')
} catch (error) {
console.log('Error when Flushing All Cache After Create')
}
if (!redisClient) {
await this.closeRedisClient()
}
}
}
console.log(['Create request', { item, data }])
return item
} catch (error) {
throw new TypeError('Error with create function')
}
}
/**
* Override sequelize.model.update to add logs
* @param {Object} data
* @param {Object} options
* @param {RedisClient} redisClient - sent if the connection should persist
* @param {Boolean} autoClearCache
* @returns {Promise<Object>}
*/
update (data, options, redisClient = null, autoClearCache = true) {
try {
// Update function dont return item
await this.model.update(data, options)
if (autoClearCache) {
// Clear redis cache for all data in this model when update
if (config.privateRuntimeConfig.cache.driver === 'redis') {
try {
await this.getRedisClient(redisClient).client.flushAll()
console.log('Flush All Cache After Update')
} catch (error) {
console.log('Error When Flushing All Cache After Update')
}
if (!redisClient) {
await this.closeRedisClient()
}
}
}
// Update not return item , so we have to call findOne to have the item
const item = await this.model.findOne(options)
console.log(['Update request', { item, data, options }])
return item
} catch (error) {
throw new TypeError('Error with update function')
}
}
/**
* Use Create if findOne doesn't returns item, or use Update
* @param {Object} data
* @param {Object} options
* @param {RedisClient} redisClient - sent if the connection should persist
* @param {Boolean} autoClearCache
* @returns {Promise<Object>}
*/
async createOrUpdate (data, options, redisClient = null, autoClearCache = true) {
const item = await this.findOne(options, true, redisClient)
if (item) {
// Update
return await this.update(data, options, redisClient, autoClearCache)
}
return await this.create(data, redisClient, autoClearCache)
}
/**
* Override Sequelize.model.findOrCreate
* @param {Object} options
* @returns {Promise<Object>}
*/
async findOrCreate (options) {
return await this.model.findOrCreate(options)
}
/**
* Override Sequelize.model.destroy
* @param {Object} options
* @param {RedisClient} redisClient - sent if the connection should persist
* @param {Boolean} autoClearCache
* @returns {Promise<Object>}
*/
async destroy (options, redisClient = null, autoClearCache = true) {
const result = await this.model.destroy(options)
if (autoClearCache) {
// Clear redis cache for all data in this model when update
if (config.privateRuntimeConfig.cache.driver === 'redis') {
try {
await this.getRedisClient(redisClient).client.flushAll()
console.log('Flush All Cache After Update')
} catch (error) {
console.log('Error When Flushing All Cache After Update')
}
if (!redisClient) {
await this.closeRedisClient()
}
}
}
console.log(['Destroy request', { options })
return result
}
/**
* find or create RedisClient instance
* @param {RedisClient} redisClient - sent if the connection should persist
*/
getRedisClient (redisClient = null) {
if (redisClient) {
this.redisClient = redisClient
} else {
this.redisClient = new RedisClient()
}
return this.redisClient
}
/**
* Close current RedisClient instance
*/
async closeRedisClient () {
if (this.redisClient) {
await this.redisClient.disconnect()
}
}
}
module.exports = AbstractRepository
'use strict'
const desymbolize = (options) => {
if (Array.isArray(options)) {
return options.map(desymbolize)
} else if (typeof options !== 'object') {
return options
} else {
const d = Object.assign(Object.create(Object.getPrototypeOf(options)), options)
Object.getOwnPropertySymbols(options).forEach(k => {
d[`<${Symbol.keyFor(k)}>`] = options[k]
delete d[k]
})
Object.keys(d).forEach(k => d[k] = desymbolize(d[k]))
return d
}
}
const cleanRedisData = (data) => {
if (Array.isArray(data)) {
return data.map(cleanRedisData)
} else if (data && data.dataValues && typeof data.dataValues === 'object') {
Object.keys(data.dataValues).forEach(key => {
data.dataValues[key] = cleanRedisData(data.dataValues[key])
})
return data.dataValues
}
return data
}
module.exports = {
desymbolize,
cleanRedisData
}
'use strict'
const Redis = require('ioredis')
const config = require('../config')
class RedisClient {
/**
* Constructor
*/
constructor (host = config.privateRuntimeConfig.cache.host, port = config.privateRuntimeConfig.cache.port, prefix = config.privateRuntimeConfig.cache.prefix) {
this.store = new Redis(port, host, { keyPrefix: prefix + ':' })
console.log('Connected to Redis, don\'t forget to quit after use.')
this.client = {
get: key => {
const start = new Date()
return this.store.get(key).finally(() => {
console.log(['redis client call', { action: 'READ', key, start }])
})
},
set: (key, val) => {
const start = new Date()
return this.store.set(key, val).finally(() => {
console.log(['redis client call', { action: 'WRITE', key, start }])
})
},
setex: (key, expires, val) => {
const start = new Date()
return this.store.setex(key, expires, val).finally(() => {
console.log(['redis client call', { action: 'WRITE', key, start, expires }])
})
},
keys: (prefix) => {
return this.store.keys(prefix)
},
del: (prefix) => {
return this.store.del(prefix).finally(() => {
console.log(['redis client call', { action: 'DELETE', prefix }])
})
},
flushAll: () => {
return new Promise((resolve, reject) => {
this.store.scanStream({ match: config.privateRuntimeConfig.cache.prefix + ':*' })
.on('data', async (keys) => {
if (keys.length) {
const pipeline = this.store.pipeline()
keys.forEach((key) => {
pipeline.del(key.replace(config.privateRuntimeConfig.cache.prefix + ':', ''))
})
await pipeline.exec()
}
return true
})
.on('end', () => {
console.log(['redis client call', { action: 'FLUSHALL' }])
resolve()
})
})
}
}
}
/**
* Close current redis connection
*/
disconnect () {
return new Promise((resolve, reject) => {
this.store.quit(() => {
console.log('Connection to Redis closed.')
return resolve(1)
})
})
}
/**
* Retrieve a cached data by key
* @param {String} key
* @returns {Promise<null|any>}
*/
async read (key) {
const res = await this.client.get(key)
if (!res) {
return null
}
try {
return JSON.parse(res)
} catch (error) {
return null
}
}
/**
* Store a key => data value
* @param {String} key
* @param {any} value
* @param {String} expires
* @returns {Promise<void>}
*/
async write (key, value, expires) {
if (!expires) {
expires = config.privateRuntimeConfig.cache.defaultExpires
}
await this.client.setex(key, expires, JSON.stringify(value))
}
/**
* Check if the key has cached data.
* If no cache found, call callback then return the response and cache it.
* @param {String} key
* @param {String} expires
* @param {Function} callback
* @returns {Promise<*|*>}
*/
async fetch (key, expires, callback) {
let object = await this.read(key)
if (object) {
return object
}
object = await callback()
await this.write(key, object, expires)
return object
}
}
module.exports = RedisClient
'use strict'
const config = require('../config')
const crypto = require('crypto')
const { desymbolize, cleanRedisData } = require('./helpers.service')
const sequelizeCache = {}
sequelizeCache.request = async (modelName, methodName, method, options, redisClient) => {
try {
const hash = crypto.createHash('sha256')
.update(JSON.stringify(desymbolize(options)), 'binary')
.digest('hex')
let cacheHit = true
const data = await redisClient.fetch(
modelName + ':' + methodName + ':' + hash,
config.privateRuntimeConfig.cache.defaultExpires,
async () => {
// If the key is not already in cache, the call is done
cacheHit = false
return await method(options)
}
)
// If it's a Redis Response, return only data.dataValues
const result = cleanRedisData(data)
console.log([methodName + ' request', { cacheHit: cacheHit, options: desymbolize(options), result }])
return result
} catch (error) {
console.log(['Error ' + methodName + ' request', { context: 'redis call', options, error: { name: error.name, message: error.message, stack: error.stack } }])
throw new TypeError('Error with ' + methodName + ' function')
}
}
module.exports = sequelizeCache
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment