├── .editorconfig ├── .gitignore ├── .jshintignore ├── .jshintrc ├── LICENSE ├── README.md ├── index.js ├── package.json └── test ├── config.json └── lib ├── redis-store-spec.js └── redis-store-zlib-spec.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | indent_style = space 9 | indent_size = 2 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | docs 4 | .idea/ 5 | package-lock.json 6 | .nyc_output 7 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | docs/ 4 | .nyc_output/ 5 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "jasmine": true, 4 | "curly": false, 5 | "quotmark": "single", 6 | "maxcomplexity": 8, 7 | "esnext": true, 8 | "unused": true, 9 | "globals": { 10 | "after": true, 11 | "afterEach": true, 12 | "assert": true, 13 | "before": true, 14 | "beforeEach": true, 15 | "describe": true, 16 | "expect": true, 17 | "it": true 18 | } 19 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Dial Once 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Node Cache Manager store for Redis 2 | ================================== 3 | 4 | (https://david-dm.org/dial-once/node-cache-manager-redis.svg)](https://david-dm.org/dial-once/node-cache-manager-redis) 5 | [![Sonar](http://proxy.dialonce.net/sonar/api/badges/gate?key=node-cache-manager-redis)](http://sonar.dialonce.net/dashboard?id=node-cache-manager-redis) 6 | [![Sonar](http://proxy.dialonce.net/sonar/api/badges/measure?key=node-cache-manager-redis&metric=ncloc)](http://sonar.dialonce.net/dashboard?id=node-cache-manager-redis) 7 | [![Sonar](http://proxy.dialonce.net/sonar/api/badges/measure?key=node-cache-manager-redis&metric=coverage)](http://sonar.dialonce.net/dashboard?id=node-cache-manager-redis) 8 | [![Sonar](http://proxy.dialonce.net/sonar/api/badges/measure?key=node-cache-manager-redis&metric=code_smells)](http://proxy.dialonce.net/sonar/api/badges/measure?key=node-cache-manager-redis&metric=coverage) 9 | [![Sonar](http://proxy.dialonce.net/sonar/api/badges/measure?key=node-cache-manager-redis&metric=bugs)](http://sonar.dialonce.net/dashboard?id=node-cache-manager-redis) 10 | [![Sonar](http://proxy.dialonce.net/sonar/api/badges/measure?key=node-cache-manager-redis&metric=sqale_debt_ratio)](http://sonar.dialonce.net/dashboard?id=node-cache-manager-redis) 11 | 12 | The Redis store for the [node-cache-manager](https://github.com/BryanDonovan/node-cache-manager) module. 13 | 14 | Installation 15 | ------------ 16 | 17 | ```sh 18 | npm install cache-manager-redis --save 19 | ``` 20 | 21 | Usage examples 22 | -------------- 23 | 24 | Here are examples that demonstrate how to implement the Redis cache store. 25 | 26 | ### Single store 27 | 28 | ```js 29 | var cacheManager = require('cache-manager'); 30 | var redisStore = require('cache-manager-redis'); 31 | 32 | var redisCache = cacheManager.caching({ 33 | store: redisStore, 34 | host: 'localhost', // default value 35 | port: 6379, // default value 36 | auth_pass: 'XXXXX', 37 | db: 0, 38 | ttl: 600 39 | }); 40 | 41 | var ttl = 5; 42 | 43 | // listen for redis connection error event 44 | redisCache.store.events.on('redisError', function(error) { 45 | // handle error here 46 | console.log(error); 47 | }); 48 | 49 | redisCache.set('foo', 'bar', { ttl: ttl }, function(err) { 50 | if (err) { 51 | throw err; 52 | } 53 | 54 | redisCache.get('foo', function(err, result) { 55 | console.log(result); 56 | // >> 'bar' 57 | redisCache.del('foo', function(err) {}); 58 | }); 59 | }); 60 | 61 | function getUser(id, cb) { 62 | setTimeout(function () { 63 | console.log("Returning user from slow database."); 64 | cb(null, {id: id, name: 'Bob'}); 65 | }, 100); 66 | } 67 | 68 | var userId = 123; 69 | var key = 'user_' + userId; 70 | 71 | // Note: ttl is optional in wrap() 72 | redisCache.wrap(key, function (cb) { 73 | getUser(userId, cb); 74 | }, { ttl: ttl }, function (err, user) { 75 | console.log(user); 76 | 77 | // Second time fetches user from redisCache 78 | redisCache.wrap(key, function (cb) { 79 | getUser(userId, cb); 80 | }, function (err, user) { 81 | console.log(user); 82 | }); 83 | }); 84 | 85 | // The del() method accepts a single key or array of keys, 86 | // with or without a callback. 87 | redisCache.set('foo', 'bar', function () { 88 | redisCache.set('bar', 'baz', function() { 89 | redisCache.set('baz', 'foo', function() { 90 | redisCache.del('foo'); 91 | redisCache.del(['bar', 'baz'], function() { }); 92 | }); 93 | }); 94 | }); 95 | 96 | // The keys() method uses the Redis SCAN command and accepts 97 | // optional `pattern` and `options` arguments. The `pattern` 98 | // must be a Redis glob-style string and defaults to '*'. The 99 | // options argument must be an object and accepts a single 100 | // `scanCount` property, which determines the number of elements 101 | // returned internally per call to SCAN. The default `scanCount` 102 | // is 100. 103 | redisCache.set('foo', 'bar', function () { 104 | redisCache.set('far', 'boo', function () { 105 | redisCache.keys('fo*', function (err, arrayOfKeys) { 106 | // arrayOfKeys: ['foo'] 107 | }); 108 | 109 | redisCache.keys(function (err, arrayOfKeys) { 110 | // arrayOfKeys: ['foo', 'far'] 111 | }); 112 | 113 | redisCache.keys('fa*', { scanCount: 10 }, function (err, arrayOfKeys) { 114 | // arrayOfKeys: ['far'] 115 | }); 116 | }); 117 | }); 118 | 119 | ``` 120 | 121 | ### Multi-store 122 | 123 | ```js 124 | var cacheManager = require('cache-manager'); 125 | var redisStore = require('cache-manager-redis'); 126 | 127 | var redisCache = cacheManager.caching({store: redisStore, db: 0, ttl: 600}); 128 | var memoryCache = cacheManager.caching({store: 'memory', max: 100, ttl: 60}); 129 | 130 | var multiCache = cacheManager.multiCaching([memoryCache, redisCache]); 131 | 132 | 133 | userId2 = 456; 134 | key2 = 'user_' + userId; 135 | ttl = 5; 136 | 137 | // Sets in all caches. 138 | multiCache.set('foo2', 'bar2', { ttl: ttl }, function(err) { 139 | if (err) { throw err; } 140 | 141 | // Fetches from highest priority cache that has the key. 142 | multiCache.get('foo2', function(err, result) { 143 | console.log(result); 144 | // >> 'bar2' 145 | 146 | // Delete from all caches 147 | multiCache.del('foo2'); 148 | }); 149 | }); 150 | 151 | // Note: ttl is optional in wrap() 152 | multiCache.wrap(key2, function (cb) { 153 | getUser(userId2, cb); 154 | }, { ttl: ttl }, function (err, user) { 155 | console.log(user); 156 | 157 | // Second time fetches user from memoryCache, since it's highest priority. 158 | // If the data expires in the memory cache, the next fetch would pull it from 159 | // the 'someOtherCache', and set the data in memory again. 160 | multiCache.wrap(key2, function (cb) { 161 | getUser(userId2, cb); 162 | }, function (err, user) { 163 | console.log(user); 164 | }); 165 | }); 166 | ``` 167 | 168 | ### Using a URL instead of options (if url is correct it overrides options host, port, db, auth_pass and ttl) 169 | Urls should be in this format `redis://[:password@]host[:port][/db-number][?ttl=value]` 170 | ```js 171 | var cacheManager = require('cache-manager'); 172 | var redisStore = require('cache-manager-redis'); 173 | 174 | var redisCache = cacheManager.caching({ 175 | store: redisStore, 176 | url: 'redis://:XXXX@localhost:6379/0?ttl=600' 177 | }); 178 | 179 | // proceed with redisCache 180 | ``` 181 | 182 | ### Seamless compression (currently only supports Node's built-in zlib / gzip implementation) 183 | 184 | ```js 185 | // Compression can be configured for the entire cache. 186 | var redisCache = cacheManager.caching({ 187 | store: redisStore, 188 | host: 'localhost', // default value 189 | port: 6379, // default value 190 | auth_pass: 'XXXXX', 191 | db: 0, 192 | ttl: 600, 193 | compress: true 194 | }); 195 | 196 | // Or on a per command basis. (only applies to get / set / wrap) 197 | redisCache.set('foo', 'bar', { compress: false }, function(err) { 198 | if (err) { 199 | throw err; 200 | } 201 | 202 | redisCache.get('foo', { compress: false }, function(err, result) { 203 | console.log(result); 204 | // >> 'bar' 205 | redisCache.del('foo', function(err) {}); 206 | }); 207 | }); 208 | 209 | // Setting the compress option to true will enable a default configuration 210 | // for best speed using gzip. For advanced use, a configuration object may 211 | // also be passed with implementation-specific parameters. Currently, only 212 | // the built-in zlib/gzip implementation is supported. 213 | var zlib = require('zlib'); 214 | var redisCache = cacheManager.caching({ 215 | store: redisStore, 216 | host: 'localhost', // default value 217 | port: 6379, // default value 218 | auth_pass: 'XXXXX', 219 | db: 0, 220 | ttl: 600, 221 | compress: { 222 | type: 'gzip', 223 | params: { 224 | level: zlib.Z_BEST_COMPRESSION 225 | } 226 | } 227 | }); 228 | ``` 229 | Currently, all implementation-specific configuration parameters are passed directly to the `zlib.gzip` and `zlib.gunzip` methods. Please see the [Node Zlib documentation](https://nodejs.org/dist/latest-v6.x/docs/api/zlib.html#zlib_class_options) for available options. 230 | 231 | Tests 232 | ----- 233 | 234 | 1. Run a Redis server 235 | 2. Run tests `npm test` or `npm run coverage` 236 | 237 | 238 | Contribution 239 | ------------ 240 | 241 | If you would like to contribute to the project, please fork it and send us a pull request. Please add tests for any new features or bug fixes. Also make sure the code coverage is not impacted. 242 | 243 | 244 | License 245 | ------- 246 | 247 | `node-cache-manager-redis` is licensed under the MIT license. 248 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var RedisPool = require('sol-redis-pool'); 4 | var EventEmitter = require('events').EventEmitter; 5 | var redisUrl = require('redis-url'); 6 | var zlib = require('zlib'); 7 | 8 | /** 9 | * The cache manager Redis Store module 10 | * @module redisStore 11 | * @param {Object} [args] - The store configuration (optional) 12 | * @param {String} args.host - The Redis server host 13 | * @param {Number} args.port - The Redis server port 14 | * @param {Number} args.db - The Redis server db 15 | * @param {function} args.isCacheableValue - function to override built-in isCacheableValue function (optional) 16 | * @param {boolean|Object} args.compress - (optional) Boolean / Config Object for pluggable compression. 17 | * Setting this to true will use a default gzip configuration for best speed. Passing in a config 18 | * object will forward those settings to the underlying compression implementation. Please see the 19 | * Node zlib documentation for a list of valid options for gzip: 20 | * https://nodejs.org/dist/latest-v4.x/docs/api/zlib.html#zlib_class_options 21 | */ 22 | function redisStore(args = {}) { 23 | var self = { 24 | name: 'redis', 25 | events: new EventEmitter() 26 | }; 27 | 28 | // cache-manager should always pass in args 29 | /* istanbul ignore next */ 30 | var redisOptions = getFromUrl(args) || args; 31 | var poolSettings = redisOptions; 32 | var Promise = args.promiseDependency || global.Promise; 33 | 34 | redisOptions.host = redisOptions.host || '127.0.0.1'; 35 | redisOptions.port = redisOptions.port || 6379; 36 | redisOptions.db = redisOptions.db || 0; 37 | 38 | // default compress config 39 | redisOptions.detect_buffers = true; 40 | var compressDefault = { 41 | type: 'gzip', 42 | params: { 43 | level: zlib.Z_BEST_SPEED 44 | } 45 | }; 46 | 47 | // if compress is boolean true, set default 48 | if (redisOptions.compress === true) { 49 | redisOptions.compress = compressDefault; 50 | } 51 | 52 | var pool = new RedisPool(redisOptions, poolSettings); 53 | 54 | pool.on('error', function(err) { 55 | self.events.emit('redisError', err); 56 | }); 57 | 58 | /** 59 | * Helper to connect to a connection pool 60 | * @private 61 | * @param {Function} cb - A callback that returns 62 | */ 63 | function connect(cb) { 64 | pool.acquireDb(cb, redisOptions.db); 65 | } 66 | 67 | /** 68 | * Helper to handle callback and release the connection 69 | * @private 70 | * @param {Object} conn - The Redis connection 71 | * @param {Function} [cb] - A callback that returns a potential error and the result 72 | * @param {Object} [opts] - The options (optional) 73 | */ 74 | function handleResponse(conn, cb, opts) { 75 | opts = opts || {}; 76 | 77 | return function(err, result) { 78 | pool.release(conn); 79 | 80 | if (err) { 81 | return cb && cb(err); 82 | } 83 | 84 | if (opts.parse) { 85 | 86 | if (result && opts.compress) { 87 | return zlib.gunzip(result, opts.compress.params || {}, function (gzErr, gzResult) { 88 | if (gzErr) { 89 | return cb && cb(gzErr); 90 | } 91 | try { 92 | gzResult = JSON.parse(gzResult); 93 | } catch (e) { 94 | return cb && cb(e); 95 | } 96 | 97 | return cb && cb(null, gzResult); 98 | }); 99 | } 100 | 101 | try { 102 | result = JSON.parse(result); 103 | } catch (e) { 104 | return cb && cb(e); 105 | } 106 | } 107 | 108 | return cb && cb(null, result); 109 | }; 110 | } 111 | 112 | /** 113 | * Extracts options from an args.url 114 | * @param {Object} args 115 | * @param {String} args.url a string in format of redis://[:password@]host[:port][/db-number][?option=value] 116 | * @returns {Object} the input object args if it is falsy, does not contain url or url is not string, otherwise a new object with own properties of args 117 | * but with host, port, db, ttl and auth_pass properties overridden by those provided in args.url. 118 | */ 119 | function getFromUrl(args) { 120 | if (!args || typeof args.url !== 'string') { 121 | return args; 122 | } 123 | 124 | try { 125 | var options = redisUrl.parse(args.url); 126 | // make a clone so we don't change input args 127 | return applyOptionsToArgs(args, options); 128 | } catch (e) { 129 | //url is unparsable so returning original 130 | return args; 131 | } 132 | 133 | } 134 | 135 | /** 136 | * Clones args'es own properties to a new object and sets isCacheableValue on the new object 137 | * @param {Object} args 138 | * @returns {Object} a clone of the args object 139 | */ 140 | function cloneArgs(args) { 141 | var newArgs = {}; 142 | for(var key in args){ 143 | if (key && args.hasOwnProperty(key)) { 144 | newArgs[key] = args[key]; 145 | } 146 | } 147 | newArgs.isCacheableValue = args.isCacheableValue && args.isCacheableValue.bind(newArgs); 148 | return newArgs; 149 | } 150 | 151 | /** 152 | * Apply some options like hostname, port, db, ttl, auth_pass, password 153 | * from options to newArgs host, port, db, auth_pass, password and ttl and return clone of args 154 | * @param {Object} args 155 | * @param {Object} options 156 | * @returns {Object} clone of args param with properties set to those of options 157 | */ 158 | function applyOptionsToArgs(args, options) { 159 | var newArgs = cloneArgs(args); 160 | newArgs.host = options.hostname; 161 | newArgs.port = parseInt(options.port, 10); 162 | newArgs.db = parseInt(options.database, 10); 163 | newArgs.auth_pass = options.password; 164 | newArgs.password = options.password; 165 | if(options.query && options.query.ttl){ 166 | newArgs.ttl = parseInt(options.query.ttl, 10); 167 | } 168 | return newArgs; 169 | } 170 | 171 | /** 172 | * Get a value for a given key. 173 | * @method get 174 | * @param {String} key - The cache key 175 | * @param {Object} [options] - The options (optional) 176 | * @param {boolean|Object} options.compress - compression configuration 177 | * @param {Function} cb - A callback that returns a potential error and the response 178 | * @returns {Promise} 179 | */ 180 | self.get = function(key, options, cb) { 181 | return new Promise(function(resolve, reject) { 182 | if (typeof options === 'function') { 183 | cb = options; 184 | options = {}; 185 | } 186 | options = options || {}; 187 | options.parse = true; 188 | 189 | cb = cb ? cb : (err, result) => err ? reject(err) : resolve(result); 190 | 191 | var compress = (options.compress || options.compress === false) ? options.compress : redisOptions.compress; 192 | if (compress) { 193 | options.compress = (compress === true) ? compressDefault : compress; 194 | key = new Buffer(key); 195 | } 196 | 197 | connect(function(err, conn) { 198 | if (err) { 199 | return cb(err); 200 | } 201 | 202 | conn.get(key, handleResponse(conn, cb, options)); 203 | }); 204 | }); 205 | }; 206 | 207 | /** 208 | * Set a value for a given key. 209 | * @method set 210 | * @param {String} key - The cache key 211 | * @param {String} value - The value to set 212 | * @param {Object} [options] - The options (optional) 213 | * @param {Object} options.ttl - The ttl value 214 | * @param {boolean|Object} options.compress - compression configuration 215 | * @param {Function} [cb] - A callback that returns a potential error, otherwise null 216 | * @returns {Promise} 217 | */ 218 | self.set = function(key, value, options, cb) { 219 | options = options || {}; 220 | if (typeof options === 'function') { 221 | cb = options; 222 | options = {}; 223 | } 224 | return new Promise(function(resolve, reject) { 225 | cb = cb || ((err, result) => err ? reject(err) : resolve(result)); 226 | 227 | if (!self.isCacheableValue(value)) { 228 | return cb(new Error('value cannot be ' + value)); 229 | } 230 | 231 | var ttl = (options.ttl || options.ttl === 0) ? options.ttl : redisOptions.ttl; 232 | var compress = (options.compress || options.compress === false) ? options.compress : redisOptions.compress; 233 | if (compress === true) { 234 | compress = compressDefault; 235 | } 236 | 237 | connect(function(err, conn) { 238 | if (err) { 239 | return cb(err); 240 | } 241 | var val = JSON.stringify(value) || '"undefined"'; 242 | 243 | // Refactored to remove duplicate code. 244 | function persist(pErr, pVal) { 245 | if (pErr) { 246 | return cb(pErr); 247 | } 248 | 249 | if (ttl) { 250 | conn.setex(key, ttl, pVal, handleResponse(conn, cb)); 251 | } else { 252 | conn.set(key, pVal, handleResponse(conn, cb)); 253 | } 254 | } 255 | 256 | if (compress) { 257 | zlib.gzip(val, compress.params || {}, persist); 258 | } else { 259 | persist(null, val); 260 | } 261 | }); 262 | }); 263 | }; 264 | 265 | /** 266 | * Delete value of a given key 267 | * @method del 268 | * @param {String|Array} key - The cache key or array of keys to delete 269 | * @param {Object} [options] - The options (optional) 270 | * @param {Function} [cb] - A callback that returns a potential error, otherwise null 271 | * @returns {Promise} 272 | */ 273 | self.del = function(key, options, cb) { 274 | return new Promise((resolve, reject) => { 275 | cb = cb || ((err) => err ? reject(err) : resolve('OK')); 276 | 277 | if (typeof options === 'function') { 278 | cb = options; 279 | options = {}; 280 | } 281 | 282 | connect(function(err, conn) { 283 | if (err) { 284 | return cb(err); 285 | } 286 | 287 | if (Array.isArray(key)) { 288 | var multi = conn.multi(); 289 | for (var i = 0, l = key.length; i < l; ++i) { 290 | multi.del(key[i]); 291 | } 292 | multi.exec(handleResponse(conn, cb)); 293 | } 294 | else { 295 | conn.del(key, handleResponse(conn, cb)); 296 | } 297 | }); 298 | }); 299 | }; 300 | 301 | /** 302 | * Delete all the keys of the currently selected DB 303 | * @method reset 304 | * @param {Function} [cb] - A callback that returns a potential error, otherwise null 305 | * @returns {Promise} 306 | */ 307 | self.reset = function(cb) { 308 | return new Promise((resolve, reject) => { 309 | cb = cb || (err => err ? reject(err) : resolve('OK')); 310 | connect(function(err, conn) { 311 | if (err) { 312 | return cb(err); 313 | } 314 | conn.flushdb(handleResponse(conn, cb)); 315 | }); 316 | }); 317 | }; 318 | 319 | /** 320 | * Returns the remaining time to live of a key that has a timeout. 321 | * @method ttl 322 | * @param {String} key - The cache key 323 | * @param {Function} cb - A callback that returns a potential error and the response 324 | * @returns {Promise} 325 | */ 326 | self.ttl = function(key, cb) { 327 | return new Promise((resolve, reject) => { 328 | cb = cb || ((err, res) => err ? reject(err) : resolve(res)); 329 | connect(function(err, conn) { 330 | if (err) { 331 | return cb(err); 332 | } 333 | conn.ttl(key, handleResponse(conn, cb)); 334 | }); 335 | }); 336 | }; 337 | 338 | /** 339 | * Returns all keys matching pattern using the SCAN command. 340 | * @method keys 341 | * @param {String} [pattern] - The pattern used to match keys (default: *) 342 | * @param {Object} [options] - The options (default: {}) 343 | * @param {number} [options.scanCount] - The number of keys to traverse with each call to SCAN (default: 100) 344 | * @param {Function} cb - A callback that returns a potential error and the response 345 | * @returns {Promise} 346 | */ 347 | self.keys = function(pattern, options, cb) { 348 | options = options || {}; 349 | 350 | // Account for all argument permutations. 351 | // Only cb supplied. 352 | if (typeof pattern === 'function') { 353 | cb = pattern; 354 | options = {}; 355 | pattern = '*'; 356 | } 357 | // options and cb supplied. 358 | else if (typeof pattern === 'object') { 359 | cb = options; 360 | options = pattern; 361 | pattern = '*'; 362 | } 363 | // pattern and cb supplied. 364 | else if (typeof options === 'function') { 365 | cb = options; 366 | options = {}; 367 | } 368 | 369 | return new Promise((resolve, reject) => { 370 | cb = cb || ((err, res) => err ? reject(err) : resolve(res)); 371 | connect(function(err, conn) { 372 | if (err) { 373 | return cb(err); 374 | } 375 | 376 | // Use an object to dedupe as scan can return duplicates 377 | var keysObj = {}; 378 | var scanCount = Number(options.scanCount) || 100; 379 | 380 | (function nextBatch(cursorId) { 381 | conn.scan(cursorId, 'match', pattern, 'count', scanCount, function (err, result) { 382 | if (err) { 383 | handleResponse(conn, cb)(err); 384 | } 385 | 386 | var nextCursorId = result[0]; 387 | var keys = result[1]; 388 | 389 | for (var i = 0, l = keys.length; i < l; ++i) { 390 | keysObj[keys[i]] = 1; 391 | } 392 | 393 | if (nextCursorId !== '0') { 394 | return nextBatch(nextCursorId); 395 | } 396 | 397 | handleResponse(conn, cb)(null, Object.keys(keysObj)); 398 | }); 399 | })(0); 400 | }); 401 | }); 402 | }; 403 | 404 | /** 405 | * Specify which values should and should not be cached. 406 | * If the function returns true, it will be stored in cache. 407 | * By default, it caches everything except undefined and null values. 408 | * Can be overriden via standard node-cache-manager options. 409 | * @method isCacheableValue 410 | * @param {String} value - The value to check 411 | * @return {Boolean} - Returns true if the value is cacheable, otherwise false. 412 | */ 413 | self.isCacheableValue = args.isCacheableValue || function(value) { 414 | return value !== undefined && value !== null; 415 | }; 416 | 417 | /** 418 | * Returns the underlying redis client connection 419 | * @method getClient 420 | * @param {Function} cb - A callback that returns a potential error and an object containing the Redis client and a done method 421 | * @returns {Promise} 422 | */ 423 | self.getClient = function(cb) { 424 | return new Promise((resolve, reject) => { 425 | cb = cb || ((err, res) => err ? reject(err) : resolve(res)); 426 | connect(function(err, conn) { 427 | if (err) { 428 | return cb(err); 429 | } 430 | cb(null, { 431 | client: conn, 432 | done: function(done) { 433 | var args = Array.prototype.slice.call(arguments, 1); 434 | pool.release(conn); 435 | 436 | if (done && typeof done === 'function') { 437 | done.apply(null, args); 438 | } 439 | } 440 | }); 441 | }); 442 | }); 443 | }; 444 | 445 | /** 446 | * Expose the raw pool object for testing purposes 447 | * @private 448 | */ 449 | self._pool = pool; 450 | 451 | return self; 452 | } 453 | 454 | module.exports = { 455 | create: function(args) { 456 | return redisStore(args); 457 | } 458 | }; 459 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cache-manager-redis", 3 | "version": "0.6.0", 4 | "description": "Redis store for the node-cache-manager", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node_modules/.bin/mocha --recursive test --exit", 8 | "cover": "CONSOLE_LOGGING=false ./node_modules/.bin/nyc --reporter=lcov ./node_modules/.bin/_mocha test --recursive --timeout=10000 --exit", 9 | "lint": "npm bin jshint .", 10 | "jsdoc": "node_modules/.bin/jsdoc . --package package.json --readme README.md --template node_modules/minami --destination docs" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/dial-once/node-cache-manager-redis.git" 15 | }, 16 | "keywords": [ 17 | "cache", 18 | "redis", 19 | "cache manager", 20 | "multiple cache" 21 | ], 22 | "author": "Dial Once", 23 | "license": "MIT", 24 | "dependencies": { 25 | "cache-manager": "^2.2.0", 26 | "redis-url": "^1.2.1", 27 | "sol-redis-pool": "^0.3.2" 28 | }, 29 | "devDependencies": { 30 | "mocha": "^5.2.0", 31 | "nyc": "^12.0.2", 32 | "jsdoc": "^3.3.3", 33 | "jshint": "^2.8.0", 34 | "minami": "^1.1.1", 35 | "sinon": "^1.17.2" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "redis": { 3 | "host": "localhost", 4 | "port": 6379, 5 | "auth_pass": "", 6 | "db": 0, 7 | "ttl": 60 8 | } 9 | } -------------------------------------------------------------------------------- /test/lib/redis-store-spec.js: -------------------------------------------------------------------------------- 1 | var config = require('../config.json'); 2 | var redisStore = require('../../index'); 3 | var sinon = require('sinon'); 4 | var assert = require('assert'); 5 | 6 | 7 | var redisCache; 8 | var customRedisCache; 9 | var sandbox; 10 | var injectError; 11 | 12 | before(function () { 13 | redisCache = require('cache-manager').caching({ 14 | store: redisStore, 15 | host: config.redis.host, 16 | port: config.redis.port, 17 | auth_pass: config.redis.auth_pass, 18 | db: config.redis.db, 19 | ttl: config.redis.ttl 20 | }); 21 | 22 | customRedisCache = require('cache-manager').caching({ 23 | store: redisStore, 24 | host: config.redis.host, 25 | port: config.redis.port, 26 | db: config.redis.db, 27 | ttl: config.redis.ttl, 28 | isCacheableValue: function (val) { 29 | // allow undefined 30 | if (val === undefined) { 31 | return true; 32 | } else if (val === 'FooBarString') { 33 | // disallow FooBarString 34 | return false; 35 | } 36 | return redisCache.store.isCacheableValue(val); 37 | } 38 | }); 39 | 40 | sandbox = sinon.sandbox.create(); 41 | 42 | injectError = () => { 43 | var pool = redisCache.store._pool; 44 | 45 | sandbox.stub(pool, 'acquireDb').yieldsAsync('Something unexpected'); 46 | sandbox.stub(pool, 'release'); 47 | }; 48 | }); 49 | 50 | beforeEach(function() { 51 | sandbox.restore(); 52 | 53 | return redisCache.reset(); 54 | }); 55 | 56 | describe ('initialization', function () { 57 | 58 | it('should create a store with password instead of auth_pass (auth_pass is deprecated for redis > 2.5)', function (done) { 59 | var redisPwdCache = require('cache-manager').caching({ 60 | store: redisStore, 61 | host: config.redis.host, 62 | port: config.redis.port, 63 | password: config.redis.auth_pass, 64 | db: config.redis.db, 65 | ttl: config.redis.ttl 66 | }); 67 | 68 | assert.equal(redisPwdCache.store._pool._redis_options.password, config.redis.auth_pass); 69 | redisPwdCache.set('pwdfoo', 'pwdbar', function (err) { 70 | assert.equal(err, null); 71 | redisCache.del('pwdfoo', function (errDel) { 72 | assert.equal(errDel, null); 73 | done(); 74 | }); 75 | }); 76 | }); 77 | 78 | }); 79 | 80 | describe('set', function () { 81 | it('should return a promise', function (done) { 82 | assert.ok(redisCache.set('foo', 'bar') instanceof Promise); 83 | done(); 84 | }); 85 | 86 | it('should resolve promise on success', function (done) { 87 | redisCache.set('foo', 'bar').then(result => { 88 | assert.equal(result, 'OK'); 89 | done(); 90 | }); 91 | }); 92 | 93 | it('should reject promise on error', function (done) { 94 | redisCache.set('foo', null) 95 | .then(() => done(new Error ('Should reject'))) 96 | .catch(() => done()); 97 | }); 98 | 99 | it('should store a value without ttl', function (done) { 100 | redisCache.set('foo', 'bar', function (err) { 101 | assert.equal(err, null); 102 | done(); 103 | }); 104 | }); 105 | 106 | it('should store a value with a specific ttl', function (done) { 107 | redisCache.set('foo', 'bar', config.redis.ttl, function (err) { 108 | assert.equal(err, null); 109 | done(); 110 | }); 111 | }); 112 | 113 | it('should store a value with a infinite ttl', function (done) { 114 | redisCache.set('foo', 'bar', {ttl: 0}, function (err) { 115 | assert.equal(err, null); 116 | redisCache.ttl('foo', function (err, ttl) { 117 | assert.equal(err, null); 118 | assert.equal(ttl, -1); 119 | done(); 120 | }); 121 | }); 122 | }); 123 | 124 | it('should not be able to store a null value (not cacheable)', function (done) { 125 | redisCache.set('foo2', null, function (err) { 126 | if (err) { 127 | return done(); 128 | } 129 | done(new Error('Null is not a valid value!')); 130 | }); 131 | }); 132 | 133 | it('should store a value without callback', function (done) { 134 | redisCache.set('foo', 'baz'); 135 | redisCache.get('foo', function (err, value) { 136 | assert.equal(err, null); 137 | assert.equal(value, 'baz'); 138 | done(); 139 | }); 140 | }); 141 | 142 | it('should not store an invalid value', function (done) { 143 | redisCache.set('foo1', undefined, function (err) { 144 | try { 145 | assert.notEqual(err, null); 146 | assert.equal(err.message, 'value cannot be undefined'); 147 | done(); 148 | } catch (e) { 149 | done(e); 150 | } 151 | }); 152 | }); 153 | 154 | it('should store an undefined value if permitted by isCacheableValue', function (done) { 155 | assert(customRedisCache.store.isCacheableValue(undefined), true); 156 | customRedisCache.set('foo3', undefined, function (err) { 157 | try { 158 | assert.equal(err, null); 159 | customRedisCache.get('foo3', function (err, data) { 160 | try { 161 | assert.equal(err, null); 162 | // redis stored undefined as 'undefined' 163 | assert.equal(data, 'undefined'); 164 | done(); 165 | } catch (e) { 166 | done(e); 167 | } 168 | }); 169 | } catch (e) { 170 | done(e); 171 | } 172 | }); 173 | }); 174 | 175 | it('should not store a value disallowed by isCacheableValue', function (done) { 176 | assert.strictEqual(customRedisCache.store.isCacheableValue('FooBarString'), false); 177 | customRedisCache.set('foobar', 'FooBarString', function (err) { 178 | try { 179 | assert.notEqual(err, null); 180 | assert.equal(err.message, 'value cannot be FooBarString'); 181 | done(); 182 | } catch (e) { 183 | done(e); 184 | } 185 | }); 186 | }); 187 | 188 | }); 189 | 190 | describe('get', function () { 191 | it('should return a promise', function (done) { 192 | assert.ok(redisCache.get('foo') instanceof Promise); 193 | done(); 194 | }); 195 | 196 | it('should resolve promise on success', function (done) { 197 | redisCache.set('foo', 'bar') 198 | .then(() => redisCache.get('foo')) 199 | .then(result => { 200 | assert.equal(result, 'bar'); 201 | done(); 202 | }); 203 | }); 204 | 205 | it('should reject promise on error', function (done) { 206 | injectError(); 207 | redisCache.get('foo') 208 | .then(() => done(new Error ('Should reject'))) 209 | .catch(() => done()); 210 | }); 211 | 212 | it('should retrieve a value for a given key', function (done) { 213 | var value = 'bar'; 214 | redisCache.set('foo', value, function () { 215 | redisCache.get('foo', function (err, result) { 216 | assert.equal(err, null); 217 | assert.equal(result, value); 218 | done(); 219 | }); 220 | }); 221 | }); 222 | 223 | it('should retrieve a value for a given key if options provided', function (done) { 224 | var value = 'bar'; 225 | redisCache.set('foo', value, function () { 226 | redisCache.get('foo', {}, function (err, result) { 227 | assert.equal(err, null); 228 | assert.equal(result, value); 229 | done(); 230 | }); 231 | }); 232 | }); 233 | 234 | it('should return null when the key is invalid', function (done) { 235 | redisCache.get('invalidKey', function (err, result) { 236 | assert.equal(err, null); 237 | assert.equal(result, null); 238 | done(); 239 | }); 240 | }); 241 | 242 | it('should return an error if there is an error acquiring a connection', function (done) { 243 | injectError(); 244 | redisCache.get('foo', function (err) { 245 | assert.notEqual(err, null); 246 | done(); 247 | }); 248 | }); 249 | }); 250 | 251 | describe('del', function () { 252 | it('should return a promise', function () { 253 | assert.ok(redisCache.del('foo', 'bar') instanceof Promise); 254 | }); 255 | 256 | it('should resolve promise on success', function () { 257 | return redisCache.del('foo', 'bar').then(result => assert.equal(result, 'OK')); 258 | }); 259 | 260 | it('should reject promise on error', function () { 261 | injectError(); 262 | 263 | return redisCache.del('foo').then(res => assert.fail(res), err => assert.notEqual(err, null)); 264 | }); 265 | 266 | it('should delete a value for a given key', function (done) { 267 | redisCache.set('foo', 'bar', function () { 268 | redisCache.del('foo', function (err) { 269 | assert.equal(err, null); 270 | done(); 271 | }); 272 | }); 273 | }); 274 | 275 | it('should delete a value for a given key without callback', function (done) { 276 | redisCache.set('foo', 'bar', function () { 277 | redisCache.del('foo'); 278 | done(); 279 | }); 280 | }); 281 | 282 | it('should delete multiple values for a given array of keys', function (done) { 283 | redisCache.set('foo', 'bar', function () { 284 | redisCache.set('bar', 'baz', function () { 285 | redisCache.set('baz', 'foo', function () { 286 | redisCache.del(['foo', 'bar', 'baz'], function (err) { 287 | assert.equal(err, null); 288 | done(); 289 | }); 290 | }); 291 | }); 292 | }); 293 | }); 294 | 295 | it('should delete multiple values for a given array of keys without callback', function (done) { 296 | redisCache.set('foo', 'bar', function () { 297 | redisCache.set('bar', 'baz', function () { 298 | redisCache.set('baz', 'foo', function () { 299 | redisCache.del(['foo', 'bar', 'baz']); 300 | done(); 301 | }); 302 | }); 303 | }); 304 | }); 305 | 306 | it('should return an error if there is an error acquiring a connection', function (done) { 307 | injectError(); 308 | redisCache.set('foo', 'bar', function () { 309 | redisCache.del('foo', function (err) { 310 | assert.notEqual(err, null); 311 | done(); 312 | }); 313 | }); 314 | }); 315 | }); 316 | 317 | describe('reset', function () { 318 | it('should return a promise', function () { 319 | assert.ok(redisCache.reset() instanceof Promise); 320 | }); 321 | 322 | it('should resolve promise on success', function () { 323 | return redisCache.reset().then(result => assert.equal(result, 'OK')); 324 | }); 325 | 326 | it('should reject promise on error', function () { 327 | injectError(); 328 | 329 | return redisCache.reset().then(res => assert.fail(res), err => assert.notEqual(err, null)); 330 | }); 331 | 332 | it('should flush underlying db', function (done) { 333 | redisCache.reset(function (err) { 334 | assert.equal(err, null); 335 | done(); 336 | }); 337 | }); 338 | 339 | it('should flush underlying db without callback', function (done) { 340 | redisCache.reset(); 341 | done(); 342 | }); 343 | 344 | it('should return an error if there is an error acquiring a connection', function (done) { 345 | injectError(); 346 | redisCache.reset(function (err) { 347 | assert.notEqual(err, null); 348 | done(); 349 | }); 350 | }); 351 | }); 352 | 353 | describe('ttl', function () { 354 | it('should return a promise', function () { 355 | assert.ok(redisCache.ttl('foo') instanceof Promise); 356 | }); 357 | 358 | it('should resolve promise on success', function () { 359 | return redisCache.ttl('foo').then(result => assert.ok(Number.isInteger(result))); 360 | }); 361 | 362 | it('should reject promise on error', function () { 363 | injectError(); 364 | 365 | return redisCache.ttl('foo').then(() => assert.fail(), err => assert.notEqual(err, null)); 366 | }); 367 | 368 | it('should retrieve ttl for a given key', function (done) { 369 | redisCache.set('foo', 'bar', function () { 370 | redisCache.ttl('foo', function (err, ttl) { 371 | assert.equal(err, null); 372 | assert.equal(ttl, config.redis.ttl); 373 | done(); 374 | }); 375 | }); 376 | }); 377 | 378 | it('should retrieve ttl for an invalid key', function (done) { 379 | redisCache.ttl('invalidKey', function (err, ttl) { 380 | assert.equal(err, null); 381 | assert.notEqual(ttl, null); 382 | done(); 383 | }); 384 | }); 385 | 386 | it('should return an error if there is an error acquiring a connection', function (done) { 387 | injectError(); 388 | redisCache.set('foo', 'bar', function () { 389 | redisCache.ttl('foo', function (err) { 390 | assert.notEqual(err, null); 391 | done(); 392 | }); 393 | }); 394 | }); 395 | }); 396 | 397 | describe('keys', function () { 398 | it('should return a promise', function () { 399 | assert.ok(redisCache.keys('f*') instanceof Promise); 400 | }); 401 | 402 | it('should resolve promise on success', function () { 403 | return redisCache.keys('f*').then(result => assert.ok(Array.isArray(result))); 404 | }); 405 | 406 | it('should reject promise on error', function () { 407 | injectError(); 408 | 409 | return redisCache.keys('f*').then(res => assert.fail(res), err => assert.notEqual(err, null)); 410 | }); 411 | 412 | it('should return an array of keys for the given pattern', function (done) { 413 | redisCache.set('foo', 'bar', function () { 414 | redisCache.set('far', 'boo', function () { 415 | redisCache.set('faz', 'bam', function () { 416 | redisCache.keys('f*', function (err, arrayOfKeys) { 417 | assert.equal(err, null); 418 | assert.notEqual(arrayOfKeys, null); 419 | assert.notEqual(arrayOfKeys.indexOf('foo'), -1); 420 | assert.equal(arrayOfKeys.length, 3); 421 | done(); 422 | }); 423 | }); 424 | }); 425 | }); 426 | }); 427 | 428 | it('should accept a scanCount option', function (done) { 429 | redisCache.set('foo', 'bar', function () { 430 | redisCache.set('far', 'boo', function () { 431 | redisCache.set('faz', 'bam', function () { 432 | redisCache.keys('f*', { scanCount: 10 }, function (err, arrayOfKeys) { 433 | assert.equal(err, null); 434 | assert.notEqual(arrayOfKeys, null); 435 | assert.notEqual(arrayOfKeys.indexOf('foo'), -1); 436 | assert.equal(arrayOfKeys.length, 3); 437 | done(); 438 | }); 439 | }); 440 | }); 441 | }); 442 | }); 443 | 444 | it('should return an array of keys without pattern', function (done) { 445 | redisCache.set('foo', 'bar', function () { 446 | redisCache.set('far', 'boo', function () { 447 | redisCache.set('faz', 'bam', function () { 448 | redisCache.keys(function (err, arrayOfKeys) { 449 | assert.equal(err, null); 450 | assert.notEqual(arrayOfKeys, null); 451 | assert.notEqual(arrayOfKeys.indexOf('foo'), -1); 452 | assert.equal(arrayOfKeys.length, 3); 453 | done(); 454 | }); 455 | }); 456 | }); 457 | }); 458 | }); 459 | 460 | it('should accept scanCount option without pattern', function (done) { 461 | redisCache.set('foo', 'bar', function () { 462 | redisCache.set('far', 'boo', function () { 463 | redisCache.set('faz', 'bam', function () { 464 | redisCache.keys({ scanCount: 10 }, function (err, arrayOfKeys) { 465 | assert.equal(err, null); 466 | assert.notEqual(arrayOfKeys, null); 467 | assert.notEqual(arrayOfKeys.indexOf('foo'), -1); 468 | assert.equal(arrayOfKeys.length, 3); 469 | done(); 470 | }); 471 | }); 472 | }); 473 | }); 474 | }); 475 | 476 | it('should return an error if there is an error acquiring a connection', function (done) { 477 | injectError(); 478 | redisCache.set('foo', 'bar', function () { 479 | redisCache.keys('f*', function (err) { 480 | assert.notEqual(err, null); 481 | done(); 482 | }); 483 | }); 484 | }); 485 | }); 486 | 487 | describe('isCacheableValue', function () { 488 | it('should return true when the value is not undefined', function (done) { 489 | assert.equal(redisCache.store.isCacheableValue(0), true); 490 | assert.equal(redisCache.store.isCacheableValue(100), true); 491 | assert.equal(redisCache.store.isCacheableValue(''), true); 492 | assert.equal(redisCache.store.isCacheableValue('test'), true); 493 | done(); 494 | }); 495 | 496 | it('should return false when the value is undefined', function (done) { 497 | assert.equal(redisCache.store.isCacheableValue(undefined), false); 498 | done(); 499 | }); 500 | 501 | it('should return false when the value is null', function (done) { 502 | assert.equal(redisCache.store.isCacheableValue(null), false); 503 | done(); 504 | }); 505 | }); 506 | 507 | describe('getClient', function () { 508 | it('should return a promise', function () { 509 | assert.ok(redisCache.store.getClient().then(redis => redis.done()) instanceof Promise); 510 | }); 511 | 512 | it('should resolve promise on success', function () { 513 | return redisCache.store.getClient().then(redis => { 514 | assert.notEqual(redis, null); 515 | redis.done(); 516 | }); 517 | }); 518 | 519 | it('should reject promise on error', function () { 520 | injectError(); 521 | 522 | return redisCache.store.getClient().then(res => assert.fail(res), err => assert.notEqual(err, null)); 523 | }); 524 | 525 | it('should return redis client', function (done) { 526 | redisCache.store.getClient(function (err, redis) { 527 | assert.equal(err, null); 528 | assert.notEqual(redis, null); 529 | assert.notEqual(redis.client, null); 530 | redis.done(done); 531 | }); 532 | }); 533 | 534 | it('should handle no done callback without an error', function (done) { 535 | redisCache.store.getClient(function (err, redis) { 536 | assert.equal(err, null); 537 | assert.notEqual(redis, null); 538 | assert.notEqual(redis.client, null); 539 | redis.done(); 540 | done(); 541 | }); 542 | }); 543 | 544 | it('should return an error if there is an error acquiring a connection', function (done) { 545 | injectError(); 546 | redisCache.store.getClient(function (err) { 547 | assert.notEqual(err, null); 548 | done(); 549 | }); 550 | }); 551 | }); 552 | 553 | describe('redisErrorEvent', function () { 554 | it('should return an error when the redis server is unavailable', function (done) { 555 | redisCache.store.events.on('redisError', function (err) { 556 | assert.notEqual(err, null); 557 | done(); 558 | }); 559 | redisCache.store._pool.emit('error', 'Something unexpected'); 560 | }); 561 | }); 562 | 563 | describe('uses url to override redis options', function () { 564 | var redisCacheByUrl; 565 | 566 | before(function () { 567 | redisCacheByUrl = require('cache-manager').caching({ 568 | store: redisStore, 569 | // redis://[:password@]host[:port][/db-number][?option=value] 570 | url: 'redis://:' + config.redis.auth_pass +'@' + config.redis.host + ':' + config.redis.port + '/' + config.redis.db +'?ttl=' + config.redis.ttl, 571 | // some fakes to see that url overrides them 572 | host: 'test-host', 573 | port: -78, 574 | db: -7, 575 | auth_pass: 'test_pass', 576 | password: 'test_pass', 577 | ttl: -6 578 | }); 579 | }); 580 | 581 | it('should ignore other options if set in url', function() { 582 | assert.equal(redisCacheByUrl.store._pool._redis_options.host, config.redis.host); 583 | assert.equal(redisCacheByUrl.store._pool._redis_options.port, config.redis.port); 584 | assert.equal(redisCacheByUrl.store._pool._redis_default_db, config.redis.db); 585 | assert.equal(redisCacheByUrl.store._pool._redis_options.auth_pass, config.redis.auth_pass); 586 | assert.equal(redisCacheByUrl.store._pool._redis_options.password, config.redis.auth_pass); 587 | }); 588 | 589 | it('should get and set values without error', function (done) { 590 | var key = 'byUrlKey'; 591 | var value = 'test'; 592 | redisCacheByUrl.set(key, value, function (err) { 593 | assert.equal(err, null); 594 | redisCacheByUrl.get(key, function(getErr, val){ 595 | assert.equal(getErr, null); 596 | assert.equal(val, value); 597 | done(); 598 | }); 599 | }); 600 | }); 601 | }); 602 | 603 | describe('overridable isCacheableValue function', function () { 604 | var redisCache2; 605 | 606 | before(function () { 607 | redisCache2 = require('cache-manager').caching({ 608 | store: redisStore, 609 | isCacheableValue: function () {return 'I was overridden';} 610 | }); 611 | }); 612 | 613 | it('should return its return value instead of the built-in function', function (done) { 614 | assert.equal(redisCache2.store.isCacheableValue(0), 'I was overridden'); 615 | done(); 616 | }); 617 | }); 618 | 619 | describe('defaults', function () { 620 | var redisCache2; 621 | 622 | before(function () { 623 | redisCache2 = require('cache-manager').caching({ 624 | store: redisStore 625 | }); 626 | }); 627 | 628 | it('should default the host to `127.0.0.1`', function () { 629 | assert.equal(redisCache2.store._pool._redis_options.host, '127.0.0.1'); 630 | }); 631 | 632 | it('should default the port to 6379', function () { 633 | assert.equal(redisCache2.store._pool._redis_options.port, 6379); 634 | }); 635 | }); 636 | 637 | describe('wrap function', function () { 638 | 639 | // Simulate retrieving a user from a database 640 | function getUser(id, cb) { 641 | setTimeout(function () { 642 | cb(null, { id: id }); 643 | }, 100); 644 | } 645 | 646 | // Simulate retrieving a user from a database with Promise 647 | function getUserPromise(id) { 648 | return new Promise(function (resolve) { 649 | setTimeout(function () { 650 | resolve({ id: id }); 651 | }, 100); 652 | }); 653 | } 654 | 655 | it('should be able to cache objects', function (done) { 656 | var userId = 123; 657 | 658 | // First call to wrap should run the code 659 | redisCache.wrap('wrap-user', function (cb) { 660 | getUser(userId, cb); 661 | }, function (err, user) { 662 | assert.equal(user.id, userId); 663 | 664 | // Second call to wrap should retrieve from cache 665 | redisCache.wrap('wrap-user', function (cb) { 666 | getUser(userId+1, cb); 667 | }, function (err, user) { 668 | assert.equal(user.id, userId); 669 | done(); 670 | }); 671 | }); 672 | }); 673 | 674 | it('should work with promises', function () { 675 | var userId = 123; 676 | 677 | // First call to wrap should run the code 678 | return redisCache 679 | .wrap('wrap-promise', function () { 680 | return getUserPromise(userId); 681 | }) 682 | .then(function (user) { 683 | assert.equal(user.id, userId); 684 | 685 | // Second call to wrap should retrieve from cache 686 | return redisCache 687 | .wrap('wrap-promise', function () { 688 | return getUserPromise(userId+1); 689 | }) 690 | .then(function (user) { 691 | assert.equal(user.id, userId); 692 | }); 693 | }); 694 | }); 695 | }); 696 | -------------------------------------------------------------------------------- /test/lib/redis-store-zlib-spec.js: -------------------------------------------------------------------------------- 1 | var config = require('../config.json'); 2 | var redisStore = require('../../index'); 3 | var sinon = require('sinon'); 4 | var assert = require('assert'); 5 | var zlib = require('zlib'); 6 | 7 | var redisCompressCache; 8 | var customRedisCompressCache; 9 | var testJson; 10 | 11 | describe('Compression Tests', function () { 12 | 13 | before(function () { 14 | redisCompressCache = require('cache-manager').caching({ 15 | store: redisStore, 16 | host: config.redis.host, 17 | port: config.redis.port, 18 | auth_pass: config.redis.auth_pass, 19 | db: config.redis.db, 20 | ttl: config.redis.ttl, 21 | compress: true 22 | }); 23 | 24 | customRedisCompressCache = require('cache-manager').caching({ 25 | store: redisStore, 26 | host: config.redis.host, 27 | port: config.redis.port, 28 | db: config.redis.db, 29 | ttl: config.redis.ttl, 30 | compress: true, 31 | isCacheableValue: function (val) { 32 | // allow undefined 33 | if (val === undefined) { 34 | return true; 35 | } else if (val === 'FooBarString') { 36 | return false; 37 | } 38 | return redisCompressCache.store.isCacheableValue(val); 39 | } 40 | }); 41 | 42 | testJson = JSON.stringify(testObject); 43 | }); 44 | 45 | beforeEach(function(done) { 46 | redisCompressCache.reset(function () { 47 | done(); 48 | }); 49 | }); 50 | 51 | describe('compress set', function () { 52 | it('should store a value without ttl', function (done) { 53 | redisCompressCache.set('foo', 'bar', function (err) { 54 | assert.equal(err, null); 55 | done(); 56 | }); 57 | }); 58 | 59 | it('should store a value with a specific ttl', function (done) { 60 | redisCompressCache.set('foo', 'bar', config.redis.ttl, function (err) { 61 | assert.equal(err, null); 62 | done(); 63 | }); 64 | }); 65 | 66 | it('should store a value with a infinite ttl', function (done) { 67 | redisCompressCache.set('foo', 'bar', { ttl: 0 }, function (err) { 68 | assert.equal(err, null); 69 | redisCompressCache.ttl('foo', function (err, ttl) { 70 | assert.equal(err, null); 71 | assert.equal(ttl, -1); 72 | done(); 73 | }); 74 | }); 75 | }); 76 | 77 | it('should not be able to store a null value', function (done) { 78 | try { 79 | redisStore.set('foo2', null, function () { 80 | done(new Error('Should not be able to store a null value')); 81 | }); 82 | } catch(e) { 83 | done(); 84 | } 85 | }); 86 | 87 | it('should store a value without callback', function (done) { 88 | redisCompressCache.set('foo', 'baz'); 89 | redisCompressCache.get('foo', function (err, value) { 90 | assert.equal(err, null); 91 | assert.equal(value, 'baz'); 92 | done(); 93 | }); 94 | }); 95 | 96 | it('should not store an invalid value', function (done) { 97 | redisCompressCache.set('foo1', undefined, function (err) { 98 | try { 99 | assert.notEqual(err, null); 100 | assert.equal(err.message, 'value cannot be undefined'); 101 | done(); 102 | } catch(e) { 103 | done(e); 104 | } 105 | }); 106 | }); 107 | 108 | it('should store an undefined value if permitted by isCacheableValue', function (done) { 109 | assert(customRedisCompressCache.store.isCacheableValue(undefined), true); 110 | customRedisCompressCache.set('foo3', undefined, function (err) { 111 | try { 112 | assert.equal(err, null); 113 | customRedisCompressCache.get('foo3', function (err, data) { 114 | try { 115 | assert.equal(err, null); 116 | // redis stored undefined as 'undefined' 117 | assert.equal(data, 'undefined'); 118 | done(); 119 | } catch(e) { 120 | done(e); 121 | } 122 | }); 123 | } catch(e) { 124 | done(e); 125 | } 126 | }); 127 | }); 128 | 129 | it('should not store a value disallowed by isCacheableValue', function (done) { 130 | assert.strictEqual(customRedisCompressCache.store.isCacheableValue('FooBarString'), false); 131 | customRedisCompressCache.set('foobar', 'FooBarString', function (err) { 132 | try { 133 | assert.notEqual(err, null); 134 | assert.equal(err.message, 'value cannot be FooBarString'); 135 | done(); 136 | } catch(e) { 137 | done(e); 138 | } 139 | }); 140 | }); 141 | }); 142 | 143 | describe('compress get', function () { 144 | it('should retrieve a value for a given key', function (done) { 145 | redisCompressCache.set('foo', testObject, function () { 146 | redisCompressCache.get('foo', function (err, result) { 147 | assert.equal(err, null); 148 | assert.deepEqual(result, testObject); 149 | done(); 150 | }); 151 | }); 152 | }); 153 | 154 | it('should retrieve a value for a given key if options provided', function (done) { 155 | redisCompressCache.set('foo', testObject, function () { 156 | redisCompressCache.get('foo', {}, function (err, result) { 157 | assert.equal(err, null); 158 | assert.deepEqual(result, testObject); 159 | done(); 160 | }); 161 | }); 162 | }); 163 | 164 | it('should return null when the key is invalid', function (done) { 165 | redisCompressCache.get('invalidKey', function (err, result) { 166 | assert.equal(err, null); 167 | assert.equal(result, null); 168 | done(); 169 | }); 170 | }); 171 | 172 | it('should return an error if there is an error acquiring a connection', function (done) { 173 | var pool = redisCompressCache.store._pool; 174 | sinon.stub(pool, 'acquireDb').yieldsAsync('Something unexpected'); 175 | sinon.stub(pool, 'release'); 176 | redisCompressCache.get('foo', function (err) { 177 | pool.acquireDb.restore(); 178 | pool.release.restore(); 179 | assert.notEqual(err, null); 180 | done(); 181 | }); 182 | }); 183 | }); 184 | 185 | describe('compress uses url to override redis options', function () { 186 | var redisCacheByUrl; 187 | 188 | before(function () { 189 | redisCacheByUrl = require('cache-manager').caching({ 190 | store: redisStore, 191 | // redis://[:password@]host[:port][/db-number][?option=value] 192 | url: 'redis://:' + config.redis.auth_pass + '@' + config.redis.host + ':' + config.redis.port + '/' + config.redis.db + '?ttl=' + config.redis.ttl, 193 | // some fakes to see that url overrides them 194 | host: 'test-host', 195 | port: -78, 196 | db: -7, 197 | auth_pass: 'test_pass', 198 | password: 'test_pass', 199 | ttl: -6, 200 | compress: true 201 | }); 202 | }); 203 | 204 | it('should ignore other options if set in url', function () { 205 | assert.equal(redisCacheByUrl.store._pool._redis_options.host, config.redis.host); 206 | assert.equal(redisCacheByUrl.store._pool._redis_options.port, config.redis.port); 207 | assert.equal(redisCacheByUrl.store._pool._redis_default_db, config.redis.db); 208 | assert.equal(redisCacheByUrl.store._pool._redis_options.auth_pass, config.redis.auth_pass); 209 | assert.equal(redisCacheByUrl.store._pool._redis_options.password, config.redis.auth_pass); 210 | }); 211 | 212 | it('should get and set values without error', function (done) { 213 | var key = 'byUrlKey'; 214 | redisCacheByUrl.set(key, testObject, function (err) { 215 | assert.equal(err, null); 216 | redisCacheByUrl.get(key, function (getErr, val) { 217 | assert.equal(getErr, null); 218 | assert.deepEqual(val, testObject); 219 | done(); 220 | }); 221 | }); 222 | }); 223 | }); 224 | 225 | describe('compress specific', function () { 226 | var bestSpeed; 227 | 228 | it('should compress the value being stored', function (done) { 229 | redisCompressCache.set('foo', testObject, function (err) { 230 | assert.equal(err, null); 231 | redisCompressCache.store.getClient(function (err, redis) { 232 | assert.equal(err, null); 233 | redis.client.strlen('foo', function (err, length) { 234 | assert.equal(err, null); 235 | console.log('\nBest Speed (gzip)'); 236 | console.log('JSON length: ', testJson.length); 237 | console.log('Compress length: ' + length); 238 | console.log('REDUCTION: ' + Math.floor((length / testJson.length) * 100) + '% of original\n'); 239 | bestSpeed = length; 240 | redis.done(); 241 | done(); 242 | }); 243 | }); 244 | }); 245 | }); 246 | 247 | it('should allow compress specific options', function (done) { 248 | var opts = { 249 | type: 'gzip', 250 | params: { level: zlib.Z_BEST_COMPRESSION } 251 | }; 252 | redisCompressCache.set('foo', testObject, { compress: opts }, function (err) { 253 | assert.equal(err, null); 254 | redisCompressCache.store.getClient(function (err, redis) { 255 | assert.equal(err, null); 256 | redis.client.strlen('foo', function (err, length) { 257 | assert.equal(err, null); 258 | assert(length < bestSpeed); 259 | console.log('\nBest Compression (gzip)'); 260 | console.log('JSON length: ', testJson.length); 261 | console.log('Compress length: ' + length); 262 | console.log('REDUCTION: ' + Math.floor((length / testJson.length) * 100) + '% of original\n'); 263 | redis.done(); 264 | redisCompressCache.get('foo', { compress: opts }, function (err, result) { 265 | assert.equal(err, null); 266 | assert.deepEqual(result, testObject); 267 | done(); 268 | }); 269 | }); 270 | }); 271 | }); 272 | }); 273 | 274 | it('should allow compress to be turned off per command', function (done) { 275 | redisCompressCache.set('foo', testObject, { compress: false }, function (err) { 276 | assert.equal(err, null); 277 | redisCompressCache.store.getClient(function (err, redis) { 278 | assert.equal(err, null); 279 | redis.client.strlen('foo', function (err, length) { 280 | assert.equal(length, testJson.length); 281 | redis.done(); 282 | redisCompressCache.get('foo', { compress: false }, function (err, result) { 283 | assert.equal(err, null); 284 | assert.deepEqual(result, testObject); 285 | done(); 286 | }); 287 | }); 288 | }); 289 | }); 290 | }); 291 | }); 292 | 293 | describe('wrap function', function () { 294 | 295 | // Simulate retrieving a user from a database 296 | function getUser(id, cb) { 297 | setTimeout(function () { 298 | cb(null, { id: id }); 299 | }, 100); 300 | } 301 | 302 | // Simulate retrieving a user from a database with Promise 303 | function getUserPromise(id) { 304 | return new Promise(function (resolve) { 305 | setTimeout(function () { 306 | resolve({ id: id }); 307 | }, 100); 308 | }); 309 | } 310 | 311 | it('should be able to cache objects', function (done) { 312 | var userId = 123; 313 | 314 | // First call to wrap should run the code 315 | redisCompressCache.wrap('wrap-compress', function (cb) { 316 | getUser(userId, cb); 317 | }, function (err, user) { 318 | assert.equal(user.id, userId); 319 | 320 | // Second call to wrap should retrieve from cache 321 | redisCompressCache.wrap('wrap-compress', function (cb) { 322 | getUser(userId+1, cb); 323 | }, function (err, user) { 324 | assert.equal(user.id, userId); 325 | done(); 326 | }); 327 | }); 328 | }); 329 | 330 | it('should work with promises', function () { 331 | var userId = 123; 332 | 333 | // First call to wrap should run the code 334 | return redisCompressCache 335 | .wrap('wrap-compress-promise', function () { 336 | return getUserPromise(userId); 337 | }) 338 | .then(function (user) { 339 | assert.equal(user.id, userId); 340 | 341 | // Second call to wrap should retrieve from cache 342 | return redisCompressCache 343 | .wrap('wrap-compress-promise', function () { 344 | return getUserPromise(userId+1); 345 | }) 346 | .then(function (user) { 347 | assert.equal(user.id, userId); 348 | }); 349 | }); 350 | }); 351 | }); 352 | }); 353 | 354 | 355 | var testObject = { 356 | _id: '57d046876102e12cd5b83fb0', 357 | index: 0, 358 | guid: '1a18758b-fa38-4ced-8d05-44637bf4716e', 359 | isActive: false, 360 | balance: '$1,116.12', 361 | picture: 'http://placehold.it/32x32', 362 | age: 39, 363 | eyeColor: 'blue', 364 | name: 'Lara Crane', 365 | gender: 'female', 366 | company: 'BIOTICA', 367 | email: 'laracrane@biotica.com', 368 | phone: '+1 (911) 538-2679', 369 | address: '330 Church Avenue, Slovan, Kansas, 3416', 370 | about: 'Aliqua incididunt eiusmod Lorem minim nostrud aliquip reprehenderit culpa aute exercitation. In deserunt irure ad reprehenderit labore cupidatat qui cupidatat dolore ullamco et do ullamco ut. Laborum cupidatat nostrud quis non quis laborum aute nisi sint consequat tempor dolore voluptate. Cillum minim minim enim ea id aliqua laboris elit exercitation.\r\nCulpa in aute est pariatur quis. Tempor dolor ullamco ex Lorem deserunt commodo aliqua. Anim officia esse veniam minim veniam laboris nostrud ipsum ullamco esse nulla adipisicing minim. Eu minim occaecat deserunt eu est ex.\r\nDuis nostrud magna excepteur id officia mollit veniam ipsum. Lorem adipisicing ad esse ad ullamco et consectetur in ex tempor mollit consequat cillum. Adipisicing ex anim consequat anim non exercitation adipisicing ipsum exercitation aute reprehenderit esse ad aliqua. Ut duis consequat cupidatat eiusmod sint voluptate nulla fugiat sunt nulla eu. Excepteur labore proident laboris enim laborum esse reprehenderit fugiat. Cupidatat sit esse voluptate magna mollit fugiat velit Lorem elit pariatur id. Deserunt in ad laboris nulla cupidatat deserunt ullamco voluptate consequat veniam elit exercitation occaecat proident.\r\nEa occaecat ullamco exercitation elit Lorem pariatur reprehenderit. Et mollit proident excepteur enim tempor excepteur sunt laborum deserunt anim fugiat dolor sunt. Nostrud pariatur incididunt aliquip dolore in id elit fugiat.\r\nVeniam ullamco cupidatat mollit commodo fugiat nisi incididunt qui reprehenderit laboris esse Lorem sint mollit. Sunt qui consectetur anim aute culpa laboris ut cupidatat incididunt elit do nisi. Minim minim incididunt eu fugiat. Eiusmod et mollit aliquip minim tempor consectetur adipisicing id sunt. Cillum in mollit elit laborum. Dolore excepteur do consectetur aliqua. Laboris velit ad proident reprehenderit voluptate nulla ipsum nisi dolore dolor.\r\n', 371 | registered: '2015-09-15T10:09:44 +07:00', 372 | latitude: 52.954985, 373 | longitude: -159.875625, 374 | tags: [ 375 | 'ad', 376 | 'consectetur', 377 | 'occaecat', 378 | 'exercitation', 379 | 'ex', 380 | 'nisi', 381 | 'magna' 382 | ], 383 | friends: [ 384 | { 385 | id: 0, 386 | name: 'Tabatha Reeves', 387 | about: 'Veniam id ea anim commodo aliqua non aliqua velit. Dolor cillum exercitation eu commodo ea amet irure aute ad. Magna officia tempor consequat irure magna sunt dolor et pariatur est.\r\nAliqua ut et commodo adipisicing in exercitation nisi. Officia in culpa velit voluptate do. Dolor deserunt ex tempor qui nulla labore exercitation nulla fugiat. Esse elit amet consectetur id ad tempor tempor. Ipsum eiusmod velit nostrud laboris in do velit occaecat eu commodo voluptate ea eu.\r\nAnim veniam commodo consectetur sit fugiat aliquip est Lorem tempor sunt. Proident laborum est commodo eiusmod irure occaecat nulla ipsum magna ullamco. Amet nisi voluptate elit quis cupidatat reprehenderit do excepteur amet sit et commodo officia.\r\nCillum dolore consectetur quis reprehenderit non laborum cillum ea minim non officia consectetur. In dolor adipisicing ea est qui enim mollit ea irure. Voluptate qui eiusmod aliqua cillum enim aliquip fugiat nostrud elit irure. Magna anim officia quis irure ut quis Lorem magna cillum voluptate et aute ad. Nostrud sit ipsum velit magna aliquip mollit incididunt velit commodo ea do cupidatat duis.\r\nEa non sint pariatur laborum deserunt veniam dolore irure ipsum voluptate. Ea ea deserunt officia sit ullamco ea. Irure in deserunt aliqua duis.\r\n' 388 | }, 389 | { 390 | id: 1, 391 | name: 'Samantha Bowen', 392 | about: 'Commodo et adipisicing tempor ea. Fugiat ea aliquip occaecat ut commodo labore in magna laborum incididunt amet enim labore. Consectetur laborum exercitation veniam aliquip labore minim ipsum exercitation officia.\r\nEsse eu consequat dolor irure elit. Sint qui elit sint officia non incididunt sunt nulla. Labore occaecat aliquip dolor culpa aliqua irure voluptate excepteur mollit sit proident. Excepteur eu veniam eu nisi enim sit qui magna ut laboris magna. Cupidatat dolor laborum adipisicing aliqua id aliquip nostrud minim nostrud cupidatat ut quis dolore non. Id ad ea pariatur esse sint ad esse cillum.\r\nVoluptate aute laborum cupidatat non minim nulla proident. Consequat quis velit culpa proident ipsum. Enim in reprehenderit dolore dolor proident occaecat laborum sunt eiusmod adipisicing quis veniam ipsum. Magna ut cillum excepteur proident nisi cillum proident nostrud voluptate deserunt. Sit ex sint sunt eu labore cillum incididunt ad proident sint amet Lorem. In duis cupidatat in aute non fugiat occaecat minim anim Lorem sit ullamco est. Aliquip et ad enim adipisicing fugiat nulla enim.\r\nElit ipsum nulla mollit magna qui. Laborum et sint anim reprehenderit ea consectetur. Elit aliqua consequat ex nostrud in. Est excepteur pariatur id ad culpa enim elit labore commodo Lorem ipsum. Labore ad ipsum occaecat veniam in ut fugiat voluptate fugiat enim ex sit duis.\r\nExcepteur quis dolore ipsum ullamco sint consectetur Lorem. Culpa cillum minim id est. Aliquip incididunt velit exercitation culpa sint officia tempor excepteur eu.\r\n' 393 | }, 394 | { 395 | id: 2, 396 | name: 'Shelia Bray', 397 | about: 'Dolor quis duis aute excepteur ad. Aliqua aute velit excepteur voluptate labore Lorem veniam incididunt anim consequat eu. Non sint aliqua do Lorem. Esse commodo sint ullamco in tempor et est sit elit irure. Commodo ex in labore officia nulla non culpa reprehenderit in elit anim aliqua eu eiusmod.\r\nNostrud sit sunt do do. Veniam consequat laborum ullamco incididunt anim. Consequat ipsum ex laboris eu ut et enim.\r\nSint Lorem dolore duis pariatur ea amet anim. Dolore aliquip sunt exercitation labore sit deserunt enim velit labore aliqua incididunt eiusmod ipsum. Est dolore id sit nisi sint labore laborum. Consequat minim fugiat duis sint. Nulla est dolore est nostrud. Pariatur aute commodo consequat exercitation nisi elit sunt incididunt mollit.\r\nProident officia commodo anim et ut. Laboris do voluptate tempor anim commodo aliqua dolore ullamco aliqua anim cupidatat amet cupidatat. Sunt adipisicing qui quis occaecat voluptate anim ad ea enim nulla sit dolor ullamco mollit.\r\nAmet pariatur quis id consectetur anim labore occaecat aliquip incididunt tempor. Culpa veniam ut aliquip sint aute et mollit nostrud excepteur non. Eiusmod cillum reprehenderit occaecat cillum ut eiusmod culpa mollit mollit qui aliqua excepteur non. Consectetur dolore sit ad et do. Fugiat adipisicing ullamco sint ad pariatur aliqua labore adipisicing labore culpa magna consectetur nostrud. Nulla duis pariatur non ut eu tempor nisi deserunt.\r\n' 398 | }, 399 | { 400 | id: 3, 401 | name: 'Dollie Suarez', 402 | about: 'Deserunt mollit non incididunt labore ipsum veniam qui ipsum veniam excepteur consectetur quis. Ullamco amet reprehenderit qui tempor do ullamco commodo reprehenderit ut in fugiat officia sunt. Tempor reprehenderit ullamco ipsum occaecat laboris ad labore duis excepteur elit do reprehenderit ut occaecat. Consectetur veniam adipisicing mollit fugiat eu duis. Esse dolore adipisicing excepteur dolor laborum nulla.\r\nElit fugiat velit eiusmod nulla labore in mollit reprehenderit laboris. Voluptate labore ea ad eiusmod esse nostrud amet. Labore anim ex id id laboris reprehenderit.\r\nSit amet aute sit aute nostrud Lorem et ad qui pariatur et duis ad. Aute nulla culpa mollit est occaecat non laborum et cillum. Eiusmod nulla ut nostrud voluptate culpa consectetur incididunt magna ad anim. Cupidatat voluptate nisi voluptate non. Dolor deserunt culpa occaecat velit sit. Ea laboris ea aliqua deserunt consequat in incididunt ex nisi duis cupidatat ullamco nostrud nulla. Eu duis reprehenderit Lorem non irure nulla dolor commodo minim id sit.\r\nConsectetur aliquip reprehenderit ea est laborum et sint nostrud cupidatat in cillum eu. Dolore exercitation ullamco do qui laborum laborum consequat veniam tempor. Enim anim elit exercitation tempor aliquip qui amet sint eiusmod tempor sunt. Sit culpa culpa ex consectetur incididunt ea pariatur dolor ad.\r\nExcepteur amet adipisicing consequat cillum proident excepteur velit ut reprehenderit. Cupidatat consequat cillum cupidatat aliqua culpa sint elit sunt sit in. Tempor enim ex magna enim fugiat reprehenderit qui laborum.\r\n' 403 | }, 404 | { 405 | id: 4, 406 | name: 'Doris Hines', 407 | about: 'Enim irure fugiat nostrud sit in cupidatat qui. Aliquip labore dolor ea ea ea dolore est non anim esse elit excepteur. Nulla nulla velit ad aute mollit do irure minim ad. Anim deserunt velit cupidatat ipsum est commodo est dolor id aute veniam adipisicing ea enim. Aliquip duis labore cupidatat et occaecat laborum qui et. Eu aliquip adipisicing irure elit minim laborum sit sint non ea.\r\nEx laborum enim ut minim cillum deserunt magna ullamco dolore. Est mollit consectetur aliquip ad labore commodo laboris quis qui do officia cillum. Fugiat deserunt aute laboris commodo sunt do consectetur quis. Ullamco nulla sunt est Lorem incididunt nostrud nostrud nostrud.\r\nQuis aute qui id do exercitation deserunt laboris exercitation ad dolore. Occaecat exercitation ex excepteur adipisicing cillum excepteur sint dolor ut id. Incididunt velit est amet nostrud irure proident cupidatat eiusmod enim esse non in Lorem. Cillum nisi irure reprehenderit eiusmod adipisicing irure laboris eu deserunt. Duis sunt laborum incididunt est mollit anim eu proident Lorem reprehenderit quis. Quis magna deserunt excepteur aute nostrud non in nostrud enim exercitation ipsum incididunt velit. Lorem et do consequat ea dolor commodo mollit enim sunt non Lorem.\r\nDolor elit aliqua ad nostrud veniam cupidatat irure consectetur do. Qui sit est elit sunt incididunt cillum magna non et excepteur elit ullamco cupidatat. Pariatur cupidatat nisi velit ut do sint aliquip sit ullamco. Sit veniam et elit cupidatat pariatur consequat cupidatat sit elit dolor tempor in Lorem nulla. Elit in deserunt mollit quis voluptate enim proident mollit. Dolor fugiat magna reprehenderit do ullamco nisi proident. Minim pariatur laboris anim cupidatat aliquip ut pariatur.\r\nVelit nisi culpa ut esse qui adipisicing esse dolor occaecat Lorem. Nulla irure tempor occaecat dolore ullamco fugiat excepteur tempor. Aliqua quis pariatur officia sit aliqua ex minim. Eiusmod exercitation laborum Lorem eu et amet ex.\r\n' 408 | }, 409 | { 410 | id: 5, 411 | name: 'Snider Blevins', 412 | about: 'Ut proident officia proident esse cillum irure anim Lorem non officia laborum. Labore duis quis eiusmod culpa Lorem commodo esse eu laborum elit. Pariatur quis cillum incididunt consequat ex laboris elit consectetur laborum ad laboris aliquip irure.\r\nId occaecat elit cillum nisi nulla amet. Elit sint eiusmod minim dolor ad magna voluptate. Ullamco nulla dolor aliqua labore amet anim. Consequat Lorem magna est eiusmod veniam amet esse nisi exercitation laboris ex reprehenderit. Quis dolor Lorem pariatur mollit esse sunt tempor labore deserunt velit tempor dolore esse.\r\nNostrud et aliqua duis excepteur mollit qui sunt esse ad deserunt. Cupidatat nulla sit velit tempor elit cillum officia. Adipisicing laborum dolor occaecat ad reprehenderit duis sunt esse esse elit deserunt. Nulla laborum ullamco mollit minim excepteur aliquip exercitation minim nisi cupidatat adipisicing ut aliqua. Cupidatat ullamco aliquip enim dolor incididunt commodo. Ullamco voluptate ut ex quis fugiat commodo.\r\nAliqua occaecat ex deserunt consequat esse cillum aliquip occaecat officia in. Magna laborum occaecat officia dolor ipsum eiusmod dolor ullamco consectetur occaecat ut. Ea dolore dolore id elit eiusmod velit mollit commodo esse sint exercitation commodo eiusmod tempor. Consequat et qui veniam culpa. Consectetur id aute ad eiusmod magna. Ex ad cillum est occaecat in dolor eiusmod officia nisi eu.\r\nEst nostrud sint non quis proident nulla nulla aliqua deserunt veniam non reprehenderit aliqua. Reprehenderit minim incididunt magna mollit qui sint sint anim officia sit exercitation officia laboris. Est cupidatat eu aute et cillum velit sit commodo duis incididunt mollit.\r\n' 413 | }, 414 | { 415 | id: 6, 416 | name: 'Mckay Mcknight', 417 | about: 'Adipisicing et laboris in officia adipisicing proident. Do excepteur culpa reprehenderit mollit est nisi. Ad nisi nostrud Lorem aliquip excepteur nulla ex amet exercitation id deserunt reprehenderit eiusmod laboris. Aliqua ea proident adipisicing excepteur voluptate elit laboris amet.\r\nCillum proident dolor est minim mollit proident non commodo tempor duis pariatur voluptate. Culpa deserunt et nisi in et. Eu ea ipsum eu aliqua commodo duis sunt et in eu veniam laborum velit. Reprehenderit duis nulla nulla ad ut. Id et cillum amet fugiat mollit. Amet in eu amet laboris velit consectetur dolore excepteur aliqua. Commodo duis laborum eu ad Lorem ut excepteur irure culpa velit.\r\nAnim in eiusmod Lorem quis est consectetur exercitation sit voluptate. Pariatur nisi elit enim veniam quis pariatur adipisicing enim non nisi Lorem labore labore eu. Veniam sunt culpa do irure anim nisi culpa et nostrud aliqua excepteur ad excepteur in. Eiusmod exercitation esse proident ut incididunt quis commodo. Labore tempor enim aute nisi exercitation dolore non.\r\nEa nulla officia nostrud proident enim officia in eu non. Enim nisi ea ad exercitation magna veniam aute sunt voluptate in elit. Nostrud consequat labore minim irure sunt. Incididunt officia laboris exercitation culpa anim eu anim est deserunt officia do magna duis. Voluptate id dolore laboris ad mollit voluptate velit est elit do eu eu minim. Reprehenderit fugiat non sunt magna. Exercitation fugiat cupidatat consectetur est minim ad aute voluptate exercitation amet.\r\nDo reprehenderit qui sunt elit. Tempor esse non fugiat qui ea Lorem sit non fugiat cillum sint aliqua. Cillum laboris laboris non pariatur enim id enim reprehenderit aliquip non reprehenderit sunt esse Lorem.\r\n' 418 | }, 419 | { 420 | id: 7, 421 | name: 'Lauren Rocha', 422 | about: 'Non aute ex qui elit pariatur commodo sunt veniam. Labore anim mollit Lorem incididunt pariatur esse do laboris enim. Est est adipisicing ea eu ex eiusmod dolore duis commodo sint sint. Laborum qui sint in dolor deserunt est sunt dolor irure. Sint labore aute minim dolor nisi ullamco velit esse ullamco culpa esse consequat irure. Nisi esse ut dolore in et in fugiat ipsum voluptate proident aliqua minim. Occaecat eu voluptate proident ea commodo cupidatat consequat est non ea ea.\r\nVeniam mollit aliquip elit et aliquip sit proident anim ea nisi ex mollit. Et reprehenderit nisi laborum enim ut. Anim veniam proident proident pariatur proident nulla labore ad mollit ea ullamco ut deserunt. Cillum amet laborum ea amet cupidatat mollit incididunt eiusmod est ipsum sit ullamco deserunt reprehenderit. Fugiat do consectetur consectetur magna eu elit nisi aute exercitation eu laboris ad cupidatat. Aliquip elit voluptate eu incididunt occaecat sunt.\r\nAliqua quis non et ullamco amet veniam mollit culpa cillum esse occaecat qui. Sint proident tempor magna dolor. Ea aliqua irure amet ipsum fugiat officia ad in consectetur. Do esse ipsum amet ipsum adipisicing eiusmod incididunt proident magna voluptate culpa excepteur.\r\nTempor ut ullamco pariatur sunt. Aute est et enim do dolor nulla voluptate tempor dolor veniam do minim exercitation esse. Sit magna aute excepteur labore culpa proident aliquip ad ad officia. Laboris sit pariatur sit incididunt duis eiusmod consectetur culpa ad. Minim voluptate id id eu veniam ut ex dolore. Consectetur sit consectetur minim proident est et occaecat non enim enim. Laboris fugiat occaecat fugiat laborum.\r\nEst ipsum culpa consequat ipsum est consectetur proident magna excepteur. Anim sint voluptate ut enim occaecat cupidatat in consectetur. Ullamco veniam amet minim tempor labore consequat ex. Aliquip consectetur occaecat ea velit ipsum consequat eiusmod incididunt in adipisicing magna. Veniam sunt culpa magna ullamco excepteur fugiat fugiat voluptate dolor adipisicing sint sunt consectetur.\r\n' 423 | }, 424 | { 425 | id: 8, 426 | name: 'Nichole Hale', 427 | about: 'Mollit et excepteur minim cupidatat nostrud. Ullamco culpa fugiat culpa dolor qui. Proident aliqua ex labore id laborum officia aliquip ex et. Ex occaecat dolore aliquip anim ipsum amet dolore amet aute cupidatat cupidatat ea veniam. Ea ullamco dolor Lorem cupidatat dolor. Deserunt voluptate fugiat dolor nostrud.\r\nQuis proident reprehenderit mollit ea culpa amet aliquip. Lorem veniam proident deserunt anim officia pariatur eiusmod. Officia cillum in velit mollit in duis minim do veniam in aliquip mollit proident ad. Voluptate ex aliqua minim velit eu incididunt commodo enim ut aliqua consequat veniam.\r\nCommodo veniam labore culpa adipisicing ullamco laborum mollit commodo elit pariatur consequat. Pariatur ex nostrud nostrud minim cupidatat incididunt aute dolore officia quis id. Sit adipisicing ipsum sunt sit est ad. Anim ea nostrud consequat duis pariatur qui. Nostrud tempor do aliqua mollit. Ex culpa consectetur elit elit ut sit. Aute sint mollit cillum sint qui et minim adipisicing in fugiat excepteur.\r\nNostrud eu in ipsum sunt tempor. Et ullamco cillum dolor qui exercitation veniam. Tempor nisi occaecat sunt nostrud in voluptate ad cupidatat ad elit in. Nisi deserunt quis esse sunt eu cupidatat esse. Laborum labore velit dolor nostrud proident deserunt ea consectetur proident do. Consectetur nisi non sit consectetur dolor quis voluptate nisi pariatur cupidatat.\r\nElit velit culpa minim mollit eu est quis aliquip. Aliqua veniam nostrud cupidatat tempor. Eiusmod excepteur quis mollit cupidatat reprehenderit irure aliqua occaecat ex sunt et culpa.\r\n' 428 | }, 429 | { 430 | id: 9, 431 | name: 'Valencia Mcbride', 432 | about: 'Nisi sit dolor in dolor deserunt id labore reprehenderit pariatur. In ad sint cupidatat velit. Amet dolore tempor tempor est nostrud quis. Commodo est dolor ad labore voluptate enim ut et duis labore minim non velit dolore. Cupidatat nulla irure quis eu qui. Quis non nostrud quis id nisi do elit veniam ex.\r\nVoluptate aliquip consequat incididunt dolor ipsum nisi quis. Proident laboris eu adipisicing ad ut laborum. Consectetur nostrud nisi velit et aute ullamco exercitation pariatur adipisicing commodo.\r\nEu minim eu ut deserunt. Adipisicing exercitation ex est tempor non elit id pariatur amet incididunt tempor. Qui exercitation duis qui ea.\r\nOfficia tempor laboris officia Lorem sint cillum tempor mollit aliquip exercitation. Nisi deserunt aliquip et aliquip non adipisicing minim laboris. In ut aute quis eu. Occaecat ut velit amet laborum qui. Amet do enim Lorem nisi.\r\nSit ipsum duis Lorem ea proident reprehenderit fugiat qui in veniam est labore veniam. Nisi non anim ullamco labore. Ut commodo id incididunt pariatur sunt et. Commodo qui aliquip elit irure eiusmod velit et ullamco. Adipisicing nostrud Lorem aute sit amet incididunt veniam officia aliquip. Culpa esse amet excepteur id sunt dolor elit reprehenderit velit ex ut quis adipisicing nostrud. Irure irure reprehenderit do quis sit non.\r\n' 433 | } 434 | ], 435 | greeting: 'Hello, Lara Crane! You have 9 unread messages.', 436 | favoriteFruit: 'apple' 437 | }; 438 | --------------------------------------------------------------------------------