Last active
February 1, 2022 13:32
-
-
Save noweh/c6894ead8f6cf5da8e35b38308f79a1f to your computer and use it in GitHub Desktop.
Node.js and Sequelize: Redis-client and AbstractRepository files for database requests
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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