├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .snyk ├── .travis.yml ├── LICENSE ├── README.md ├── example ├── app.es5.js └── app.js ├── jest.config.js ├── jest └── redisClient.js ├── lib ├── SequelizeRedisModel.js └── index.js ├── package-lock.json ├── package.json └── src ├── SequelizeRedisModel.js ├── __tests__ └── sequelize-redis.test.js └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"], 3 | "plugins": [ 4 | [ 5 | "@babel/plugin-transform-runtime", 6 | { 7 | "corejs": false, 8 | "helpers": true, 9 | "regenerator": true, 10 | "useESModules": false 11 | } 12 | ] 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib/** 2 | coverage/** 3 | 4 | *.es5.js 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-airbnb-base", 3 | "env": { 4 | "jest/globals": true, 5 | "node": true, 6 | "mocha": true 7 | }, 8 | "rules": { 9 | "import/no-extraneous-dependencies": ["error", {"devDependencies": ["src/__tests__/**/*.js"]}], 10 | "max-len": ["error", { 11 | "code": 120, 12 | "ignoreComments": true, 13 | "ignoreTrailingComments": true, 14 | "ignoreUrls": true, 15 | "ignoreStrings": true, 16 | "ignoreTemplateLiterals": true, 17 | "ignoreRegExpLiterals": true 18 | }], 19 | }, 20 | "parser": "babel-eslint", 21 | "parserOptions": { 22 | "ecmaVersion": 2017, 23 | "sourceType": "module", 24 | "ecmaFeatures": { 25 | "legacyDecorators": true, 26 | "jsx": true, 27 | "experimentalObjectRestSpread": true 28 | } 29 | }, 30 | "plugins": [ 31 | "import", "jest" 32 | ], 33 | "globals": { 34 | "__DEVELOPMENT__": true 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | 61 | .idea 62 | .DS_Store 63 | -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.13.1 3 | # ignores vulnerabilities until expiry date; change duration by modifying expiry date 4 | ignore: 5 | 'npm:braces:20180219': 6 | - jest > jest-cli > micromatch > braces: 7 | reason: None given 8 | expires: '2018-12-14T18:21:58.159Z' 9 | - expect > jest-message-util > micromatch > braces: 10 | reason: None given 11 | expires: '2018-12-14T18:21:58.159Z' 12 | - jest > jest-cli > jest-runtime > micromatch > braces: 13 | reason: None given 14 | expires: '2018-12-14T18:21:58.159Z' 15 | - jest > jest-cli > jest-haste-map > micromatch > braces: 16 | reason: None given 17 | expires: '2018-12-14T18:21:58.159Z' 18 | - jest > jest-cli > jest-config > micromatch > braces: 19 | reason: None given 20 | expires: '2018-12-14T18:21:58.159Z' 21 | - babel-jest > babel-plugin-istanbul > test-exclude > micromatch > braces: 22 | reason: None given 23 | expires: '2018-12-14T18:21:58.159Z' 24 | - jest > jest-cli > jest-message-util > micromatch > braces: 25 | reason: None given 26 | expires: '2018-12-14T18:21:58.159Z' 27 | - babel-cli > chokidar > anymatch > micromatch > braces: 28 | reason: None given 29 | expires: '2018-12-14T18:21:58.159Z' 30 | - jest > jest-cli > jest-runner > jest-runtime > micromatch > braces: 31 | reason: None given 32 | expires: '2018-12-14T18:21:58.159Z' 33 | - jest > jest-cli > jest-runner > jest-haste-map > micromatch > braces: 34 | reason: None given 35 | expires: '2018-12-14T18:21:58.159Z' 36 | - jest > jest-cli > jest-runtime > jest-haste-map > micromatch > braces: 37 | reason: None given 38 | expires: '2018-12-14T18:21:58.159Z' 39 | - jest > jest-cli > jest-runner > jest-config > micromatch > braces: 40 | reason: None given 41 | expires: '2018-12-14T18:21:58.159Z' 42 | - jest > jest-cli > jest-runtime > jest-config > micromatch > braces: 43 | reason: None given 44 | expires: '2018-12-14T18:21:58.159Z' 45 | - jest > jest-cli > jest-runner > jest-message-util > micromatch > braces: 46 | reason: None given 47 | expires: '2018-12-14T18:21:58.159Z' 48 | - jest > jest-cli > jest-util > jest-message-util > micromatch > braces: 49 | reason: None given 50 | expires: '2018-12-14T18:21:58.160Z' 51 | - jest > jest-cli > jest-runtime > jest-message-util > micromatch > braces: 52 | reason: None given 53 | expires: '2018-12-14T18:21:58.160Z' 54 | - jest > jest-cli > jest-snapshot > jest-message-util > micromatch > braces: 55 | reason: None given 56 | expires: '2018-12-14T18:21:58.160Z' 57 | - jest > jest-cli > jest-runner > jest-runtime > jest-config > micromatch > braces: 58 | reason: None given 59 | expires: '2018-12-14T18:21:58.160Z' 60 | - jest > jest-cli > jest-runtime > jest-snapshot > jest-message-util > micromatch > braces: 61 | reason: None given 62 | expires: '2018-12-14T18:21:58.160Z' 63 | - jest > jest-cli > jest-runner > jest-jasmine2 > jest-message-util > micromatch > braces: 64 | reason: None given 65 | expires: '2018-12-14T18:21:58.160Z' 66 | - jest > jest-cli > jest-runner > jest-runtime > jest-message-util > micromatch > braces: 67 | reason: None given 68 | expires: '2018-12-14T18:21:58.160Z' 69 | - jest > jest-cli > jest-resolve-dependencies > jest-snapshot > jest-message-util > micromatch > braces: 70 | reason: None given 71 | expires: '2018-12-14T18:21:58.160Z' 72 | - jest > jest-cli > jest-runner > jest-util > jest-message-util > micromatch > braces: 73 | reason: None given 74 | expires: '2018-12-14T18:21:58.160Z' 75 | - jest > jest-cli > jest-runtime > babel-plugin-istanbul > test-exclude > micromatch > braces: 76 | reason: None given 77 | expires: '2018-12-14T18:21:58.160Z' 78 | - jest > jest-cli > jest-runtime > jest-util > jest-message-util > micromatch > braces: 79 | reason: None given 80 | expires: '2018-12-14T18:21:58.160Z' 81 | - jest > jest-cli > jest-config > jest-util > jest-message-util > micromatch > braces: 82 | reason: None given 83 | expires: '2018-12-14T18:21:58.160Z' 84 | - jest > jest-cli > jest-runner > jest-runtime > jest-haste-map > micromatch > braces: 85 | reason: None given 86 | expires: '2018-12-14T18:21:58.160Z' 87 | - jest > jest-cli > jest-environment-jsdom > jest-util > jest-message-util > micromatch > braces: 88 | reason: None given 89 | expires: '2018-12-14T18:21:58.160Z' 90 | - jest > jest-cli > jest-config > jest-jasmine2 > jest-message-util > micromatch > braces: 91 | reason: None given 92 | expires: '2018-12-14T18:21:58.160Z' 93 | - jest > jest-cli > jest-runtime > jest-config > jest-jasmine2 > jest-message-util > micromatch > braces: 94 | reason: None given 95 | expires: '2018-12-14T18:21:58.160Z' 96 | - jest > jest-cli > jest-runner > jest-config > jest-jasmine2 > jest-message-util > micromatch > braces: 97 | reason: None given 98 | expires: '2018-12-14T18:21:58.160Z' 99 | - jest > jest-cli > jest-runner > jest-config > jest-util > jest-message-util > micromatch > braces: 100 | reason: None given 101 | expires: '2018-12-14T18:21:58.161Z' 102 | - jest > jest-cli > jest-runner > jest-runtime > jest-util > jest-message-util > micromatch > braces: 103 | reason: None given 104 | expires: '2018-12-14T18:21:58.161Z' 105 | - jest > jest-cli > jest-config > jest-environment-jsdom > jest-util > jest-message-util > micromatch > braces: 106 | reason: None given 107 | expires: '2018-12-14T18:21:58.161Z' 108 | - jest > jest-cli > jest-runner > jest-jasmine2 > expect > jest-message-util > micromatch > braces: 109 | reason: None given 110 | expires: '2018-12-14T18:21:58.161Z' 111 | - jest > jest-cli > jest-config > jest-environment-node > jest-util > jest-message-util > micromatch > braces: 112 | reason: None given 113 | expires: '2018-12-14T18:21:58.161Z' 114 | - jest > jest-cli > jest-runner > jest-runtime > babel-plugin-istanbul > test-exclude > micromatch > braces: 115 | reason: None given 116 | expires: '2018-12-14T18:21:58.161Z' 117 | - jest > jest-cli > jest-config > jest-jasmine2 > jest-snapshot > jest-message-util > micromatch > braces: 118 | reason: None given 119 | expires: '2018-12-14T18:21:58.161Z' 120 | - jest > jest-cli > jest-runner > jest-jasmine2 > jest-snapshot > jest-message-util > micromatch > braces: 121 | reason: None given 122 | expires: '2018-12-14T18:21:58.161Z' 123 | - jest > jest-cli > jest-config > jest-jasmine2 > jest-util > jest-message-util > micromatch > braces: 124 | reason: None given 125 | expires: '2018-12-14T18:21:58.161Z' 126 | - jest > jest-cli > jest-config > babel-jest > babel-plugin-istanbul > test-exclude > micromatch > braces: 127 | reason: None given 128 | expires: '2018-12-14T18:21:58.162Z' 129 | - jest > jest-cli > jest-config > jest-jasmine2 > expect > jest-message-util > micromatch > braces: 130 | reason: None given 131 | expires: '2018-12-14T18:21:58.162Z' 132 | - jest > jest-cli > jest-runner > jest-jasmine2 > jest-util > jest-message-util > micromatch > braces: 133 | reason: None given 134 | expires: '2018-12-14T18:21:58.162Z' 135 | - jest > jest-cli > jest-runtime > jest-config > jest-util > jest-message-util > micromatch > braces: 136 | reason: None given 137 | expires: '2018-12-14T18:21:58.162Z' 138 | - jest > jest-cli > jest-runner > jest-runtime > jest-snapshot > jest-message-util > micromatch > braces: 139 | reason: None given 140 | expires: '2018-12-14T18:21:58.162Z' 141 | - jest > jest-cli > jest-runner > jest-config > jest-jasmine2 > expect > jest-message-util > micromatch > braces: 142 | reason: None given 143 | expires: '2018-12-14T18:21:58.162Z' 144 | - jest > jest-cli > jest-runtime > jest-config > jest-environment-jsdom > jest-util > jest-message-util > micromatch > braces: 145 | reason: None given 146 | expires: '2018-12-14T18:21:58.162Z' 147 | - jest > jest-cli > jest-runner > jest-runtime > jest-config > jest-util > jest-message-util > micromatch > braces: 148 | reason: None given 149 | expires: '2018-12-14T18:21:58.162Z' 150 | - jest > jest-cli > jest-runtime > jest-config > jest-environment-node > jest-util > jest-message-util > micromatch > braces: 151 | reason: None given 152 | expires: '2018-12-14T18:21:58.162Z' 153 | - jest > jest-cli > jest-runner > jest-config > jest-environment-node > jest-util > jest-message-util > micromatch > braces: 154 | reason: None given 155 | expires: '2018-12-14T18:21:58.162Z' 156 | - jest > jest-cli > jest-runner > jest-config > jest-environment-jsdom > jest-util > jest-message-util > micromatch > braces: 157 | reason: None given 158 | expires: '2018-12-14T18:21:58.162Z' 159 | - jest > jest-cli > jest-runtime > jest-config > jest-jasmine2 > jest-util > jest-message-util > micromatch > braces: 160 | reason: None given 161 | expires: '2018-12-14T18:21:58.162Z' 162 | - jest > jest-cli > jest-runner > jest-runtime > jest-config > jest-jasmine2 > jest-message-util > micromatch > braces: 163 | reason: None given 164 | expires: '2018-12-14T18:21:58.162Z' 165 | - jest > jest-cli > jest-runner > jest-config > jest-jasmine2 > jest-util > jest-message-util > micromatch > braces: 166 | reason: None given 167 | expires: '2018-12-14T18:21:58.162Z' 168 | - jest > jest-cli > jest-runtime > jest-config > babel-jest > babel-plugin-istanbul > test-exclude > micromatch > braces: 169 | reason: None given 170 | expires: '2018-12-14T18:21:58.162Z' 171 | - jest > jest-cli > jest-runtime > jest-config > jest-jasmine2 > expect > jest-message-util > micromatch > braces: 172 | reason: None given 173 | expires: '2018-12-14T18:21:58.162Z' 174 | - jest > jest-cli > jest-runner > jest-config > babel-jest > babel-plugin-istanbul > test-exclude > micromatch > braces: 175 | reason: None given 176 | expires: '2018-12-14T18:21:58.162Z' 177 | - jest > jest-cli > jest-runtime > jest-config > jest-jasmine2 > jest-snapshot > jest-message-util > micromatch > braces: 178 | reason: None given 179 | expires: '2018-12-14T18:21:58.162Z' 180 | - jest > jest-cli > jest-runner > jest-config > jest-jasmine2 > jest-snapshot > jest-message-util > micromatch > braces: 181 | reason: None given 182 | expires: '2018-12-14T18:21:58.162Z' 183 | - jest > jest-cli > jest-runner > jest-runtime > jest-config > jest-environment-jsdom > jest-util > jest-message-util > micromatch > braces: 184 | reason: None given 185 | expires: '2018-12-14T18:21:58.163Z' 186 | - jest > jest-cli > jest-runner > jest-runtime > jest-config > jest-environment-node > jest-util > jest-message-util > micromatch > braces: 187 | reason: None given 188 | expires: '2018-12-14T18:21:58.163Z' 189 | - jest > jest-cli > jest-runner > jest-runtime > jest-config > jest-jasmine2 > jest-util > jest-message-util > micromatch > braces: 190 | reason: None given 191 | expires: '2018-12-14T18:21:58.163Z' 192 | - jest > jest-cli > jest-runner > jest-runtime > jest-config > babel-jest > babel-plugin-istanbul > test-exclude > micromatch > braces: 193 | reason: None given 194 | expires: '2018-12-14T18:21:58.163Z' 195 | - jest > jest-cli > jest-runner > jest-runtime > jest-config > jest-jasmine2 > expect > jest-message-util > micromatch > braces: 196 | reason: None given 197 | expires: '2018-12-14T18:21:58.163Z' 198 | - jest > jest-cli > jest-runner > jest-runtime > jest-config > jest-jasmine2 > jest-snapshot > jest-message-util > micromatch > braces: 199 | reason: None given 200 | expires: '2018-12-14T18:21:58.163Z' 201 | 'npm:chownr:20180731': 202 | - babel-cli > chokidar > fsevents > node-pre-gyp > tar > chownr: 203 | reason: None given 204 | expires: '2018-12-14T18:21:58.163Z' 205 | - jest > jest-cli > jest-haste-map > sane > fsevents > node-pre-gyp > tar > chownr: 206 | reason: None given 207 | expires: '2018-12-14T18:21:58.163Z' 208 | - jest > jest-cli > jest-runtime > jest-haste-map > sane > fsevents > node-pre-gyp > tar > chownr: 209 | reason: None given 210 | expires: '2018-12-14T18:21:58.163Z' 211 | - jest > jest-cli > jest-runner > jest-haste-map > sane > fsevents > node-pre-gyp > tar > chownr: 212 | reason: None given 213 | expires: '2018-12-14T18:21:58.163Z' 214 | - jest > jest-cli > jest-runner > jest-runtime > jest-haste-map > sane > fsevents > node-pre-gyp > tar > chownr: 215 | reason: None given 216 | expires: '2018-12-14T18:21:58.163Z' 217 | 'npm:mem:20180117': 218 | - jest > jest-cli > yargs > os-locale > mem: 219 | reason: None given 220 | expires: '2018-12-14T18:21:58.163Z' 221 | - jest > jest-cli > jest-runtime > yargs > os-locale > mem: 222 | reason: None given 223 | expires: '2018-12-14T18:21:58.163Z' 224 | - jest > jest-cli > jest-runner > jest-runtime > yargs > os-locale > mem: 225 | reason: None given 226 | expires: '2018-12-14T18:21:58.163Z' 227 | patch: {} 228 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | services: 4 | - redis-server 5 | - mysql 6 | 7 | before_install: 8 | - mysql -e 'CREATE DATABASE tests;' 9 | 10 | node_js: 11 | - "6" 12 | - "8" 13 | 14 | cache: 15 | directories: 16 | - node_modules 17 | 18 | script: 19 | - npm run lint 20 | - npm run test 21 | - npm run build 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Idan Gozlan 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sequelize-redis 2 | [![Build Status](https://travis-ci.org/idangozlan/sequelize-redis.svg?branch=master)](https://travis-ci.org/idangozlan/sequelize-redis) 3 | [![codecov.io Code Coverage](https://img.shields.io/codecov/c/github/idangozlan/sequelize-redis.svg?maxAge=2592000)](https://codecov.io/github/idangozlan/sequelize-redis?branch=master) 4 | [![Known Vulnerabilities](https://snyk.io/test/github/idangozlan/sequelize-redis/badge.svg)](https://snyk.io/test/github/idangozlan/sequelize-redis) 5 | 6 | A semi-automatic caching wrapper for Sequelize v4 NodeJS framework 7 | 8 | ### Installation 9 | 10 | ``` 11 | 12 | npm install sequelize-redis 13 | 14 | ``` 15 | ### requirements 16 | - Sequelize V4 17 | - [Redis Client (promisified with bluebird) ](https://github.com/NodeRedis/node_redis#promises) 18 | 19 | ## Usage 20 | 1. Init our Sequelize cache manager: 21 | ``` 22 | import SequelizeRedis from 'sequelize-redis'; 23 | import redis from 'redis'; 24 | import bluebird from 'bluebird'; 25 | 26 | // Let's promisify Redis 27 | bluebird.promisifyAll(redis.RedisClient.prototype); 28 | bluebird.promisifyAll(redis.Multi.prototype); 29 | 30 | // Define your redisClient 31 | const redisClient = redis.createClient({ /* Redis configuration comes here */ }); 32 | 33 | // Let's start 34 | const sequelizeRedis = new SequelizeRedis(redisClient); 35 | ``` 36 | 37 | 2. Wrap our the Sequelize original model: 38 | ``` 39 | // models.User refers to model of sequelize 40 | const User = sequelizeRedis.getModel(models.User, { ttl: 60 * 60 * 24 }); 41 | ``` 42 | The second argument of `getModel` is optional: 43 | 44 | | Key | Description | Default value | 45 | |-----|-----------------------------|---------------| 46 | | ttl | Defines cache TTL (seconds) | null | 47 | 48 | 49 | 3. Then we can start use the model wrapper: 50 | ``` 51 | const userUUID = '75292c75-4c7a-4a11-92ac-57f929f50e23'; 52 | const userCacheKey = `user_${userUUID}`; 53 | // We can use the default sequelize methods by adding suffix of "Cached" 54 | // for example, findbyPkCached: 55 | const [user, cacheHit] = await User.findbyPkCached(userCacheKey, userUUID); 56 | // We can also use the non cached methods (original methods) 57 | const user = await User.findbyPk(userUUID); 58 | ``` 59 | 60 | Results of Cached methods (for ex. `findbyPkCached`) will be array with following arguments: 61 | 1. Sequelize response (same as on original method) 62 | 2. Cache hit indication (`true` / `false`) 63 | 64 | 65 | Supported Methods: 66 | `find` 67 | `findOne` 68 | `findAll` 69 | `findAndCount` 70 | `findAndCountAll` 71 | `findbyPk` 72 | `all` 73 | `min` 74 | `max` 75 | `sum` 76 | `count` 77 | 78 | ## Cache Invalidation 79 | Just use regular Redis API: 80 | ``` 81 | redisClient.del('SampleKey'); 82 | ``` 83 | 84 | ## Contribution 85 | Feel free to contribute and submit issues. 86 | 87 | #### PR 88 | Please make sure that your code is linted and getting build successfully 89 | 90 | 91 | #### Thanks 92 | Inspired by `rfink/sequelize-redis-cache/` 93 | 94 | ### License 95 | MIT (Idan Gozlan) 96 | -------------------------------------------------------------------------------- /example/app.es5.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); 4 | 5 | var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator")); 6 | 7 | var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray")); 8 | 9 | var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator")); 10 | 11 | /* eslint-disable no-console */ 12 | var bluebird = require('bluebird'); 13 | 14 | var redis = require('redis'); 15 | 16 | var Sequelize = require('sequelize'); 17 | 18 | var SequelizeRedis = require('..'); 19 | /* Promisify Redis */ 20 | 21 | 22 | var redisClient = redis.createClient(); 23 | bluebird.promisifyAll(redisClient); 24 | /* Create new Sequelize Redis instance */ 25 | 26 | var sequelizeRedis = new SequelizeRedis(redisClient); 27 | var sequelize = new Sequelize('test_db', 'root', '1234', { 28 | host: 'localhost', 29 | dialect: 'mysql', 30 | logging: false, 31 | pool: { 32 | max: 5, 33 | min: 0, 34 | acquire: 30000, 35 | idle: 10000 36 | } 37 | }); 38 | var User = sequelize.define('user', { 39 | uuid: { 40 | type: Sequelize.UUID, 41 | primaryKey: true 42 | }, 43 | username: Sequelize.STRING, 44 | birthday: Sequelize.DATE 45 | }); 46 | var SRUser = sequelizeRedis.getModel(User, { 47 | ttl: 60 * 60 * 24 48 | }); 49 | var userUUID = '75292c75-4c7a-4a11-92ac-57f929f50e23'; 50 | var userCacheKey = "user_".concat(userUUID); 51 | sequelize.sync({ 52 | force: true 53 | }).then( 54 | /*#__PURE__*/ 55 | (0, _asyncToGenerator2.default)( 56 | /*#__PURE__*/ 57 | _regenerator.default.mark(function _callee() { 58 | var _ref2, _ref3, resUser, cacheHit, orgUser; 59 | 60 | return _regenerator.default.wrap(function _callee$(_context) { 61 | while (1) { 62 | switch (_context.prev = _context.next) { 63 | case 0: 64 | _context.next = 2; 65 | return User.create({ 66 | uuid: userUUID, 67 | username: 'idangozlan', 68 | birthday: new Date(1980, 6, 20) 69 | }); 70 | 71 | case 2: 72 | _context.next = 4; 73 | return SRUser.findByPkCached(userCacheKey, userUUID); 74 | 75 | case 4: 76 | _ref2 = _context.sent; 77 | _ref3 = (0, _slicedToArray2.default)(_ref2, 2); 78 | resUser = _ref3[0]; 79 | cacheHit = _ref3[1]; 80 | console.log('Sequelize Redis Model:', resUser.toJSON(), cacheHit); // We can also use the non cached methods (original methods) 81 | 82 | _context.next = 11; 83 | return User.findByPk(userUUID); 84 | 85 | case 11: 86 | orgUser = _context.sent; 87 | console.log('Original Model:', orgUser.toJSON()); 88 | _context.next = 15; 89 | return sequelize.close(); 90 | 91 | case 15: 92 | _context.next = 17; 93 | return redisClient.quit(); 94 | 95 | case 17: 96 | case "end": 97 | return _context.stop(); 98 | } 99 | } 100 | }, _callee, this); 101 | }))); 102 | -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const bluebird = require('bluebird'); 3 | const redis = require('redis'); 4 | const Sequelize = require('sequelize'); 5 | const SequelizeRedis = require('..'); 6 | 7 | /* Promisify Redis */ 8 | const redisClient = redis.createClient(); 9 | bluebird.promisifyAll(redisClient); 10 | 11 | /* Create new Sequelize Redis instance */ 12 | const sequelizeRedis = new SequelizeRedis(redisClient); 13 | 14 | 15 | const sequelize = new Sequelize('test_db', 'root', '1234', { 16 | host: 'localhost', 17 | dialect: 'mysql', 18 | logging: false, 19 | 20 | pool: { 21 | max: 5, 22 | min: 0, 23 | acquire: 30000, 24 | idle: 10000, 25 | }, 26 | }); 27 | 28 | const User = sequelize.define('user', { 29 | uuid: { 30 | type: Sequelize.UUID, 31 | primaryKey: true, 32 | }, 33 | username: Sequelize.STRING, 34 | birthday: Sequelize.DATE, 35 | }); 36 | 37 | const SRUser = sequelizeRedis.getModel(User, { ttl: 60 * 60 * 24 }); 38 | 39 | const userUUID = '75292c75-4c7a-4a11-92ac-57f929f50e23'; 40 | const userCacheKey = `user_${userUUID}`; 41 | 42 | sequelize.sync({ force: true }) 43 | .then(async () => { 44 | await User.create({ 45 | uuid: userUUID, 46 | username: 'idangozlan', 47 | birthday: new Date(1980, 6, 20), 48 | }); 49 | 50 | // We can use the default sequelize methods by adding suffix of "Cached" 51 | // for example, findByIdCached: 52 | const [resUser, cacheHit] = await SRUser.findByPkCached(userCacheKey, userUUID); 53 | console.log('Sequelize Redis Model:', resUser.toJSON(), cacheHit); 54 | 55 | 56 | // We can also use the non cached methods (original methods) 57 | const orgUser = await User.findByPk(userUUID); 58 | console.log('Original Model:', orgUser.toJSON()); 59 | 60 | await sequelize.close(); 61 | await redisClient.quit(); 62 | }); 63 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverage: true, 3 | verbose: true, 4 | testEnvironment: 'node', 5 | modulePathIgnorePatterns: ['/lib/'], 6 | }; 7 | -------------------------------------------------------------------------------- /jest/redisClient.js: -------------------------------------------------------------------------------- 1 | import bluebird from 'bluebird'; 2 | import redis from 'redis-mock'; // eslint-disable-line import/no-extraneous-dependencies 3 | 4 | const redisClient = redis.createClient(); 5 | 6 | bluebird.promisifyAll(redisClient); 7 | 8 | export default redisClient; 9 | -------------------------------------------------------------------------------- /lib/SequelizeRedisModel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); 4 | 5 | var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof")); 6 | 7 | var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator")); 8 | 9 | var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator")); 10 | 11 | var _objectSpread2 = _interopRequireDefault(require("@babel/runtime/helpers/objectSpread")); 12 | 13 | var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck")); 14 | 15 | var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass")); 16 | 17 | var _require = require('json-buffer'), 18 | stringify = _require.stringify, 19 | parse = _require.parse; 20 | 21 | var methods = ['find', 'findOne', 'findAll', 'findAndCount', 'findAndCountAll', 'findById', 'findByPk', 'all', 'min', 'max', 'sum', 'count']; 22 | 23 | module.exports = 24 | /*#__PURE__*/ 25 | function () { 26 | function SequelizeRedisModel(model, redisPromisifiedClient) { 27 | var _this = this; 28 | 29 | var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 30 | (0, _classCallCheck2.default)(this, SequelizeRedisModel); 31 | this.options = (0, _objectSpread2.default)({}, options); 32 | this.model = model; 33 | this.redisClient = redisPromisifiedClient; 34 | methods.forEach(function (method) { 35 | _this["".concat(method, "Cached")] = 36 | /*#__PURE__*/ 37 | function () { 38 | var _ref = (0, _asyncToGenerator2.default)( 39 | /*#__PURE__*/ 40 | _regenerator.default.mark(function _callee(cacheKey) { 41 | var _len, 42 | args, 43 | _key, 44 | _args = arguments; 45 | 46 | return _regenerator.default.wrap(function _callee$(_context) { 47 | while (1) { 48 | switch (_context.prev = _context.next) { 49 | case 0: 50 | for (_len = _args.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { 51 | args[_key - 1] = _args[_key]; 52 | } 53 | 54 | return _context.abrupt("return", _this.run.apply(_this, [cacheKey, method].concat(args))); 55 | 56 | case 2: 57 | case "end": 58 | return _context.stop(); 59 | } 60 | } 61 | }, _callee, this); 62 | })); 63 | 64 | return function (_x) { 65 | return _ref.apply(this, arguments); 66 | }; 67 | }(); 68 | 69 | _this[method] = 70 | /*#__PURE__*/ 71 | (0, _asyncToGenerator2.default)( 72 | /*#__PURE__*/ 73 | _regenerator.default.mark(function _callee2() { 74 | var _this$model; 75 | 76 | var _args2 = arguments; 77 | return _regenerator.default.wrap(function _callee2$(_context2) { 78 | while (1) { 79 | switch (_context2.prev = _context2.next) { 80 | case 0: 81 | return _context2.abrupt("return", (_this$model = _this.model)[method].apply(_this$model, _args2)); 82 | 83 | case 1: 84 | case "end": 85 | return _context2.stop(); 86 | } 87 | } 88 | }, _callee2, this); 89 | })); 90 | }); 91 | } 92 | 93 | (0, _createClass2.default)(SequelizeRedisModel, [{ 94 | key: "run", 95 | value: function () { 96 | var _run = (0, _asyncToGenerator2.default)( 97 | /*#__PURE__*/ 98 | _regenerator.default.mark(function _callee3(cacheKey, method) { 99 | var _this2 = this, 100 | _this$model2; 101 | 102 | var cached, 103 | _len2, 104 | args, 105 | _key2, 106 | parsed, 107 | _result, 108 | queryOptions, 109 | buildOptions, 110 | result, 111 | toCache, 112 | _args3 = arguments; 113 | 114 | return _regenerator.default.wrap(function _callee3$(_context3) { 115 | while (1) { 116 | switch (_context3.prev = _context3.next) { 117 | case 0: 118 | if (methods.includes(method)) { 119 | _context3.next = 2; 120 | break; 121 | } 122 | 123 | throw new Error('Unsupported method'); 124 | 125 | case 2: 126 | if (this.model[method]) { 127 | _context3.next = 4; 128 | break; 129 | } 130 | 131 | throw new Error('Unsupported Method by Sequelize model'); 132 | 133 | case 4: 134 | _context3.prev = 4; 135 | _context3.next = 7; 136 | return this.redisClient.getAsync(cacheKey); 137 | 138 | case 7: 139 | cached = _context3.sent; 140 | _context3.next = 13; 141 | break; 142 | 143 | case 10: 144 | _context3.prev = 10; 145 | _context3.t0 = _context3["catch"](4); 146 | throw new Error("Cant get cached object from Redis ".concat(_context3.t0.message)); 147 | 148 | case 13: 149 | for (_len2 = _args3.length, args = new Array(_len2 > 2 ? _len2 - 2 : 0), _key2 = 2; _key2 < _len2; _key2++) { 150 | args[_key2 - 2] = _args3[_key2]; 151 | } 152 | 153 | if (!cached) { 154 | _context3.next = 31; 155 | break; 156 | } 157 | 158 | _context3.prev = 15; 159 | parsed = parse(cached); 160 | _context3.next = 22; 161 | break; 162 | 163 | case 19: 164 | _context3.prev = 19; 165 | _context3.t1 = _context3["catch"](15); 166 | throw new Error("Cant parse JSON of cached model's object: ".concat(_context3.t1.message)); 167 | 168 | case 22: 169 | _context3.prev = 22; 170 | // console.log('From Cache'); 171 | queryOptions = args[0]; 172 | 173 | if (queryOptions && !!queryOptions.raw) { 174 | _result = parsed; 175 | } else if (parsed.rows) { 176 | _result = (0, _objectSpread2.default)({}, parsed, { 177 | rows: parsed.rows.map(function (parsedRow) { 178 | return _this2.model.build(parsedRow); 179 | }) 180 | }); 181 | } else if (typeof parsed === 'number') { 182 | _result = parsed; 183 | } else if (queryOptions) { 184 | buildOptions = { 185 | raw: !!queryOptions.raw, 186 | isNewRecord: !!queryOptions.isNewRecord 187 | }; 188 | 189 | if (queryOptions.include) { 190 | buildOptions.include = queryOptions.include; 191 | } 192 | 193 | _result = this.model.build(parsed, buildOptions); 194 | } else { 195 | _result = this.model.build(parsed); 196 | } 197 | 198 | return _context3.abrupt("return", [_result, true]); 199 | 200 | case 28: 201 | _context3.prev = 28; 202 | _context3.t2 = _context3["catch"](22); 203 | throw new Error("Cant build model from cached JSON: ".concat(_context3.t2.message)); 204 | 205 | case 31: 206 | _context3.next = 33; 207 | return (_this$model2 = this.model)[method].apply(_this$model2, args); 208 | 209 | case 33: 210 | result = _context3.sent; 211 | 212 | if (result) { 213 | _context3.next = 36; 214 | break; 215 | } 216 | 217 | return _context3.abrupt("return", [null, false]); 218 | 219 | case 36: 220 | if (!(Array.isArray(result) || result.rows || typeof result === 'number')) { 221 | _context3.next = 40; 222 | break; 223 | } 224 | 225 | // Array for findAll, result.rows for findAndCountAll, typeof number for count/max/sum/etc 226 | toCache = result; 227 | _context3.next = 45; 228 | break; 229 | 230 | case 40: 231 | if (!result.toString().includes('[object SequelizeInstance')) { 232 | _context3.next = 44; 233 | break; 234 | } 235 | 236 | toCache = result; 237 | _context3.next = 45; 238 | break; 239 | 240 | case 44: 241 | throw new Error("Unkown result type: ".concat((0, _typeof2.default)(result))); 242 | 243 | case 45: 244 | this.redisClient.set(cacheKey, stringify(toCache)); 245 | 246 | if (this.options.ttl) { 247 | this.redisClient.expire(cacheKey, this.options.ttl); 248 | } 249 | 250 | return _context3.abrupt("return", [result, false]); 251 | 252 | case 48: 253 | case "end": 254 | return _context3.stop(); 255 | } 256 | } 257 | }, _callee3, this, [[4, 10], [15, 19], [22, 28]]); 258 | })); 259 | 260 | function run(_x2, _x3) { 261 | return _run.apply(this, arguments); 262 | } 263 | 264 | return run; 265 | }() 266 | }]); 267 | return SequelizeRedisModel; 268 | }(); -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); 4 | 5 | var _objectSpread2 = _interopRequireDefault(require("@babel/runtime/helpers/objectSpread")); 6 | 7 | var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck")); 8 | 9 | var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass")); 10 | 11 | var SequelizeRedisModel = require('./SequelizeRedisModel'); 12 | 13 | module.exports = 14 | /*#__PURE__*/ 15 | function () { 16 | function SequelizeRedis(redisClient) { 17 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 18 | (0, _classCallCheck2.default)(this, SequelizeRedis); 19 | this.redisClient = redisClient; 20 | this.options = options; 21 | } 22 | 23 | (0, _createClass2.default)(SequelizeRedis, [{ 24 | key: "getModel", 25 | value: function getModel(model) { 26 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 27 | return new SequelizeRedisModel(model, this.redisClient, (0, _objectSpread2.default)({}, this.options, options)); 28 | } 29 | }]); 30 | return SequelizeRedis; 31 | }(); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sequelize-redis", 3 | "author": "Idan Gozlan (http://github.com/idangozlan)", 4 | "description": "A semi-automatic caching wrapper for Sequelize NodeJS framework", 5 | "homepage": "https://github.com/idangozlan/sequelize-redis", 6 | "license": "MIT", 7 | "version": "1.0.12", 8 | "main": "lib/index.js", 9 | "dependencies": { 10 | "@babel/runtime": "^7.2.0", 11 | "bluebird": "^3.5.3", 12 | "jest": "^24.0.0", 13 | "json-buffer": "^3.0.1", 14 | "mysql2": "^1.6.4", 15 | "redis": "^2.8.0", 16 | "sequelize": "^4.42.0" 17 | }, 18 | "devDependencies": { 19 | "@babel/cli": "^7.2.3", 20 | "@babel/core": "^7.2.2", 21 | "@babel/plugin-transform-runtime": "^7.2.0", 22 | "@babel/preset-env": "^7.2.3", 23 | "babel-core": "^7.0.0-bridge.0", 24 | "babel-eslint": "^10.0.1", 25 | "better-npm-run": "0.1.1", 26 | "dotenv": "^6.2.0", 27 | "eslint": "5.12.0", 28 | "eslint-config-airbnb-base": "13.1.0", 29 | "eslint-plugin-import": "2.14.0", 30 | "eslint-plugin-jest": "^22.1.2", 31 | "eslint-plugin-jsx-a11y": "6.1.2", 32 | "husky": "^1.3.1", 33 | "jest-environment-node": "^23.4.0", 34 | "redis-mock": "^0.42.0", 35 | "rimraf": "^2.6.3" 36 | }, 37 | "repository": { 38 | "type": "git", 39 | "url": "git+ssh://git@github.com/idangozlan/sequelize-redis.git" 40 | }, 41 | "scripts": { 42 | "build": "npm run clean:build && babel ./src --out-dir ./lib --ignore src/**/*.test.js && babel ./example/app.js --out-file ./example/app.es5.js", 43 | "prepublish": "npm run build", 44 | "clean:build": "rimraf lib", 45 | "lint": "eslint -c .eslintrc . --fix", 46 | "test": "jest", 47 | "precommit": "npm run lint", 48 | "update": "ncu -a -u -x \"\"" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/SequelizeRedisModel.js: -------------------------------------------------------------------------------- 1 | const { stringify, parse } = require('json-buffer'); 2 | 3 | const methods = [ 4 | 'find', 5 | 'findOne', 6 | 'findAll', 7 | 'findAndCount', 8 | 'findAndCountAll', 9 | 'findById', 10 | 'findByPk', 11 | 'all', 12 | 'min', 13 | 'max', 14 | 'sum', 15 | 'count', 16 | ]; 17 | 18 | module.exports = class SequelizeRedisModel { 19 | constructor(model, redisPromisifiedClient, options = {}) { 20 | this.options = { 21 | ...options, 22 | }; 23 | 24 | this.model = model; 25 | this.redisClient = redisPromisifiedClient; 26 | methods.forEach((method) => { 27 | this[`${method}Cached`] = async (cacheKey, ...args) => this.run(cacheKey, method, ...args); 28 | this[method] = async (...args) => this.model[method](...args); 29 | }); 30 | } 31 | 32 | async run(cacheKey, method, ...args) { 33 | if (!methods.includes(method)) { 34 | throw new Error('Unsupported method'); 35 | } 36 | if (!this.model[method]) { 37 | throw new Error('Unsupported Method by Sequelize model'); 38 | } 39 | 40 | let cached; 41 | try { 42 | cached = await this.redisClient.getAsync(cacheKey); 43 | } catch (error) { 44 | throw new Error(`Cant get cached object from Redis ${error.message}`); 45 | } 46 | 47 | if (cached) { 48 | let parsed; 49 | try { 50 | parsed = parse(cached); 51 | } catch (error) { 52 | throw new Error(`Cant parse JSON of cached model's object: ${error.message}`); 53 | } 54 | 55 | try { 56 | // console.log('From Cache'); 57 | let result; 58 | const [queryOptions] = args; 59 | 60 | if (queryOptions && !!queryOptions.raw) { 61 | result = parsed; 62 | } else if (parsed.rows) { 63 | result = { 64 | ...parsed, 65 | rows: parsed.rows.map(parsedRow => this.model.build(parsedRow)), 66 | }; 67 | } else if (typeof parsed === 'number') { 68 | result = parsed; 69 | } else if (queryOptions) { 70 | const buildOptions = { 71 | raw: !!queryOptions.raw, 72 | isNewRecord: !!queryOptions.isNewRecord, 73 | }; 74 | if (queryOptions.include) { 75 | buildOptions.include = queryOptions.include; 76 | } 77 | result = this.model.build(parsed, buildOptions); 78 | } else { 79 | result = this.model.build(parsed); 80 | } 81 | 82 | return [result, true]; 83 | } catch (error) { 84 | throw new Error(`Cant build model from cached JSON: ${error.message}`); 85 | } 86 | } 87 | 88 | // console.log('From DB'); 89 | const result = await this.model[method](...args); 90 | let toCache; 91 | if (!result) { 92 | return [null, false]; 93 | } if (Array.isArray(result) || result.rows || typeof result === 'number') { 94 | // Array for findAll, result.rows for findAndCountAll, typeof number for count/max/sum/etc 95 | toCache = result; 96 | } else if (result.toString().includes(('[object SequelizeInstance'))) { 97 | toCache = result; 98 | } else { 99 | throw new Error(`Unkown result type: ${typeof result}`); 100 | } 101 | 102 | this.redisClient.set(cacheKey, stringify(toCache)); 103 | 104 | if (this.options.ttl) { 105 | this.redisClient.expire(cacheKey, this.options.ttl); 106 | } 107 | 108 | return [result, false]; 109 | } 110 | }; 111 | -------------------------------------------------------------------------------- /src/__tests__/sequelize-redis.test.js: -------------------------------------------------------------------------------- 1 | import { config as runDotenv } from 'dotenv'; 2 | import Sequelize from 'sequelize'; 3 | import redisClient from '../../jest/redisClient'; 4 | import SequelizeRedis from '../index'; 5 | 6 | runDotenv(); // to run with .env file for local env vars 7 | 8 | const db = { 9 | host: process.env.DB_HOST || 'localhost', 10 | port: process.env.DB_PORT || 3306, 11 | database: process.env.DB_NAME || 'tests', 12 | user: process.env.DB_USER || 'root', 13 | password: process.env.DB_PASS || '', 14 | }; 15 | const sequelizeOpts = { 16 | host: db.host, 17 | port: db.port, 18 | dialect: process.env.DB_DIALECT || 'mysql', 19 | logging: !!process.env.DB_LOG, 20 | operatorsAliases: Sequelize.Op, 21 | }; 22 | 23 | /* global describe */ 24 | /* global it */ 25 | /* global before */ 26 | /* global after */ 27 | 28 | function onErr(err) { 29 | throw err; 30 | } 31 | 32 | describe('Sequelize-Redis-Cache', () => { 33 | let sequelize; 34 | let User; 35 | let GitHubUser; 36 | let UserCacher; 37 | const cacheKey = 'test'; 38 | const userTTL = 1; // seconds 39 | 40 | beforeAll(async () => { 41 | redisClient.on('error', onErr); 42 | 43 | sequelize = new Sequelize(db.database, db.user, db.password, sequelizeOpts); 44 | 45 | User = sequelize.define( 46 | 'user', { 47 | id: { 48 | type: Sequelize.INTEGER, 49 | primaryKey: true, 50 | autoIncrement: true, 51 | }, 52 | username: { 53 | unique: true, 54 | allowNull: false, 55 | type: Sequelize.STRING, 56 | }, 57 | createdAt: Sequelize.DATE, 58 | updatedAt: Sequelize.DATE, 59 | }, 60 | { 61 | tableName: 'User', 62 | }, 63 | ); 64 | 65 | GitHubUser = sequelize.define( 66 | 'githubUser', { 67 | userId: { 68 | type: Sequelize.INTEGER, 69 | }, 70 | username: { 71 | type: Sequelize.STRING, 72 | primaryKey: true, 73 | }, 74 | createdAt: Sequelize.DATE, 75 | updatedAt: Sequelize.DATE, 76 | }, 77 | { 78 | tableName: 'GitHubUser', 79 | }, 80 | ); 81 | GitHubUser.belongsTo(User, { foreignKey: 'userId' }); 82 | User.hasOne(GitHubUser, { foreignKey: 'userId' }); 83 | 84 | 85 | try { 86 | await sequelize.sync({ force: true }); 87 | } catch (error) { 88 | throw new Error(`Cant sync Sequelize! ${error.message}`); 89 | } 90 | 91 | const user = await User.create({ 92 | username: 'idan', 93 | }); 94 | 95 | await GitHubUser.create({ 96 | userId: user.get('id'), 97 | username: 'idangozlan', 98 | }); 99 | 100 | await User.create({ 101 | username: 'idan2', 102 | }); 103 | 104 | const sequelizeCacher = new SequelizeRedis(redisClient); 105 | 106 | UserCacher = sequelizeCacher.getModel(User, { ttl: userTTL }); 107 | }); 108 | 109 | afterAll(async () => { 110 | await sequelize.close(); 111 | }); 112 | 113 | it('should fetch user from database with original Sequelize model', async () => { 114 | expect.assertions(1); 115 | 116 | const user = await User.findByPk(1); 117 | expect(user.get('username')).toEqual('idan'); 118 | }); 119 | 120 | it('should fetch users from database with raw:true option', async () => { 121 | expect.assertions(3); 122 | 123 | redisClient.del(cacheKey); 124 | const [users, isCached] = await UserCacher.findAllCached(cacheKey, { raw: true }); 125 | 126 | expect(users.length).toEqual(2); 127 | expect(isCached).toEqual(false); 128 | expect(users.toString().includes(('[object SequelizeInstance'))).toEqual(false); 129 | }); 130 | 131 | it('should fetch user from database with cached Sequelize model with original method', async () => { 132 | expect.assertions(1); 133 | 134 | const user = await UserCacher.findByPk(1); 135 | expect(user.get('username')).toEqual('idan'); 136 | }); 137 | 138 | it('should fetch user from database', async () => { 139 | expect.assertions(2); 140 | 141 | redisClient.del(cacheKey); 142 | const [user, isCached] = await UserCacher.findByPkCached(cacheKey, 1); 143 | 144 | expect(isCached).toEqual(false); 145 | expect(user.get('username')).toEqual('idan'); 146 | }); 147 | 148 | it('should fetch user from cache', async () => { 149 | expect.assertions(2); 150 | 151 | const [user, isCached] = await UserCacher.findByPkCached(cacheKey, 1); 152 | 153 | expect(isCached).toEqual(true); 154 | expect(user.get('username')).toEqual('idan'); 155 | }); 156 | 157 | it('should fetch user from database after ttl reached', async () => (new Promise(async (resolve, reject) => { 158 | expect.assertions(2); 159 | 160 | await (new Promise(resolveTimeout => setTimeout(resolveTimeout, userTTL * 1000))); 161 | const [user, isCached] = await UserCacher.findByPkCached(cacheKey, 1); 162 | try { 163 | expect(isCached).toEqual(false); 164 | expect(user.get('username')).toEqual('idan'); 165 | } catch (error) { 166 | reject(error); 167 | } 168 | 169 | return resolve(); 170 | }))); 171 | 172 | it('should fetch empty res with not cached flag', async () => { 173 | expect.assertions(2); 174 | 175 | const [user, isCached] = await UserCacher.findByPkCached('user_3', 3); 176 | expect(user).toBe(null); 177 | expect(isCached).toEqual(false); 178 | }); 179 | 180 | it('should fetch users from db', async () => { 181 | expect.assertions(4); 182 | 183 | redisClient.del(cacheKey); 184 | const [users, isCached] = await UserCacher.findAllCached(cacheKey); 185 | 186 | expect(users.length).toEqual(2); 187 | expect(isCached).toEqual(false); 188 | expect(users[0].get('username')).toEqual('idan'); 189 | expect(users[1].get('username')).toEqual('idan2'); 190 | }); 191 | 192 | it('should fetch users from cache', async () => { 193 | expect.assertions(4); 194 | 195 | const [users, isCached] = await UserCacher.findAllCached(cacheKey); 196 | 197 | expect(users.length).toEqual(2); 198 | expect(isCached).toEqual(true); 199 | expect(users[0].get('username')).toEqual('idan'); 200 | expect(users[1].get('username')).toEqual('idan2'); 201 | }); 202 | 203 | it('should fetch users from db and spread associations into JSON', async () => { 204 | expect.assertions(4); 205 | 206 | redisClient.del(cacheKey); 207 | const [users, isCached] = await UserCacher.findAllCached(cacheKey, { include: [GitHubUser] }); 208 | 209 | expect(users.length).toEqual(2); 210 | expect(isCached).toEqual(false); 211 | expect(users[0].githubUser.username).toEqual('idangozlan'); 212 | expect(users[0].githubUser.get('username')).toEqual('idangozlan'); 213 | }); 214 | 215 | it('should fetch users from cache with spreaded associations', async () => { 216 | expect.assertions(4); 217 | 218 | const [users, isCached] = await UserCacher.findAllCached(cacheKey, { include: [GitHubUser] }); 219 | 220 | expect(users.length).toEqual(2); 221 | expect(isCached).toEqual(true); 222 | expect(users[0].githubUser.username).toEqual('idangozlan'); 223 | expect(users[0].githubUser.get('username')).toEqual('idangozlan'); 224 | }); 225 | 226 | it('should fetch user with includes from database', async () => { 227 | expect.assertions(3); 228 | 229 | redisClient.del(cacheKey); 230 | const [user, isCached] = await UserCacher.findOneCached(cacheKey, { 231 | where: { 232 | id: 1, 233 | }, 234 | include: [GitHubUser], 235 | }); 236 | expect(isCached).toEqual(false); 237 | expect(user.get('username')).toEqual('idan'); 238 | expect(user.githubUser.get('username')).toEqual('idangozlan'); 239 | }); 240 | 241 | it('should fetch user with includes from cache', async () => { 242 | expect.assertions(3); 243 | 244 | const [user, isCached] = await UserCacher.findOneCached(cacheKey, { 245 | where: { 246 | id: 1, 247 | }, 248 | include: [GitHubUser], 249 | }); 250 | expect(isCached).toEqual(true); 251 | expect(user.get('username')).toEqual('idan'); 252 | expect(user.githubUser.get('username')).toEqual('idangozlan'); 253 | }); 254 | 255 | it('should findAndCountAll from db', async () => { 256 | expect.assertions(5); 257 | 258 | redisClient.del(cacheKey); 259 | const [res, isCached] = await UserCacher.findAndCountAllCached(cacheKey, { 260 | where: { 261 | username: 'idan', 262 | }, 263 | }); 264 | 265 | expect(isCached).toEqual(false); 266 | expect(res.count).toEqual(1); 267 | expect(res.rows).not.toBe(null); 268 | expect(res.rows.length).toEqual(1); 269 | expect(res.rows[0].get('username')).toEqual('idan'); 270 | }); 271 | 272 | it('should findAndCountAll from cache', async () => { 273 | expect.assertions(5); 274 | 275 | const [res, isCached] = await UserCacher.findAndCountAllCached(cacheKey, { 276 | where: { 277 | username: 'idan', 278 | }, 279 | }); 280 | 281 | expect(isCached).toEqual(true); 282 | expect(res.count).toEqual(1); 283 | expect(res.rows).not.toBe(null); 284 | expect(res.rows.length).toEqual(1); 285 | expect(res.rows[0].get('username')).toEqual('idan'); 286 | }); 287 | 288 | it('should count from db', async () => { 289 | expect.assertions(2); 290 | 291 | redisClient.del(cacheKey); 292 | const [res, isCached] = await UserCacher.countCached(cacheKey, { 293 | where: { 294 | username: 'idan', 295 | }, 296 | }); 297 | 298 | expect(isCached).toEqual(false); 299 | expect(res).toEqual(1); 300 | }); 301 | 302 | it('should count from cache', async () => { 303 | expect.assertions(2); 304 | 305 | const [res, isCached] = await UserCacher.countCached(cacheKey, { 306 | where: { 307 | username: 'idan', 308 | }, 309 | }); 310 | 311 | expect(isCached).toEqual(true); 312 | 313 | expect(res).toEqual(1); 314 | }); 315 | 316 | it('should sum from db', async () => { 317 | expect.assertions(2); 318 | 319 | redisClient.del(cacheKey); 320 | const [res, isCached] = await UserCacher.sumCached(cacheKey, 'id'); 321 | 322 | expect(isCached).toEqual(false); 323 | 324 | expect(res).toEqual(3); // user id 1 + user id 2 = 3 325 | }); 326 | 327 | it('should sum from cache', async () => { 328 | expect.assertions(2); 329 | 330 | const [res, isCached] = await UserCacher.sumCached(cacheKey, 'id'); 331 | 332 | expect(isCached).toEqual(true); 333 | 334 | expect(res).toEqual(3); // user id 1 + user id 2 = 3 335 | }); 336 | 337 | it('should max from db', async () => { 338 | expect.assertions(2); 339 | 340 | redisClient.del(cacheKey); 341 | const [res, isCached] = await UserCacher.maxCached(cacheKey, 'id'); 342 | 343 | expect(isCached).toEqual(false); 344 | 345 | expect(res).toEqual(2); // user id 2 346 | }); 347 | 348 | it('should max from cache', async () => { 349 | expect.assertions(2); 350 | 351 | const [res, isCached] = await UserCacher.maxCached(cacheKey, 'id'); 352 | 353 | expect(isCached).toEqual(true); 354 | 355 | expect(res).toEqual(2); // user id 2 356 | }); 357 | 358 | it('should min from db', async () => { 359 | expect.assertions(2); 360 | 361 | redisClient.del(cacheKey); 362 | const [res, isCached] = await UserCacher.minCached(cacheKey, 'id'); 363 | 364 | expect(isCached).toEqual(false); 365 | 366 | expect(res).toEqual(1); // user id 1 367 | }); 368 | 369 | it('should min from cache', async () => { 370 | expect.assertions(2); 371 | 372 | const [res, isCached] = await UserCacher.minCached(cacheKey, 'id'); 373 | 374 | expect(isCached).toEqual(true); 375 | 376 | expect(res).toEqual(1); // user id 1 377 | }); 378 | }); 379 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const SequelizeRedisModel = require('./SequelizeRedisModel'); 2 | 3 | module.exports = class SequelizeRedis { 4 | constructor(redisClient, options = {}) { 5 | this.redisClient = redisClient; 6 | this.options = options; 7 | } 8 | 9 | getModel(model, options = {}) { 10 | return (new SequelizeRedisModel(model, this.redisClient, { ...this.options, ...options })); 11 | } 12 | }; 13 | --------------------------------------------------------------------------------