├── .babelrc ├── .editorconfig ├── .eslintrc ├── .github ├── contributing.md ├── issue_template.md └── pull_request_template.md ├── .gitignore ├── .istanbul.yml ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── lib ├── library.js ├── library.js.map ├── library.min.js └── library.min.js.map ├── mocha.opts ├── package-lock.json ├── package.json ├── src ├── hooks │ ├── cache.js │ ├── helpers │ │ └── path.js │ └── redis.js ├── index.js ├── redisClient.js └── routes │ ├── cache.js │ └── helpers │ └── redis.js ├── test ├── cache.test.js ├── client.test.js ├── hooks │ ├── redis-after.test.js │ ├── redis-before.test.js │ └── redis-remove-hook.test.js ├── index.test.js ├── routes-functions.test.js ├── routes-http.test.js └── run-last │ └── redis-client.test.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", { 5 | "targets": { 6 | "node": "current" 7 | } 8 | } 9 | ] 10 | ], 11 | "plugins": ["babel-plugin-add-module-exports"], 12 | "env": { 13 | "test": { 14 | "plugins": [ "istanbul"] 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | 8 | "globals": { 9 | "document": false, 10 | "escape": false, 11 | "navigator": false, 12 | "unescape": false, 13 | "window": false, 14 | "describe": true, 15 | "before": true, 16 | "it": true, 17 | "expect": true, 18 | "sinon": true 19 | }, 20 | 21 | "parser": "babel-eslint", 22 | 23 | "plugins": [ 24 | 25 | ], 26 | 27 | "rules": { 28 | "block-scoped-var": 2, 29 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 30 | "camelcase": [2, { "properties": "always" }], 31 | "comma-dangle": [2, "never"], 32 | "comma-spacing": [2, { "before": false, "after": true }], 33 | "comma-style": [2, "last"], 34 | "complexity": 0, 35 | "consistent-return": 2, 36 | "consistent-this": 0, 37 | "curly": [2, "multi-line"], 38 | "default-case": 0, 39 | "dot-location": [2, "property"], 40 | "dot-notation": 0, 41 | "eol-last": 2, 42 | "eqeqeq": [2, "allow-null"], 43 | "func-names": 0, 44 | "func-style": 0, 45 | "generator-star-spacing": [2, "both"], 46 | "guard-for-in": 0, 47 | "handle-callback-err": [2, "^(err|error|anySpecificError)$" ], 48 | "indent": [2, 2, { "SwitchCase": 1 }], 49 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }], 50 | "keyword-spacing": [2, {"before": true, "after": true}], 51 | "linebreak-style": 0, 52 | "max-depth": 0, 53 | "max-len": [2, 120, 4], 54 | "max-nested-callbacks": 0, 55 | "max-params": 0, 56 | "max-statements": 0, 57 | "new-cap": [2, { "newIsCap": true, "capIsNew": false }], 58 | "newline-after-var": [2, "always"], 59 | "new-parens": 2, 60 | "no-alert": 0, 61 | "no-array-constructor": 2, 62 | "no-bitwise": 0, 63 | "no-caller": 2, 64 | "no-catch-shadow": 0, 65 | "no-cond-assign": 2, 66 | "no-console": 0, 67 | "no-constant-condition": 0, 68 | "no-continue": 0, 69 | "no-control-regex": 2, 70 | "no-debugger": 2, 71 | "no-delete-var": 2, 72 | "no-div-regex": 0, 73 | "no-dupe-args": 2, 74 | "no-dupe-keys": 2, 75 | "no-duplicate-case": 2, 76 | "no-else-return": 2, 77 | "no-empty": 0, 78 | "no-empty-character-class": 2, 79 | "no-eq-null": 0, 80 | "no-eval": 2, 81 | "no-ex-assign": 2, 82 | "no-extend-native": 2, 83 | "no-extra-bind": 2, 84 | "no-extra-boolean-cast": 2, 85 | "no-extra-parens": 0, 86 | "no-extra-semi": 0, 87 | "no-extra-strict": 0, 88 | "no-fallthrough": 2, 89 | "no-floating-decimal": 2, 90 | "no-func-assign": 2, 91 | "no-implied-eval": 2, 92 | "no-inline-comments": 0, 93 | "no-inner-declarations": [2, "functions"], 94 | "no-invalid-regexp": 2, 95 | "no-irregular-whitespace": 2, 96 | "no-iterator": 2, 97 | "no-label-var": 2, 98 | "no-labels": 2, 99 | "no-lone-blocks": 0, 100 | "no-lonely-if": 0, 101 | "no-loop-func": 0, 102 | "no-mixed-requires": 0, 103 | "no-mixed-spaces-and-tabs": [2, false], 104 | "no-multi-spaces": 2, 105 | "no-multi-str": 2, 106 | "no-multiple-empty-lines": [2, { "max": 1 }], 107 | "no-native-reassign": 2, 108 | "no-negated-in-lhs": 2, 109 | "no-nested-ternary": 0, 110 | "no-new": 2, 111 | "no-new-func": 2, 112 | "no-new-object": 2, 113 | "no-new-require": 2, 114 | "no-new-wrappers": 2, 115 | "no-obj-calls": 2, 116 | "no-octal": 2, 117 | "no-octal-escape": 2, 118 | "no-path-concat": 0, 119 | "no-plusplus": 0, 120 | "no-process-env": 0, 121 | "no-process-exit": 0, 122 | "no-proto": 2, 123 | "no-redeclare": 2, 124 | "no-regex-spaces": 2, 125 | "no-reserved-keys": 0, 126 | "no-restricted-modules": 0, 127 | "no-return-assign": 2, 128 | "no-script-url": 0, 129 | "no-self-compare": 2, 130 | "no-sequences": 2, 131 | "no-shadow": 0, 132 | "no-shadow-restricted-names": 2, 133 | "no-spaced-func": 2, 134 | "no-sparse-arrays": 2, 135 | "no-sync": 0, 136 | "no-ternary": 0, 137 | "no-throw-literal": 2, 138 | "no-trailing-spaces": 2, 139 | "no-undef": 2, 140 | "no-undef-init": 2, 141 | "no-undefined": 0, 142 | "no-underscore-dangle": 0, 143 | "no-unneeded-ternary": 2, 144 | "no-unreachable": 2, 145 | "no-unused-expressions": 0, 146 | "no-unused-vars": [2, { "vars": "all", "args": "none" }], 147 | "no-use-before-define": 2, 148 | "no-var": 0, 149 | "no-void": 0, 150 | "no-warning-comments": 0, 151 | "no-with": 2, 152 | "one-var": 0, 153 | "operator-assignment": 0, 154 | "operator-linebreak": [2, "after"], 155 | "padded-blocks": 0, 156 | "quote-props": 0, 157 | "quotes": [2, "single", "avoid-escape"], 158 | "radix": 2, 159 | "semi": [2, "always"], 160 | "semi-spacing": 0, 161 | "sort-vars": 0, 162 | "space-before-blocks": [2, "always"], 163 | "space-before-function-paren": [2, {"anonymous": "always", "named": "never"}], 164 | "space-in-brackets": 0, 165 | "space-in-parens": [2, "never"], 166 | "space-infix-ops": 2, 167 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 168 | "spaced-comment": [2, "always"], 169 | "strict": 0, 170 | "use-isnan": 2, 171 | "valid-jsdoc": 0, 172 | "valid-typeof": 2, 173 | "vars-on-top": 2, 174 | "wrap-iife": [2, "any"], 175 | "wrap-regex": 0, 176 | "yoda": [2, "never"] 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /.github/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to Feathers 2 | 3 | Thank you for contributing to Feathers! :heart: :tada: 4 | 5 | This repo is the main core and where most issues are reported. Feathers embraces modularity and is broken up across many repos. To make this easier to manage we currently use [Zenhub](https://www.zenhub.com/) for issue triage and visibility. They have a free browser plugin you can install so that you can see what is in flight at any time, but of course you also always see current issues in Github. 6 | 7 | ## Report a bug 8 | 9 | Before creating an issue please make sure you have checked out the docs, specifically the [FAQ](https://docs.feathersjs.com/help/faq.html) section. You might want to also try searching Github. It's pretty likely someone has already asked a similar question. 10 | 11 | If you haven't found your answer please feel free to join our [slack channel](http://slack.feathersjs.com), create an issue on Github, or post on [Stackoverflow](http://stackoverflow.com) using the `feathers` or `feathersjs` tag. We try our best to monitor Stackoverflow but you're likely to get more immediate responses in Slack and Github. 12 | 13 | Issues can be reported in the [issue tracker](https://github.com/feathersjs/feathers/issues). Since feathers combines many modules it can be hard for us to assess the root cause without knowing which modules are being used and what your configuration looks like, so **it helps us immensely if you can link to a simple example that reproduces your issue**. 14 | 15 | ## Report a Security Concern 16 | 17 | We take security very seriously at Feathers. We welcome any peer review of our 100% open source code to ensure nobody's Feathers app is ever compromised or hacked. As a web application developer you are responsible for any security breaches. We do our very best to make sure Feathers is as secure as possible by default. 18 | 19 | In order to give the community time to respond and upgrade we strongly urge you report all security issues to us. Send one of the core team members a PM in [Slack](http://slack.feathersjs.com) or email us at hello@feathersjs.com with details and we will respond ASAP. 20 | 21 | For full details refer to our [Security docs](https://docs.feathersjs.com/SECURITY.html). 22 | 23 | ## Pull Requests 24 | 25 | We :heart: pull requests and we're continually working to make it as easy as possible for people to contribute, including a [Plugin Generator](https://github.com/feathersjs/generator-feathers-plugin) and a [common test suite](https://github.com/feathersjs/feathers-service-tests) for database adapters. 26 | 27 | We prefer small pull requests with minimal code changes. The smaller they are the easier they are to review and merge. A core team member will pick up your PR and review it as soon as they can. They may ask for changes or reject your pull request. This is not a reflection of you as an engineer or a person. Please accept feedback graciously as we will also try to be sensitive when providing it. 28 | 29 | Although we generally accept many PRs they can be rejected for many reasons. We will be as transparent as possible but it may simply be that you do not have the same context or information regarding the roadmap that the core team members have. We value the time you take to put together any contributions so we pledge to always be respectful of that time and will try to be as open as possible so that you don't waste it. :smile: 30 | 31 | **All PRs (except documentation) should be accompanied with tests and pass the linting rules.** 32 | 33 | ### Code style 34 | 35 | Before running the tests from the `test/` folder `npm test` will run ESlint. You can check your code changes individually by running `npm run lint`. 36 | 37 | ### ES6 compilation 38 | 39 | Feathers uses [Babel](https://babeljs.io/) to leverage the latest developments of the JavaScript language. All code and samples are currently written in ES2015. To transpile the code in this repository run 40 | 41 | > npm run compile 42 | 43 | __Note:__ `npm test` will run the compilation automatically before the tests. 44 | 45 | ### Tests 46 | 47 | [Mocha](http://mochajs.org/) tests are located in the `test/` folder and can be run using the `npm run mocha` or `npm test` (with ESLint and code coverage) command. 48 | 49 | ### Documentation 50 | 51 | Feathers documentation is contained in Markdown files in the [feathers-docs](https://github.com/feathersjs/feathers-docs) repository. To change the documentation submit a pull request to that repo, referencing any other PR if applicable, and the docs will be updated with the next release. 52 | 53 | ## External Modules 54 | 55 | If you're written something awesome for Feathers, the Feathers ecosystem, or using Feathers please add it to the [showcase](https://docs.feathersjs.com/why/showcase.html). You also might want to check out the [Plugin Generator](https://github.com/feathersjs/generator-feathers-plugin) that can be used to scaffold plugins to be Feathers compliant from the start. 56 | 57 | If you think it would be a good core module then please contact one of the Feathers core team members in [Slack](http://slack.feathersjs.com) and we can discuss whether it belongs in core and how to get it there. :beers: 58 | 59 | ## Contributor Code of Conduct 60 | 61 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 62 | 63 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. 64 | 65 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 66 | 67 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 68 | 69 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 70 | 71 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) 72 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ### Steps to reproduce 2 | 3 | (First please check that this issue is not already solved as [described 4 | here](https://github.com/feathersjs/feathers/blob/master/.github/contributing.md#report-a-bug)) 5 | 6 | - [ ] Tell us what broke. The more detailed the better. 7 | - [ ] If you can, please create a simple example that reproduces the issue and link to a gist, jsbin, repo, etc. 8 | 9 | ### Expected behavior 10 | Tell us what should happen 11 | 12 | ### Actual behavior 13 | Tell us what happens instead 14 | 15 | ### System configuration 16 | 17 | Tell us about the applicable parts of your setup. 18 | 19 | **Module versions** (especially the part that's not working): 20 | 21 | **NodeJS version**: 22 | 23 | **Operating System**: 24 | 25 | **Browser Version**: 26 | 27 | **React Native Version**: 28 | 29 | **Module Loader**: -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Summary 2 | 3 | (If you have not already please refer to the contributing guideline as [described 4 | here](https://github.com/feathersjs/feathers/blob/master/.github/contributing.md#pull-requests)) 5 | 6 | - [ ] Tell us about the problem your pull request is solving. 7 | - [ ] Are there any open issues that are related to this? 8 | - [ ] Is this PR dependent on PRs in other repos? 9 | 10 | If so, please mention them to keep the conversations linked together. 11 | 12 | ### Other Information 13 | 14 | If there's anything else that's important and relevant to your pull 15 | request, mention that information here. This could include 16 | benchmarks, or other information. 17 | 18 | Your PR will be reviewed by a core team member and they will work with you to get your changes merged in a timely manner. If merged your PR will automatically be added to the changelog in the next release. 19 | 20 | If your changes involve documentation updates please mention that and link the appropriate PR in [feathers-docs](https://github.com/feathersjs/feathers-docs). 21 | 22 | Thanks for contributing to Feathers! :heart: -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Logs 4 | logs 5 | *.log 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib- 14 | .nyc_output 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | .coveralls.yml 19 | coverage.lcov 20 | 21 | 22 | # Compiled files 23 | # lib/ 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # node-waf configuration 29 | .lock-wscript 30 | 31 | # Compiled binary addons (http://nodejs.org/api/addons.html) 32 | build/Release 33 | 34 | # Dependency directory 35 | # Commenting this out is preferred by some people, see 36 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 37 | node_modules 38 | 39 | # Remove some common IDE working directories 40 | .idea 41 | .vscode 42 | -------------------------------------------------------------------------------- /.istanbul.yml: -------------------------------------------------------------------------------- 1 | verbose: false 2 | instrumentation: 3 | root: ./lib/ 4 | include-all-sources: true 5 | reporting: 6 | print: summary 7 | reports: 8 | - html 9 | - text 10 | - lcov 11 | watermarks: 12 | statements: [50, 80] 13 | lines: [50, 80] 14 | functions: [50, 80] 15 | branches: [50, 80] 16 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .editorconfig 2 | .jshintrc 3 | .travis.yml 4 | .istanbul.yml 5 | .babelrc 6 | .idea/ 7 | .vscode/ 8 | test/ 9 | coverage/ 10 | coverage.lcov 11 | .coveralls.yml 12 | mocha.opts 13 | weback.config.js 14 | .github/ 15 | .nyc_output 16 | coverage/ 17 | node_modules/ 18 | src/ 19 | test/ 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | services: 3 | - redis-server 4 | node_js: 5 | - "10" 6 | - "9" 7 | - "8" 8 | before_install: 9 | - npm install -g npm 10 | - npm install -g codecov 11 | script: 12 | - npm run dev-travis 13 | cache: 14 | directories: 15 | - "node_modules" 16 | after_success: 17 | - npm run coveralls 18 | - npm run codecov 19 | notifications: 20 | email: false -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Feathers 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 | # Redis Cache 2 | 3 | [![Greenkeeper badge](https://badges.greenkeeper.io/idealley/feathers-hooks-rediscache.svg)](https://greenkeeper.io/) 4 | [![Build Status](https://travis-ci.org/idealley/feathers-hooks-rediscache.png?branch=master)](https://travis-ci.org/idealley/feathers-hooks-rediscache) 5 | [![codecov](https://codecov.io/gh/idealley/feathers-hooks-rediscache/branch/master/graph/badge.svg)](https://codecov.io/gh/idealley/feathers-hooks-rediscache) 6 | 7 | > Cache any route with redis 8 | 9 | ## Releases 10 | * Versions 1.x.x are compatible with Feathersjs 3.x.x 11 | * Versions 0.x.x are compatible with Feathersjs 2.x.x -> this branch will not be updated anymore 12 | 13 | ## Installation 14 | 15 | ### Feathers 3.x.x 16 | ``` 17 | npm install feathers-hooks-rediscache --save 18 | ``` 19 | 20 | ### Feathers 2.x.x 21 | If you do not use nested routes you can install version 1.x.x if not: 22 | ``` 23 | npm install feathers-hooks-rediscache@0.3.6 --save-exact 24 | ``` 25 | 26 | ## Purpose 27 | The purpose of these hooks is to provide redis caching for APIs endpoints. Using redis is a very good option for clusturing your API. As soon as a request is cached it is available to all the other nodes in the cluster, which is not true for usual in memory cache as each node has its own memory allocated. This means that each node has to cache all requests individually. 28 | 29 | Each request to an endpoint can be cached. Route variables and params are cached on a per request base. If a param to call is set to true and then to false two responses will be cached. 30 | 31 | The cache can be purged for an individual route, but also for a group of routes. This is very useful if you have an API endpoint that creates a list of articles, and an endpoint that returns an individual article. If the article is modified, the list of articles should, most likely, be purged as well. This can be done by calling one endpoint. 32 | 33 | ### Routes exemples 34 | In the same fashion if you have many variants of the same endpoint that return similar content based on parameters you can bust the whole group as well: 35 | 36 | ```js 37 | '/articles' // list 38 | '/articles/article' //individual item 39 | '/articles/article?markdown=true' // variant 40 | ``` 41 | #### Clearing cache 42 | These are all listed in a redis list under `group-articles` and can be busted by calling `/cache/clear/group/article` or `/cache/clear/group/articles` it does not matter. All urls keys will be purged. 43 | 44 | You can also purge single cached paths as by doing GET requests on 45 | ```js 46 | '/cache/clear/single/articles' 47 | '/cache/clear/single/articles/article' 48 | '/cache/clear/single/articles/article?markdown=true' // works with query strings too 49 | ``` 50 | It was meant to be used over **_HTTP_**, not yet tested with sockets. 51 | 52 | ## Available hooks 53 | More details and example use bellow 54 | 55 | ### Before 56 | * `redisBeforeHook` - retrives the data from redis 57 | 58 | ### After 59 | * `hookCache` - set defaults caching duration, an object can be passed with the duration in seconds 60 | * `redisAfterHook` - saves to redis 61 | * `hookRemoveCacheInformation` - removes the cache object from responses (does not clear from Redis) 62 | 63 | 64 | ## Documentation 65 | Add the different hooks. The order matters (see below). A `cache` object will be added to your response. This is useful as other systems can use this object to purge the cache if needed. 66 | 67 | If the cache object is not needed/wanted it can be removed with the after hook `hookRemoveCacheInformation()` 68 | 69 | ### Configuration 70 | #### Redis 71 | To configure the redis connection the feathers configuration system can be used. 72 | ```js 73 | //config/default.json 74 | { 75 | "host": "localhost", 76 | "port": 3030, 77 | "redis": { 78 | "host": "my-redis-service.example.com", 79 | "port": 1234 80 | } 81 | } 82 | ``` 83 | * if no config is provided, default config from the [redis module](https://github.com/NodeRedis/node_redis) is used 84 | 85 | #### Hooks Configuration 86 | A redisCache object can be added to the default feathers configuration 87 | 88 | ```js 89 | //config/default.json 90 | 91 | "redisCache" : { 92 | "defaultDuration": 3600, 93 | "parseNestedRoutes": true, 94 | "removePathFromCacheKey": true, 95 | "env": "NODE_ENV" 96 | }; 97 | ``` 98 | ##### defaultDuration 99 | The default duration can be configured by passing the duration in seconds to the property `defaultDuration`. 100 | This can be overridden at the hook level (see the full example bellow) 101 | 102 | ##### parseNestedRoutes 103 | If your API uses nested routes like `/author/:authorId/book` you should turn on the option `parseNestedRoutes`. Otherwise you could have conflicting cache keys. 104 | 105 | ##### removePathFromCacheKey 106 | `removePathFromCacheKey` is an option that is useful when working with content and slugs. If when this option is turned on you can have the following issue. If your routes use IDs then you could have a conflict and the cache might return the wrong value: 107 | 108 | ```js 109 | 'user/123' 110 | 'article/123' 111 | ``` 112 | 113 | both items with id `123` would be saved under the same cache key... thus replacing each other and returning one for the other, thus by default the key includes the path to diferenciate them. when working with content you could have an external system busting the cache that is not aware of your API routes. That system would know the slug, but cannot bust the cache as it would have to call `/cache/clear/single/:path/target`, with this option that system can simply call `:target` which would be the slug/alias of the article. 114 | 115 | ##### env 116 | The default environement is production, but it is anoying when running test as the hooks output information to the console. Therefore if you youse this option, you can set `test` as an environement and the hooks will not output anything to the console. if you use `NODE_ENV` it will pick up the `process.env.NODE_ENV` variable. This is useful for CI or CLI. 117 | 118 | ##### immediateCacheKey 119 | By default the redis cache key gets determined in `redisAfterHook` based on the path. However if you're doing a lot of query manipulation you might want to set the cache key before anything else to keep its size as small as possible. You can achieve this by setting `immediateCacheKey: true` what will set the cache key in the `redisBeforeHook`. Then your hooks might look similar to: 120 | 121 | ```js 122 | { 123 | before: { 124 | find: [redisBefore({ immediateCacheKey: true }), someQueryManipulation()] 125 | }, 126 | after: { 127 | find: [cache(), redisAfter()] 128 | } 129 | } 130 | ``` 131 | 132 | 133 | Available routes: 134 | ```js 135 | // this route is disable as I noticed issues when redis has many keys, 136 | // I will put it back when I have a more robust solution 137 | // '/cache/index' // returns an array with all the keys 138 | '/cache/clear' // clears the whole cache 139 | '/cache/clear/single/:target' // clears a single route if you want to purge a route with params just adds them target?param=1 140 | '/cache/clear/group/:target' // clears a group 141 | ``` 142 | 143 | ## Complete Example 144 | 145 | Here's an example of a Feathers server that uses `feathers-hooks-rediscache`. 146 | 147 | ```js 148 | const feathers = require('feathers'); 149 | const rest = require('feathers-rest'); 150 | const hooks = require('feathers-hooks'); 151 | const bodyParser = require('body-parser'); 152 | const errorHandler = require('feathers-errors/handler'); 153 | const routes = require('feathers-hooks-rediscache').cacheRoutes; 154 | const redisClient = require('feathers-hooks-rediscache').redisClient; 155 | 156 | // Initialize the application 157 | const app = feathers() 158 | .configure(rest()) 159 | .configure(hooks()) 160 | // configure the redis client 161 | .configure(redisClient) 162 | 163 | // Needed for parsing bodies (login) 164 | .use(bodyParser.json()) 165 | .use(bodyParser.urlencoded({ extended: true })) 166 | // add the cache routes (endpoints) to the app 167 | .use('/cache', routes(app)) 168 | .use(errorHandler()); 169 | 170 | app.listen(3030); 171 | 172 | console.log('Feathers app started on 127.0.0.1:3030'); 173 | ``` 174 | 175 | Add hooks on the routes that need caching 176 | ```js 177 | //services/.hooks.js 178 | 179 | const redisBefore = require('feathers-hooks-rediscache').redisBeforeHook; 180 | const redisAfter = require('feathers-hooks-rediscache').redisAfterHook; 181 | const cache = require('feathers-hooks-rediscache').hookCache; 182 | 183 | 184 | module.exports = { 185 | before: { 186 | all: [], 187 | find: [redisBefore()], 188 | get: [redisBefore()], 189 | create: [], 190 | update: [], 191 | patch: [], 192 | remove: [] 193 | }, 194 | 195 | after: { 196 | all: [], 197 | find: [cache({duration: 3600 * 24 * 7}), redisAfter()], 198 | get: [cache({duration: 3600 * 24 * 7}), redisAfter()], 199 | create: [], 200 | update: [], 201 | patch: [], 202 | remove: [] 203 | }, 204 | 205 | error: { 206 | all: [], 207 | find: [], 208 | get: [], 209 | create: [], 210 | update: [], 211 | patch: [], 212 | remove: [] 213 | } 214 | }; 215 | ``` 216 | * the duration is in seconds and will automatically expire 217 | * you may just use `cache()` without specifying a duration, any request will be cached for a day or with the global configured value (see configuration above). 218 | 219 | ## License 220 | 221 | Copyright (c) 2018 222 | 223 | Licensed under the [MIT license](LICENSE). 224 | 225 | ## Change log 226 | ### v.1.1 227 | * Fixes #23. Feathers works slightly differently, now the change is reflected in the hooks. Tests were adapted. 228 | * Fixes #24. When it can Webpack 4 tries to evaluate code and remove "dead" code, therefore a condition testing for the environement was being evaluated, the value was set to the build environement... To bypass that, the hooks consider that they run in production mode. If you want to set a different one add a property `env: "NODE_ENV"` to the rediscache object, it will pick up your node environement and pass it to the hooks. 229 | ### v1.1.0 230 | * The `/index` path as well as the scan methods have been removed for now. In fact, testing on a Redis instance with more than 30k keys, it might bring down your server. I need to find a better way to return keys, or to search for them. So to prevent any problem I have removed it. (the code is commented out). 231 | ### v1.0.3 232 | * Webpack 4 233 | * Dependencies update 234 | * Loging of info: modification of the console display 235 | ### v1.0.0 236 | * Compatibility with Feathers 3.x.x 237 | * Nested routes fix #3 238 | ### v0.3.6 239 | * Fixed config issue, Now using minified version. Thank you @oppodeldoc 240 | ### v0.3.5 241 | * Now the ability to parse optional params in nested routes. Thank you @oppodeldoc 242 | ### v0.3.4 243 | * new scan method that takes params and a Set to make sure keys are unique. 244 | ### v0.3.0 245 | * introduces a breaking change: `.use('/cache', routes(app))` 246 | -------------------------------------------------------------------------------- /lib/library.js: -------------------------------------------------------------------------------- 1 | (function webpackUniversalModuleDefinition(root, factory) { 2 | if(typeof exports === 'object' && typeof module === 'object') 3 | module.exports = factory(); 4 | else if(typeof define === 'function' && define.amd) 5 | define("library", [], factory); 6 | else if(typeof exports === 'object') 7 | exports["library"] = factory(); 8 | else 9 | root["library"] = factory(); 10 | })(global, function() { 11 | return /******/ (function(modules) { // webpackBootstrap 12 | /******/ // The module cache 13 | /******/ var installedModules = {}; 14 | /******/ 15 | /******/ // The require function 16 | /******/ function __webpack_require__(moduleId) { 17 | /******/ 18 | /******/ // Check if module is in cache 19 | /******/ if(installedModules[moduleId]) { 20 | /******/ return installedModules[moduleId].exports; 21 | /******/ } 22 | /******/ // Create a new module (and put it into the cache) 23 | /******/ var module = installedModules[moduleId] = { 24 | /******/ i: moduleId, 25 | /******/ l: false, 26 | /******/ exports: {} 27 | /******/ }; 28 | /******/ 29 | /******/ // Execute the module function 30 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 31 | /******/ 32 | /******/ // Flag the module as loaded 33 | /******/ module.l = true; 34 | /******/ 35 | /******/ // Return the exports of the module 36 | /******/ return module.exports; 37 | /******/ } 38 | /******/ 39 | /******/ 40 | /******/ // expose the modules object (__webpack_modules__) 41 | /******/ __webpack_require__.m = modules; 42 | /******/ 43 | /******/ // expose the module cache 44 | /******/ __webpack_require__.c = installedModules; 45 | /******/ 46 | /******/ // define getter function for harmony exports 47 | /******/ __webpack_require__.d = function(exports, name, getter) { 48 | /******/ if(!__webpack_require__.o(exports, name)) { 49 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); 50 | /******/ } 51 | /******/ }; 52 | /******/ 53 | /******/ // define __esModule on exports 54 | /******/ __webpack_require__.r = function(exports) { 55 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 56 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 57 | /******/ } 58 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 59 | /******/ }; 60 | /******/ 61 | /******/ // create a fake namespace object 62 | /******/ // mode & 1: value is a module id, require it 63 | /******/ // mode & 2: merge all properties of value into the ns 64 | /******/ // mode & 4: return value when already ns object 65 | /******/ // mode & 8|1: behave like require 66 | /******/ __webpack_require__.t = function(value, mode) { 67 | /******/ if(mode & 1) value = __webpack_require__(value); 68 | /******/ if(mode & 8) return value; 69 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; 70 | /******/ var ns = Object.create(null); 71 | /******/ __webpack_require__.r(ns); 72 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); 73 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); 74 | /******/ return ns; 75 | /******/ }; 76 | /******/ 77 | /******/ // getDefaultExport function for compatibility with non-harmony modules 78 | /******/ __webpack_require__.n = function(module) { 79 | /******/ var getter = module && module.__esModule ? 80 | /******/ function getDefault() { return module['default']; } : 81 | /******/ function getModuleExports() { return module; }; 82 | /******/ __webpack_require__.d(getter, 'a', getter); 83 | /******/ return getter; 84 | /******/ }; 85 | /******/ 86 | /******/ // Object.prototype.hasOwnProperty.call 87 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 88 | /******/ 89 | /******/ // __webpack_public_path__ 90 | /******/ __webpack_require__.p = ""; 91 | /******/ 92 | /******/ 93 | /******/ // Load entry module and return exports 94 | /******/ return __webpack_require__(__webpack_require__.s = "./src/index.js"); 95 | /******/ }) 96 | /************************************************************************/ 97 | /******/ ({ 98 | 99 | /***/ "./src/hooks/cache.js": 100 | /*!****************************!*\ 101 | !*** ./src/hooks/cache.js ***! 102 | \****************************/ 103 | /*! no static exports found */ 104 | /***/ (function(module, exports, __webpack_require__) { 105 | 106 | "use strict"; 107 | 108 | 109 | Object.defineProperty(exports, "__esModule", { 110 | value: true 111 | }); 112 | exports.cache = cache; 113 | exports.removeCacheInformation = removeCacheInformation; 114 | 115 | /** 116 | * After hook - generates a cache object that is needed 117 | * for the redis hook and the express middelware. 118 | */ 119 | const defaults = { 120 | defaultDuration: 3600 * 24 121 | }; 122 | 123 | function cache(options) { 124 | // eslint-disable-line no-unused-vars 125 | return function (hook) { 126 | const cacheOptions = hook.app.get('redisCache'); 127 | options = Object.assign({}, defaults, cacheOptions, options); 128 | 129 | if (!hook.result.hasOwnProperty('cache')) { 130 | let cache = {}; 131 | 132 | if (Array.isArray(hook.result)) { 133 | const array = hook.result; 134 | cache.wrapped = array; 135 | hook.result = {}; 136 | } 137 | 138 | cache = Object.assign({}, cache, { 139 | cached: false, 140 | duration: options.duration || options.defaultDuration 141 | }); 142 | hook.result.cache = cache; 143 | } 144 | 145 | return Promise.resolve(hook); 146 | }; 147 | } 148 | 149 | ; 150 | 151 | function removeCacheInformation(options) { 152 | // eslint-disable-line no-unused-vars 153 | return function (hook) { 154 | if (hook.result.hasOwnProperty('cache')) { 155 | delete hook.result.cache; 156 | } 157 | 158 | return Promise.resolve(hook); 159 | }; 160 | } 161 | 162 | ; 163 | 164 | /***/ }), 165 | 166 | /***/ "./src/hooks/helpers/path.js": 167 | /*!***********************************!*\ 168 | !*** ./src/hooks/helpers/path.js ***! 169 | \***********************************/ 170 | /*! no static exports found */ 171 | /***/ (function(module, exports, __webpack_require__) { 172 | 173 | "use strict"; 174 | 175 | 176 | Object.defineProperty(exports, "__esModule", { 177 | value: true 178 | }); 179 | exports.parsePath = parsePath; 180 | 181 | var _qs = _interopRequireDefault(__webpack_require__(/*! qs */ "qs")); 182 | 183 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 184 | 185 | function parseNestedPath(path, params) { 186 | const re = new RegExp(':([^\\/\\?]+)\\??', 'g'); 187 | let match = null; 188 | 189 | while ((match = re.exec(path)) !== null) { 190 | if (Object.keys(params.route).includes(match[1])) { 191 | path = path.replace(match[0], params.route[match[1]]); 192 | } 193 | } 194 | 195 | return path; 196 | } 197 | 198 | function parsePath(hook, config = { 199 | removePathFromCacheKey: false, 200 | parseNestedRoutes: false 201 | }) { 202 | const q = hook.params.query || {}; 203 | const remove = config.removePathFromCacheKey; 204 | const parseNestedRoutes = config.parseNestedRoutes; 205 | let path = remove && hook.id ? '' : `${hook.path}`; 206 | 207 | if (!remove && parseNestedRoutes) { 208 | path = parseNestedPath(path, hook.params); 209 | } 210 | 211 | if (hook.id) { 212 | if (path.length !== 0 && !remove) { 213 | path += '/'; 214 | } 215 | 216 | if (Object.keys(q).length > 0) { 217 | path += `${hook.id}?${_qs.default.stringify(q, { 218 | encode: false 219 | })}`; 220 | } else { 221 | path += `${hook.id}`; 222 | } 223 | } else { 224 | if (Object.keys(q).length > 0) { 225 | path += `?${_qs.default.stringify(q, { 226 | encode: false 227 | })}`; 228 | } 229 | } 230 | 231 | return path; 232 | } 233 | 234 | /***/ }), 235 | 236 | /***/ "./src/hooks/redis.js": 237 | /*!****************************!*\ 238 | !*** ./src/hooks/redis.js ***! 239 | \****************************/ 240 | /*! no static exports found */ 241 | /***/ (function(module, exports, __webpack_require__) { 242 | 243 | "use strict"; 244 | 245 | 246 | Object.defineProperty(exports, "__esModule", { 247 | value: true 248 | }); 249 | exports.before = before; 250 | exports.after = after; 251 | 252 | var _moment = _interopRequireDefault(__webpack_require__(/*! moment */ "moment")); 253 | 254 | var _chalk = _interopRequireDefault(__webpack_require__(/*! chalk */ "chalk")); 255 | 256 | var _path = __webpack_require__(/*! ./helpers/path */ "./src/hooks/helpers/path.js"); 257 | 258 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 259 | 260 | const defaults = { 261 | env: 'production', 262 | defaultDuration: 3600 * 24, 263 | immediateCacheKey: false 264 | }; 265 | 266 | function before(options) { 267 | // eslint-disable-line no-unused-vars 268 | return function (hook) { 269 | const cacheOptions = hook.app.get('redisCache'); 270 | options = Object.assign({}, defaults, cacheOptions, options); 271 | return new Promise(resolve => { 272 | const client = hook.app.get('redisClient'); 273 | 274 | if (!client) { 275 | resolve(hook); 276 | } 277 | 278 | const path = (0, _path.parsePath)(hook, options); 279 | client.get(path, (err, reply) => { 280 | if (err !== null) resolve(hook); 281 | 282 | if (reply) { 283 | let data = JSON.parse(reply); 284 | const duration = (0, _moment.default)(data.cache.expiresOn).format('DD MMMM YYYY - HH:mm:ss'); 285 | hook.result = data; 286 | resolve(hook); 287 | /* istanbul ignore next */ 288 | 289 | if (options.env !== 'test') { 290 | console.log(`${_chalk.default.cyan('[redis]')} returning cached value for ${_chalk.default.green(path)}.`); 291 | console.log(`> Expires on ${duration}.`); 292 | } 293 | } else { 294 | if (options.immediateCacheKey === true) { 295 | hook.params.cacheKey = path; 296 | } 297 | 298 | resolve(hook); 299 | } 300 | }); 301 | }); 302 | }; 303 | } 304 | 305 | ; 306 | 307 | function after(options) { 308 | // eslint-disable-line no-unused-vars 309 | return function (hook) { 310 | const cacheOptions = hook.app.get('redisCache'); 311 | options = Object.assign({}, defaults, cacheOptions, options); 312 | return new Promise(resolve => { 313 | if (!hook.result.cache.cached) { 314 | const duration = hook.result.cache.duration || options.defaultDuration; 315 | const client = hook.app.get('redisClient'); 316 | 317 | if (!client) { 318 | resolve(hook); 319 | } 320 | 321 | const path = hook.params.cacheKey || (0, _path.parsePath)(hook, options); // adding a cache object 322 | 323 | Object.assign(hook.result.cache, { 324 | cached: true, 325 | duration: duration, 326 | expiresOn: (0, _moment.default)().add(_moment.default.duration(duration, 'seconds')), 327 | parent: hook.path, 328 | group: hook.path ? `group-${hook.path}` : '', 329 | key: path 330 | }); 331 | client.set(path, JSON.stringify(hook.result)); 332 | client.expire(path, hook.result.cache.duration); 333 | 334 | if (hook.path) { 335 | client.rpush(hook.result.cache.group, path); 336 | } 337 | /* istanbul ignore next */ 338 | 339 | 340 | if (options.env !== 'test') { 341 | console.log(`${_chalk.default.cyan('[redis]')} added ${_chalk.default.green(path)} to the cache.`); 342 | console.log(`> Expires in ${_moment.default.duration(duration, 'seconds').humanize()}.`); 343 | } 344 | } 345 | 346 | if (hook.result.cache.hasOwnProperty('wrapped')) { 347 | const { 348 | wrapped 349 | } = hook.result.cache; 350 | hook.result = wrapped; 351 | } 352 | 353 | resolve(hook); 354 | }); 355 | }; 356 | } 357 | 358 | ; 359 | 360 | /***/ }), 361 | 362 | /***/ "./src/index.js": 363 | /*!**********************!*\ 364 | !*** ./src/index.js ***! 365 | \**********************/ 366 | /*! no static exports found */ 367 | /***/ (function(module, exports, __webpack_require__) { 368 | 369 | "use strict"; 370 | 371 | 372 | Object.defineProperty(exports, "__esModule", { 373 | value: true 374 | }); 375 | exports.default = void 0; 376 | 377 | var _cache = _interopRequireDefault(__webpack_require__(/*! ./routes/cache */ "./src/routes/cache.js")); 378 | 379 | var _redisClient = _interopRequireDefault(__webpack_require__(/*! ./redisClient */ "./src/redisClient.js")); 380 | 381 | var _cache2 = __webpack_require__(/*! ./hooks/cache */ "./src/hooks/cache.js"); 382 | 383 | var _redis = __webpack_require__(/*! ./hooks/redis */ "./src/hooks/redis.js"); 384 | 385 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 386 | 387 | var _default = { 388 | redisClient: _redisClient.default, 389 | cacheRoutes: _cache.default, 390 | hookCache: _cache2.cache, 391 | hookRemoveCacheInformation: _cache2.removeCacheInformation, 392 | redisBeforeHook: _redis.before, 393 | redisAfterHook: _redis.after 394 | }; 395 | exports.default = _default; 396 | module.exports = exports.default; 397 | 398 | /***/ }), 399 | 400 | /***/ "./src/redisClient.js": 401 | /*!****************************!*\ 402 | !*** ./src/redisClient.js ***! 403 | \****************************/ 404 | /*! no static exports found */ 405 | /***/ (function(module, exports, __webpack_require__) { 406 | 407 | "use strict"; 408 | 409 | 410 | Object.defineProperty(exports, "__esModule", { 411 | value: true 412 | }); 413 | exports.default = redisClient; 414 | 415 | var _redis = _interopRequireDefault(__webpack_require__(/*! redis */ "redis")); 416 | 417 | var _chalk = _interopRequireDefault(__webpack_require__(/*! chalk */ "chalk")); 418 | 419 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 420 | 421 | function redisClient() { 422 | // eslint-disable-line no-unused-vars 423 | const app = this; 424 | const cacheOptions = app.get('redisCache') || {}; 425 | const retryInterval = cacheOptions.retryInterval || 10000; 426 | const redisOptions = Object.assign({}, this.get('redis'), { 427 | retry_strategy: function (options) { 428 | // eslint-disable-line camelcase 429 | app.set('redisClient', undefined); 430 | /* istanbul ignore next */ 431 | 432 | if (cacheOptions.env !== 'test') { 433 | console.log(`${_chalk.default.yellow('[redis]')} not connected`); 434 | } 435 | 436 | return retryInterval; 437 | } 438 | }); 439 | 440 | const client = _redis.default.createClient(redisOptions); 441 | 442 | app.set('redisClient', client); 443 | client.on('ready', () => { 444 | app.set('redisClient', client); 445 | /* istanbul ignore next */ 446 | 447 | if (cacheOptions.env !== 'test') { 448 | console.log(`${_chalk.default.green('[redis]')} connected`); 449 | } 450 | }); 451 | return this; 452 | } 453 | 454 | module.exports = exports.default; 455 | 456 | /***/ }), 457 | 458 | /***/ "./src/routes/cache.js": 459 | /*!*****************************!*\ 460 | !*** ./src/routes/cache.js ***! 461 | \*****************************/ 462 | /*! no static exports found */ 463 | /***/ (function(module, exports, __webpack_require__) { 464 | 465 | "use strict"; 466 | 467 | 468 | Object.defineProperty(exports, "__esModule", { 469 | value: true 470 | }); 471 | exports.default = void 0; 472 | 473 | var _express = _interopRequireDefault(__webpack_require__(/*! express */ "express")); 474 | 475 | var _redis = _interopRequireDefault(__webpack_require__(/*! ./helpers/redis */ "./src/routes/helpers/redis.js")); 476 | 477 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 478 | 479 | const HTTP_OK = 200; 480 | const HTTP_NO_CONTENT = 204; 481 | const HTTP_SERVER_ERROR = 500; 482 | const HTTP_NOT_FOUND = 404; 483 | 484 | function routes(app) { 485 | const router = _express.default.Router(); 486 | 487 | const client = app.get('redisClient'); 488 | const h = new _redis.default(client); 489 | router.get('/clear', (req, res) => { 490 | client.flushall('ASYNC', () => { 491 | res.status(HTTP_OK).json({ 492 | message: 'Cache cleared', 493 | status: HTTP_OK 494 | }); 495 | }); 496 | }); // clear a unique route 497 | // clear a unique route 498 | 499 | router.get('/clear/single/*', (req, res) => { 500 | let target = decodeURIComponent(req.params[0]); // Formated options following ? 501 | 502 | const query = req.query; 503 | const hasQueryString = query && Object.keys(query).length !== 0; // Target should always be defined as Express router raises 404 504 | // as route is not handled 505 | 506 | if (target.length) { 507 | if (hasQueryString) { 508 | // Keep queries in a single string with the taget 509 | target = decodeURIComponent(req.url.split('/').slice(3).join('/')); 510 | } // Gets the value of a key in the redis client 511 | 512 | 513 | client.get(`${target}`, (err, reply) => { 514 | if (err) { 515 | res.status(HTTP_SERVER_ERROR).json({ 516 | message: 'something went wrong' + err.message 517 | }); 518 | } else { 519 | // If the key existed 520 | if (reply) { 521 | // Clear existing cached key 522 | h.clearSingle(target).then(r => { 523 | res.status(HTTP_OK).json({ 524 | message: `cache cleared for key (${hasQueryString ? 'with' : 'without'} params): ${target}`, 525 | status: HTTP_OK 526 | }); 527 | }); 528 | } else { 529 | /** 530 | * Empty reply means the key does not exist. 531 | * Must use HTTP_OK with express as HTTP's RFC stats 204 should not 532 | * provide a body, message would then be lost. 533 | */ 534 | res.status(HTTP_OK).json({ 535 | message: `cache already cleared for key (${hasQueryString ? 'with' : 'without'} params): ${target}`, 536 | status: HTTP_NO_CONTENT 537 | }); 538 | } 539 | } 540 | }); 541 | } else { 542 | res.status(HTTP_NOT_FOUND).end(); 543 | } 544 | }); // clear a group 545 | 546 | router.get('/clear/group/*', (req, res) => { 547 | let target = decodeURIComponent(req.params[0]); // Target should always be defined as Express router raises 404 548 | // as route is not handled 549 | 550 | if (target.length) { 551 | target = 'group-' + target; // Returns elements of the list associated to the target/key 0 being the 552 | // first and -1 specifying get all till the latest 553 | 554 | client.lrange(target, 0, -1, (err, reply) => { 555 | if (err) { 556 | res.status(HTTP_SERVER_ERROR).json({ 557 | message: 'something went wrong' + err.message 558 | }); 559 | } else { 560 | // If the list/group existed and contains something 561 | if (reply && Array.isArray(reply) && reply.length > 0) { 562 | // Clear existing cached group key 563 | h.clearGroup(target).then(r => { 564 | res.status(HTTP_OK).json({ 565 | message: `cache cleared for the group key: ${decodeURIComponent(req.params[0])}`, 566 | status: HTTP_OK 567 | }); 568 | }); 569 | } else { 570 | /** 571 | * Empty reply means the key does not exist. 572 | * Must use HTTP_OK with express as HTTP's RFC stats 204 should not 573 | * provide a body, message would then be lost. 574 | */ 575 | res.status(HTTP_OK).json({ 576 | message: `cache already cleared for the group key: ${decodeURIComponent(req.params[0])}`, 577 | status: HTTP_NO_CONTENT 578 | }); 579 | } 580 | } 581 | }); 582 | } else { 583 | res.status(HTTP_NOT_FOUND).end(); 584 | } 585 | }); // add route to display cache index 586 | // this has been removed for performance issues 587 | // router.get('/index', (req, res) => { 588 | // let results = new Set(); 589 | // h.scanAsync('0', '*', results) 590 | // .then(data => { 591 | // res.status(200).json(data); 592 | // }) 593 | // .catch(err => { 594 | // res.status(404).json(err); 595 | // }); 596 | // }); 597 | 598 | return router; 599 | } 600 | 601 | var _default = routes; 602 | exports.default = _default; 603 | module.exports = exports.default; 604 | 605 | /***/ }), 606 | 607 | /***/ "./src/routes/helpers/redis.js": 608 | /*!*************************************!*\ 609 | !*** ./src/routes/helpers/redis.js ***! 610 | \*************************************/ 611 | /*! no static exports found */ 612 | /***/ (function(module, exports, __webpack_require__) { 613 | 614 | "use strict"; 615 | 616 | 617 | Object.defineProperty(exports, "__esModule", { 618 | value: true 619 | }); 620 | exports.default = void 0; 621 | 622 | class RedisCache { 623 | constructor(client) { 624 | this.client = client; 625 | } 626 | /** 627 | * scan the redis index 628 | */ 629 | // scan() { 630 | // // starts at 0 if cursor is again 0 it means the iteration is finished 631 | // let cursor = '0'; 632 | // return new Promise((resolve, reject) => { 633 | // this.client.scan(cursor, 'MATCH', '*', 'COUNT', '100', (err, reply) => { 634 | // if (err) { 635 | // reject(err); 636 | // } 637 | // cursor = reply[0]; 638 | // if (cursor === '0') { 639 | // resolve(reply[1]); 640 | // } else { 641 | // // do your processing 642 | // // reply[1] is an array of matched keys. 643 | // // console.log(reply[1]); 644 | // return this.scan(); 645 | // } 646 | // return false; 647 | // }); 648 | // }); 649 | // } 650 | 651 | /** 652 | * Async scan of the redis index 653 | * Do not for get to passin a Set 654 | * myResults = new Set(); 655 | * 656 | * scanAsync('0', "NOC-*[^listen]*", myResults).map( 657 | * myResults => { console.log( myResults); } 658 | * ); 659 | * 660 | * @param {String} cursor - string '0' 661 | * @param {String} patern - string '0' 662 | * @param {Set} returnSet - pass a set to have unique keys 663 | */ 664 | // scanAsync(cursor, pattern, returnSet) { 665 | // // starts at 0 if cursor is again 0 it means the iteration is finished 666 | // return new Promise((resolve, reject) => { 667 | // this.client.scan(cursor, 'MATCH', pattern, 'COUNT', '100', (err, reply) => { 668 | // if (err) { 669 | // reject(err); 670 | // } 671 | // cursor = reply[0]; 672 | // const keys = reply[1]; 673 | // keys.forEach((key, i) => { 674 | // returnSet.add(key); 675 | // }); 676 | // if (cursor === '0') { 677 | // resolve(Array.from(returnSet)); 678 | // } 679 | // return this.scanAsync(cursor, pattern, returnSet); 680 | // }); 681 | // }); 682 | // } 683 | 684 | /** 685 | * Clean single item from the cache 686 | * @param {string} key - the key to find in redis 687 | */ 688 | 689 | 690 | clearSingle(key) { 691 | return new Promise((resolve, reject) => { 692 | this.client.del(`${key}`, (err, reply) => { 693 | if (err) reject(false); 694 | 695 | if (reply === 1) { 696 | resolve(true); 697 | } 698 | 699 | resolve(false); 700 | }); 701 | }); 702 | } 703 | /** 704 | * Clear a group 705 | * @param {string} key - key of the group to clean 706 | */ 707 | 708 | 709 | clearGroup(key) { 710 | return new Promise((resolve, reject) => { 711 | this.client.lrange(key, 0, -1, (err, reply) => { 712 | if (err) { 713 | reject(err); 714 | } 715 | 716 | this.clearAll(reply).then(this.client.del(key, (e, r) => { 717 | resolve(r === 1); 718 | })); 719 | }); 720 | }); 721 | } 722 | /** 723 | * Clear all keys of a redis list 724 | * @param {Object[]} array 725 | */ 726 | 727 | 728 | clearAll(array) { 729 | return new Promise(resolve => { 730 | if (!array.length) resolve(false); 731 | let i = 0; 732 | 733 | for (i; i < array.length; i++) { 734 | this.clearSingle(array[i]).then(r => { 735 | if (i === array.length - 1) { 736 | resolve(r); 737 | } 738 | }); 739 | } 740 | }); 741 | } 742 | 743 | } 744 | 745 | exports.default = RedisCache; 746 | ; 747 | module.exports = exports.default; 748 | 749 | /***/ }), 750 | 751 | /***/ "chalk": 752 | /*!************************!*\ 753 | !*** external "chalk" ***! 754 | \************************/ 755 | /*! no static exports found */ 756 | /***/ (function(module, exports) { 757 | 758 | module.exports = require("chalk"); 759 | 760 | /***/ }), 761 | 762 | /***/ "express": 763 | /*!**************************!*\ 764 | !*** external "express" ***! 765 | \**************************/ 766 | /*! no static exports found */ 767 | /***/ (function(module, exports) { 768 | 769 | module.exports = require("express"); 770 | 771 | /***/ }), 772 | 773 | /***/ "moment": 774 | /*!*************************!*\ 775 | !*** external "moment" ***! 776 | \*************************/ 777 | /*! no static exports found */ 778 | /***/ (function(module, exports) { 779 | 780 | module.exports = require("moment"); 781 | 782 | /***/ }), 783 | 784 | /***/ "qs": 785 | /*!*********************!*\ 786 | !*** external "qs" ***! 787 | \*********************/ 788 | /*! no static exports found */ 789 | /***/ (function(module, exports) { 790 | 791 | module.exports = require("qs"); 792 | 793 | /***/ }), 794 | 795 | /***/ "redis": 796 | /*!************************!*\ 797 | !*** external "redis" ***! 798 | \************************/ 799 | /*! no static exports found */ 800 | /***/ (function(module, exports) { 801 | 802 | module.exports = require("redis"); 803 | 804 | /***/ }) 805 | 806 | /******/ }); 807 | }); 808 | //# sourceMappingURL=library.js.map -------------------------------------------------------------------------------- /lib/library.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack://library/webpack/universalModuleDefinition","webpack://library/webpack/bootstrap","webpack://library/./src/hooks/cache.js","webpack://library/./src/hooks/helpers/path.js","webpack://library/./src/hooks/redis.js","webpack://library/./src/index.js","webpack://library/./src/redisClient.js","webpack://library/./src/routes/cache.js","webpack://library/./src/routes/helpers/redis.js","webpack://library/external \"chalk\"","webpack://library/external \"express\"","webpack://library/external \"moment\"","webpack://library/external \"qs\"","webpack://library/external \"redis\""],"names":["defaults","defaultDuration","cache","options","hook","cacheOptions","app","get","Object","assign","result","hasOwnProperty","Array","isArray","array","wrapped","cached","duration","Promise","resolve","removeCacheInformation","parseNestedPath","path","params","re","RegExp","match","exec","keys","route","includes","replace","parsePath","config","removePathFromCacheKey","parseNestedRoutes","q","query","remove","id","length","qs","stringify","encode","env","immediateCacheKey","before","client","err","reply","data","JSON","parse","expiresOn","format","console","log","chalk","cyan","green","cacheKey","after","add","moment","parent","group","key","set","expire","rpush","humanize","redisClient","cacheRoutes","hookCache","hookRemoveCacheInformation","redisBeforeHook","redisAfterHook","retryInterval","redisOptions","retry_strategy","undefined","yellow","redis","createClient","on","HTTP_OK","HTTP_NO_CONTENT","HTTP_SERVER_ERROR","HTTP_NOT_FOUND","routes","router","express","Router","h","RedisCache","req","res","flushall","status","json","message","target","decodeURIComponent","hasQueryString","url","split","slice","join","clearSingle","then","r","end","lrange","clearGroup","constructor","reject","del","clearAll","e","i"],"mappings":"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","file":"library.js","sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine(\"library\", [], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"library\"] = factory();\n\telse\n\t\troot[\"library\"] = factory();\n})(global, function() {\nreturn "," \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = \"./src/index.js\");\n","/**\n * After hook - generates a cache object that is needed\n * for the redis hook and the express middelware.\n */\nconst defaults = {\n defaultDuration: 3600 * 24\n};\n\nexport function cache(options) { // eslint-disable-line no-unused-vars\n return function (hook) {\n const cacheOptions = hook.app.get('redisCache');\n\n options = Object.assign({}, defaults, cacheOptions, options);\n\n if (!hook.result.hasOwnProperty('cache')) {\n let cache = {};\n\n if (Array.isArray(hook.result)) {\n const array = hook.result;\n\n cache.wrapped = array;\n hook.result = {};\n }\n\n cache = Object.assign({}, cache, {\n cached: false,\n duration: options.duration || options.defaultDuration\n });\n\n hook.result.cache = cache;\n }\n return Promise.resolve(hook);\n };\n};\n\nexport function removeCacheInformation(options) { // eslint-disable-line no-unused-vars\n return function (hook) {\n if (hook.result.hasOwnProperty('cache')) {\n delete hook.result.cache;\n }\n return Promise.resolve(hook);\n };\n};\n","import qs from 'qs';\n\nfunction parseNestedPath(path, params) {\n const re = new RegExp(':([^\\\\/\\\\?]+)\\\\??', 'g');\n let match = null;\n\n while ((match = re.exec(path)) !== null) {\n if (Object.keys(params.route).includes(match[1])) {\n path = path.replace(match[0], params.route[match[1]]);\n }\n }\n\n return path;\n}\n\nfunction parsePath(hook, config = {removePathFromCacheKey: false, parseNestedRoutes: false}) {\n const q = hook.params.query || {};\n const remove = config.removePathFromCacheKey;\n const parseNestedRoutes = config.parseNestedRoutes;\n let path = remove && hook.id ? '' : `${hook.path}`;\n\n if (!remove && parseNestedRoutes) {\n path = parseNestedPath(path, hook.params);\n }\n\n if (hook.id) {\n if (path.length !== 0 && !remove) {\n path += '/';\n }\n if (Object.keys(q).length > 0) {\n path += `${hook.id}?${qs.stringify(q, { encode: false })}`;\n } else {\n path += `${hook.id}`;\n }\n } else {\n if (Object.keys(q).length > 0) {\n path += `?${qs.stringify(q, { encode: false })}`;\n }\n }\n\n return path;\n}\n\nexport { parsePath };\n","import moment from 'moment';\nimport chalk from 'chalk';\nimport { parsePath } from './helpers/path';\n\nconst defaults = {\n env: 'production',\n defaultDuration: 3600 * 24,\n immediateCacheKey: false\n};\n\nexport function before(options) { // eslint-disable-line no-unused-vars\n return function (hook) {\n const cacheOptions = hook.app.get('redisCache');\n\n options = Object.assign({}, defaults, cacheOptions, options);\n\n return new Promise(resolve => {\n const client = hook.app.get('redisClient');\n\n if (!client) {\n resolve(hook);\n }\n\n const path = parsePath(hook, options);\n\n client.get(path, (err, reply) => {\n if (err !== null) resolve(hook);\n if (reply) {\n let data = JSON.parse(reply);\n const duration = moment(data.cache.expiresOn).format('DD MMMM YYYY - HH:mm:ss');\n\n hook.result = data;\n resolve(hook);\n\n /* istanbul ignore next */\n if (options.env !== 'test') {\n console.log(`${chalk.cyan('[redis]')} returning cached value for ${chalk.green(path)}.`);\n console.log(`> Expires on ${duration}.`);\n }\n } else {\n if (options.immediateCacheKey === true) {\n hook.params.cacheKey = path;\n }\n resolve(hook);\n }\n });\n });\n };\n};\n\nexport function after(options) { // eslint-disable-line no-unused-vars\n return function (hook) {\n const cacheOptions = hook.app.get('redisCache');\n\n options = Object.assign({}, defaults, cacheOptions, options);\n\n return new Promise(resolve => {\n if (!hook.result.cache.cached) {\n const duration = hook.result.cache.duration || options.defaultDuration;\n const client = hook.app.get('redisClient');\n\n if (!client) {\n resolve(hook);\n }\n\n const path = hook.params.cacheKey || parsePath(hook, options);\n\n // adding a cache object\n Object.assign(hook.result.cache, {\n cached: true,\n duration: duration,\n expiresOn: moment().add(moment.duration(duration, 'seconds')),\n parent: hook.path,\n group: hook.path ? `group-${hook.path}` : '',\n key: path\n });\n\n client.set(path, JSON.stringify(hook.result));\n client.expire(path, hook.result.cache.duration);\n if (hook.path) {\n client.rpush(hook.result.cache.group, path);\n }\n\n /* istanbul ignore next */\n if (options.env !== 'test') {\n console.log(`${chalk.cyan('[redis]')} added ${chalk.green(path)} to the cache.`);\n console.log(`> Expires in ${moment.duration(duration, 'seconds').humanize()}.`);\n }\n }\n\n if (hook.result.cache.hasOwnProperty('wrapped')) {\n const { wrapped } = hook.result.cache;\n\n hook.result = wrapped;\n }\n\n resolve(hook);\n });\n };\n};\n\n","import cacheRoutes from './routes/cache';\nimport redisClient from './redisClient';\nimport { cache as hookCache } from './hooks/cache';\nimport { removeCacheInformation as hookRemoveCacheInformation } from './hooks/cache';\nimport { before as redisBeforeHook} from './hooks/redis';\nimport { after as redisAfterHook} from './hooks/redis';\n\nexport default {\n redisClient,\n cacheRoutes,\n hookCache,\n hookRemoveCacheInformation,\n redisBeforeHook,\n redisAfterHook\n};\n","import redis from 'redis';\nimport chalk from 'chalk';\n\nexport default function redisClient() { // eslint-disable-line no-unused-vars\n const app = this;\n const cacheOptions = app.get('redisCache') || {};\n const retryInterval = cacheOptions.retryInterval || 10000;\n const redisOptions = Object.assign({}, this.get('redis'), {\n retry_strategy: function (options) { // eslint-disable-line camelcase\n app.set('redisClient', undefined);\n /* istanbul ignore next */\n if (cacheOptions.env !== 'test') {\n console.log(`${chalk.yellow('[redis]')} not connected`);\n }\n return retryInterval;\n }\n });\n const client = redis.createClient(redisOptions);\n\n app.set('redisClient', client);\n\n client.on('ready', () => {\n app.set('redisClient', client);\n /* istanbul ignore next */\n if (cacheOptions.env !== 'test') {\n console.log(`${chalk.green('[redis]')} connected`);\n }\n });\n return this;\n}\n","import express from 'express';\n\nimport RedisCache from './helpers/redis';\n\nconst HTTP_OK = 200;\nconst HTTP_NO_CONTENT = 204;\nconst HTTP_SERVER_ERROR = 500;\nconst HTTP_NOT_FOUND = 404;\n\nfunction routes(app) {\n const router = express.Router();\n const client = app.get('redisClient');\n const h = new RedisCache(client);\n\n router.get('/clear', (req, res) => {\n client.flushall('ASYNC', () => {\n res.status(HTTP_OK).json({\n message: 'Cache cleared',\n status: HTTP_OK\n });\n });\n }); // clear a unique route\n\n // clear a unique route\n router.get('/clear/single/*', (req, res) => {\n let target = decodeURIComponent(req.params[0]);\n // Formated options following ?\n const query = req.query;\n const hasQueryString = (query && (Object.keys(query).length !== 0));\n\n // Target should always be defined as Express router raises 404\n // as route is not handled\n if (target.length) {\n if (hasQueryString) {\n // Keep queries in a single string with the taget\n target = decodeURIComponent(req.url.split('/').slice(3).join('/'));\n }\n\n // Gets the value of a key in the redis client\n client.get(`${target}`, (err, reply) => {\n if (err) {\n res.status(HTTP_SERVER_ERROR).json({\n message: 'something went wrong' + err.message\n });\n } else {\n // If the key existed\n if (reply) {\n // Clear existing cached key\n h.clearSingle(target).then(r => {\n res.status(HTTP_OK).json({\n message: `cache cleared for key (${hasQueryString ?\n 'with' : 'without'} params): ${target}`,\n status: HTTP_OK\n });\n });\n } else {\n /**\n * Empty reply means the key does not exist.\n * Must use HTTP_OK with express as HTTP's RFC stats 204 should not\n * provide a body, message would then be lost.\n */\n res.status(HTTP_OK).json({\n message: `cache already cleared for key (${hasQueryString ?\n 'with' : 'without'} params): ${target}`,\n status: HTTP_NO_CONTENT\n });\n }\n\n }\n });\n } else {\n res.status(HTTP_NOT_FOUND).end();\n }\n });\n\n // clear a group\n router.get('/clear/group/*', (req, res) => {\n let target = decodeURIComponent(req.params[0]);\n\n // Target should always be defined as Express router raises 404\n // as route is not handled\n if (target.length) {\n target = 'group-' + target;\n // Returns elements of the list associated to the target/key 0 being the\n // first and -1 specifying get all till the latest\n client.lrange(target, 0, -1, (err, reply) => {\n if (err) {\n res.status(HTTP_SERVER_ERROR).json({\n message: 'something went wrong' + err.message\n });\n } else {\n // If the list/group existed and contains something\n if (reply && Array.isArray(reply) && (reply.length > 0)) {\n // Clear existing cached group key\n h.clearGroup(target).then(r => {\n res.status(HTTP_OK).json({\n message:\n `cache cleared for the group key: ${decodeURIComponent(req.params[0])}`,\n status: HTTP_OK\n });\n });\n } else {\n /**\n * Empty reply means the key does not exist.\n * Must use HTTP_OK with express as HTTP's RFC stats 204 should not\n * provide a body, message would then be lost.\n */\n res.status(HTTP_OK).json({\n message:\n `cache already cleared for the group key: ${decodeURIComponent(req.params[0])}`,\n status: HTTP_NO_CONTENT\n });\n }\n }\n });\n } else {\n res.status(HTTP_NOT_FOUND).end();\n }\n });\n\n // add route to display cache index\n // this has been removed for performance issues\n // router.get('/index', (req, res) => {\n // let results = new Set();\n\n // h.scanAsync('0', '*', results)\n // .then(data => {\n // res.status(200).json(data);\n // })\n // .catch(err => {\n // res.status(404).json(err);\n // });\n\n // });\n\n return router;\n}\n\nexport default routes;\n","export default class RedisCache {\n constructor(client) {\n this.client = client;\n }\n\n /**\n * scan the redis index\n */\n // scan() {\n // // starts at 0 if cursor is again 0 it means the iteration is finished\n // let cursor = '0';\n\n // return new Promise((resolve, reject) => {\n // this.client.scan(cursor, 'MATCH', '*', 'COUNT', '100', (err, reply) => {\n // if (err) {\n // reject(err);\n // }\n\n // cursor = reply[0];\n // if (cursor === '0') {\n // resolve(reply[1]);\n // } else {\n // // do your processing\n // // reply[1] is an array of matched keys.\n // // console.log(reply[1]);\n // return this.scan();\n // }\n // return false;\n // });\n\n // });\n // }\n\n /**\n * Async scan of the redis index\n * Do not for get to passin a Set\n * myResults = new Set();\n *\n * scanAsync('0', \"NOC-*[^listen]*\", myResults).map(\n * myResults => { console.log( myResults); }\n * );\n *\n * @param {String} cursor - string '0'\n * @param {String} patern - string '0'\n * @param {Set} returnSet - pass a set to have unique keys\n */\n // scanAsync(cursor, pattern, returnSet) {\n // // starts at 0 if cursor is again 0 it means the iteration is finished\n\n // return new Promise((resolve, reject) => {\n // this.client.scan(cursor, 'MATCH', pattern, 'COUNT', '100', (err, reply) => {\n\n // if (err) {\n // reject(err);\n // }\n\n // cursor = reply[0];\n // const keys = reply[1];\n\n // keys.forEach((key, i) => {\n // returnSet.add(key);\n // });\n\n // if (cursor === '0') {\n // resolve(Array.from(returnSet));\n // }\n\n // return this.scanAsync(cursor, pattern, returnSet);\n // });\n // });\n // }\n\n /**\n * Clean single item from the cache\n * @param {string} key - the key to find in redis\n */\n clearSingle(key) {\n return new Promise((resolve, reject) => {\n this.client.del(`${key}`, (err, reply) => {\n if (err) reject(false);\n if (reply === 1) {\n resolve(true);\n }\n resolve(false);\n });\n });\n }\n\n /**\n * Clear a group\n * @param {string} key - key of the group to clean\n */\n clearGroup(key) {\n return new Promise((resolve, reject) => {\n this.client.lrange(key, 0, -1, (err, reply) => {\n if (err) {\n reject(err);\n }\n this.clearAll(reply).then(\n this.client.del(key, (e, r) => {\n resolve(r === 1);\n })\n );\n });\n });\n }\n\n /**\n * Clear all keys of a redis list\n * @param {Object[]} array\n */\n clearAll(array) {\n return new Promise(resolve => {\n if (!array.length) resolve(false);\n let i = 0;\n\n for (i; i < array.length; i++) {\n this.clearSingle(array[i]).then(r => {\n if (i === array.length - 1) {\n resolve(r);\n }\n });\n }\n });\n }\n};\n","module.exports = require(\"chalk\");","module.exports = require(\"express\");","module.exports = require(\"moment\");","module.exports = require(\"qs\");","module.exports = require(\"redis\");"],"sourceRoot":""} -------------------------------------------------------------------------------- /lib/library.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("library",[],t):"object"==typeof exports?exports.library=t():e.library=t()}(global,function(){return function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=11)}([function(e,t){e.exports=require("chalk")},function(e,t){e.exports=require("qs")},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.parsePath=function(e,t={removePathFromCacheKey:!1,parseNestedRoutes:!1}){const r=e.params.query||{},n=t.removePathFromCacheKey,s=t.parseNestedRoutes;let a=n&&e.id?"":`${e.path}`;!n&&s&&(a=function(e,t){const r=new RegExp(":([^\\/\\?]+)\\??","g");let n=null;for(;null!==(n=r.exec(e));)Object.keys(t.route).includes(n[1])&&(e=e.replace(n[0],t.route[n[1]]));return e}(a,e.params));e.id?(0===a.length||n||(a+="/"),Object.keys(r).length>0?a+=`${e.id}?${o.default.stringify(r,{encode:!1})}`:a+=`${e.id}`):Object.keys(r).length>0&&(a+=`?${o.default.stringify(r,{encode:!1})}`);return a};var n,o=(n=r(1))&&n.__esModule?n:{default:n}},function(e,t){e.exports=require("moment")},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.before=function(e){return function(t){const r=t.app.get("redisCache");return e=Object.assign({},u,r,e),new Promise(r=>{const a=t.app.get("redisClient");a||r(t);const u=(0,s.parsePath)(t,e);a.get(u,(s,a)=>{if(null!==s&&r(t),a){let s=JSON.parse(a);const c=(0,n.default)(s.cache.expiresOn).format("DD MMMM YYYY - HH:mm:ss");t.result=s,r(t),"test"!==e.env&&(console.log(`${o.default.cyan("[redis]")} returning cached value for ${o.default.green(u)}.`),console.log(`> Expires on ${c}.`))}else!0===e.immediateCacheKey&&(t.params.cacheKey=u),r(t)})})}},t.after=function(e){return function(t){const r=t.app.get("redisCache");return e=Object.assign({},u,r,e),new Promise(r=>{if(!t.result.cache.cached){const a=t.result.cache.duration||e.defaultDuration,u=t.app.get("redisClient");u||r(t);const c=t.params.cacheKey||(0,s.parsePath)(t,e);Object.assign(t.result.cache,{cached:!0,duration:a,expiresOn:(0,n.default)().add(n.default.duration(a,"seconds")),parent:t.path,group:t.path?`group-${t.path}`:"",key:c}),u.set(c,JSON.stringify(t.result)),u.expire(c,t.result.cache.duration),t.path&&u.rpush(t.result.cache.group,c),"test"!==e.env&&(console.log(`${o.default.cyan("[redis]")} added ${o.default.green(c)} to the cache.`),console.log(`> Expires in ${n.default.duration(a,"seconds").humanize()}.`))}if(t.result.cache.hasOwnProperty("wrapped")){const{wrapped:e}=t.result.cache;t.result=e}r(t)})}};var n=a(r(3)),o=a(r(0)),s=r(2);function a(e){return e&&e.__esModule?e:{default:e}}const u={env:"production",defaultDuration:86400,immediateCacheKey:!1}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.cache=function(e){return function(t){const r=t.app.get("redisCache");if(e=Object.assign({},n,r,e),!t.result.hasOwnProperty("cache")){let r={};if(Array.isArray(t.result)){const e=t.result;r.wrapped=e,t.result={}}r=Object.assign({},r,{cached:!1,duration:e.duration||e.defaultDuration}),t.result.cache=r}return Promise.resolve(t)}},t.removeCacheInformation=function(e){return function(e){return e.result.hasOwnProperty("cache")&&delete e.result.cache,Promise.resolve(e)}};const n={defaultDuration:86400}},function(e,t){e.exports=require("redis")},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(){const e=this,t=e.get("redisCache")||{},r=t.retryInterval||1e4,s=Object.assign({},this.get("redis"),{retry_strategy:function(n){return e.set("redisClient",void 0),"test"!==t.env&&console.log(`${o.default.yellow("[redis]")} not connected`),r}}),a=n.default.createClient(s);return e.set("redisClient",a),a.on("ready",()=>{e.set("redisClient",a),"test"!==t.env&&console.log(`${o.default.green("[redis]")} connected`)}),this};var n=s(r(6)),o=s(r(0));function s(e){return e&&e.__esModule?e:{default:e}}e.exports=t.default},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;t.default=class{constructor(e){this.client=e}clearSingle(e){return new Promise((t,r)=>{this.client.del(`${e}`,(e,n)=>{e&&r(!1),1===n&&t(!0),t(!1)})})}clearGroup(e){return new Promise((t,r)=>{this.client.lrange(e,0,-1,(n,o)=>{n&&r(n),this.clearAll(o).then(this.client.del(e,(e,r)=>{t(1===r)}))})})}clearAll(e){return new Promise(t=>{e.length||t(!1);let r=0;for(;r{r===e.length-1&&t(n)})})}},e.exports=t.default},function(e,t){e.exports=require("express")},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var n=s(r(9)),o=s(r(8));function s(e){return e&&e.__esModule?e:{default:e}}const a=200,u=204,c=500,l=404;var i=function(e){const t=n.default.Router(),r=e.get("redisClient"),s=new o.default(r);return t.get("/clear",(e,t)=>{r.flushall("ASYNC",()=>{t.status(a).json({message:"Cache cleared",status:a})})}),t.get("/clear/single/*",(e,t)=>{let n=decodeURIComponent(e.params[0]);const o=e.query,i=o&&0!==Object.keys(o).length;n.length?(i&&(n=decodeURIComponent(e.url.split("/").slice(3).join("/"))),r.get(`${n}`,(e,r)=>{e?t.status(c).json({message:"something went wrong"+e.message}):r?s.clearSingle(n).then(e=>{t.status(a).json({message:`cache cleared for key (${i?"with":"without"} params): ${n}`,status:a})}):t.status(a).json({message:`cache already cleared for key (${i?"with":"without"} params): ${n}`,status:u})})):t.status(l).end()}),t.get("/clear/group/*",(e,t)=>{let n=decodeURIComponent(e.params[0]);n.length?(n="group-"+n,r.lrange(n,0,-1,(r,o)=>{r?t.status(c).json({message:"something went wrong"+r.message}):o&&Array.isArray(o)&&o.length>0?s.clearGroup(n).then(r=>{t.status(a).json({message:`cache cleared for the group key: ${decodeURIComponent(e.params[0])}`,status:a})}):t.status(a).json({message:`cache already cleared for the group key: ${decodeURIComponent(e.params[0])}`,status:u})})):t.status(l).end()}),t};t.default=i,e.exports=t.default},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var n=u(r(10)),o=u(r(7)),s=r(5),a=r(4);function u(e){return e&&e.__esModule?e:{default:e}}var c={redisClient:o.default,cacheRoutes:n.default,hookCache:s.cache,hookRemoveCacheInformation:s.removeCacheInformation,redisBeforeHook:a.before,redisAfterHook:a.after};t.default=c,e.exports=t.default}])}); 2 | //# sourceMappingURL=library.min.js.map -------------------------------------------------------------------------------- /lib/library.min.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack://library/webpack/universalModuleDefinition","webpack://library/webpack/bootstrap","webpack://library/external \"chalk\"","webpack://library/external \"qs\"","webpack://library/./src/hooks/helpers/path.js","webpack://library/external \"moment\"","webpack://library/./src/hooks/redis.js","webpack://library/./src/hooks/cache.js","webpack://library/external \"redis\"","webpack://library/./src/redisClient.js","webpack://library/./src/routes/helpers/redis.js","webpack://library/external \"express\"","webpack://library/./src/routes/cache.js","webpack://library/./src/index.js"],"names":["root","factory","exports","module","define","amd","global","installedModules","__webpack_require__","moduleId","i","l","modules","call","m","c","d","name","getter","o","Object","defineProperty","enumerable","get","r","Symbol","toStringTag","value","t","mode","__esModule","ns","create","key","bind","n","object","property","prototype","hasOwnProperty","p","s","require","hook","config","removePathFromCacheKey","parseNestedRoutes","q","params","query","remove","path","id","re","RegExp","match","exec","keys","route","includes","replace","parseNestedPath","length","qs","stringify","encode","_qs","options","cacheOptions","app","assign","defaults","Promise","resolve","client","_path","parsePath","err","reply","data","JSON","parse","duration","_moment","default","cache","expiresOn","format","result","env","console","log","chalk","cyan","green","immediateCacheKey","cacheKey","cached","defaultDuration","add","moment","parent","group","set","expire","rpush","humanize","wrapped","_interopRequireDefault","_chalk","Array","isArray","array","this","retryInterval","redisOptions","retry_strategy","undefined","yellow","redis","createClient","on","_redis","constructor","clearSingle","reject","del","clearGroup","lrange","clearAll","then","e","_express","HTTP_OK","HTTP_NO_CONTENT","HTTP_SERVER_ERROR","HTTP_NOT_FOUND","router","express","Router","h","RedisCache","req","res","flushall","status","json","message","target","decodeURIComponent","hasQueryString","url","split","slice","join","end","_cache","_redisClient","_cache2","redisClient","cacheRoutes","hookCache","hookRemoveCacheInformation","redisBeforeHook","redisAfterHook"],"mappings":"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","file":"library.min.js","sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine(\"library\", [], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"library\"] = factory();\n\telse\n\t\troot[\"library\"] = factory();\n})(global, function() {\nreturn "," \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 11);\n","module.exports = require(\"chalk\");","module.exports = require(\"qs\");","import qs from 'qs';\n\nfunction parseNestedPath(path, params) {\n const re = new RegExp(':([^\\\\/\\\\?]+)\\\\??', 'g');\n let match = null;\n\n while ((match = re.exec(path)) !== null) {\n if (Object.keys(params.route).includes(match[1])) {\n path = path.replace(match[0], params.route[match[1]]);\n }\n }\n\n return path;\n}\n\nfunction parsePath(hook, config = {removePathFromCacheKey: false, parseNestedRoutes: false}) {\n const q = hook.params.query || {};\n const remove = config.removePathFromCacheKey;\n const parseNestedRoutes = config.parseNestedRoutes;\n let path = remove && hook.id ? '' : `${hook.path}`;\n\n if (!remove && parseNestedRoutes) {\n path = parseNestedPath(path, hook.params);\n }\n\n if (hook.id) {\n if (path.length !== 0 && !remove) {\n path += '/';\n }\n if (Object.keys(q).length > 0) {\n path += `${hook.id}?${qs.stringify(q, { encode: false })}`;\n } else {\n path += `${hook.id}`;\n }\n } else {\n if (Object.keys(q).length > 0) {\n path += `?${qs.stringify(q, { encode: false })}`;\n }\n }\n\n return path;\n}\n\nexport { parsePath };\n","module.exports = require(\"moment\");","import moment from 'moment';\nimport chalk from 'chalk';\nimport { parsePath } from './helpers/path';\n\nconst defaults = {\n env: 'production',\n defaultDuration: 3600 * 24,\n immediateCacheKey: false\n};\n\nexport function before(options) { // eslint-disable-line no-unused-vars\n return function (hook) {\n const cacheOptions = hook.app.get('redisCache');\n\n options = Object.assign({}, defaults, cacheOptions, options);\n\n return new Promise(resolve => {\n const client = hook.app.get('redisClient');\n\n if (!client) {\n resolve(hook);\n }\n\n const path = parsePath(hook, options);\n\n client.get(path, (err, reply) => {\n if (err !== null) resolve(hook);\n if (reply) {\n let data = JSON.parse(reply);\n const duration = moment(data.cache.expiresOn).format('DD MMMM YYYY - HH:mm:ss');\n\n hook.result = data;\n resolve(hook);\n\n /* istanbul ignore next */\n if (options.env !== 'test') {\n console.log(`${chalk.cyan('[redis]')} returning cached value for ${chalk.green(path)}.`);\n console.log(`> Expires on ${duration}.`);\n }\n } else {\n if (options.immediateCacheKey === true) {\n hook.params.cacheKey = path;\n }\n resolve(hook);\n }\n });\n });\n };\n};\n\nexport function after(options) { // eslint-disable-line no-unused-vars\n return function (hook) {\n const cacheOptions = hook.app.get('redisCache');\n\n options = Object.assign({}, defaults, cacheOptions, options);\n\n return new Promise(resolve => {\n if (!hook.result.cache.cached) {\n const duration = hook.result.cache.duration || options.defaultDuration;\n const client = hook.app.get('redisClient');\n\n if (!client) {\n resolve(hook);\n }\n\n const path = hook.params.cacheKey || parsePath(hook, options);\n\n // adding a cache object\n Object.assign(hook.result.cache, {\n cached: true,\n duration: duration,\n expiresOn: moment().add(moment.duration(duration, 'seconds')),\n parent: hook.path,\n group: hook.path ? `group-${hook.path}` : '',\n key: path\n });\n\n client.set(path, JSON.stringify(hook.result));\n client.expire(path, hook.result.cache.duration);\n if (hook.path) {\n client.rpush(hook.result.cache.group, path);\n }\n\n /* istanbul ignore next */\n if (options.env !== 'test') {\n console.log(`${chalk.cyan('[redis]')} added ${chalk.green(path)} to the cache.`);\n console.log(`> Expires in ${moment.duration(duration, 'seconds').humanize()}.`);\n }\n }\n\n if (hook.result.cache.hasOwnProperty('wrapped')) {\n const { wrapped } = hook.result.cache;\n\n hook.result = wrapped;\n }\n\n resolve(hook);\n });\n };\n};\n\n","/**\n * After hook - generates a cache object that is needed\n * for the redis hook and the express middelware.\n */\nconst defaults = {\n defaultDuration: 3600 * 24\n};\n\nexport function cache(options) { // eslint-disable-line no-unused-vars\n return function (hook) {\n const cacheOptions = hook.app.get('redisCache');\n\n options = Object.assign({}, defaults, cacheOptions, options);\n\n if (!hook.result.hasOwnProperty('cache')) {\n let cache = {};\n\n if (Array.isArray(hook.result)) {\n const array = hook.result;\n\n cache.wrapped = array;\n hook.result = {};\n }\n\n cache = Object.assign({}, cache, {\n cached: false,\n duration: options.duration || options.defaultDuration\n });\n\n hook.result.cache = cache;\n }\n return Promise.resolve(hook);\n };\n};\n\nexport function removeCacheInformation(options) { // eslint-disable-line no-unused-vars\n return function (hook) {\n if (hook.result.hasOwnProperty('cache')) {\n delete hook.result.cache;\n }\n return Promise.resolve(hook);\n };\n};\n","module.exports = require(\"redis\");","import redis from 'redis';\nimport chalk from 'chalk';\n\nexport default function redisClient() { // eslint-disable-line no-unused-vars\n const app = this;\n const cacheOptions = app.get('redisCache') || {};\n const retryInterval = cacheOptions.retryInterval || 10000;\n const redisOptions = Object.assign({}, this.get('redis'), {\n retry_strategy: function (options) { // eslint-disable-line camelcase\n app.set('redisClient', undefined);\n /* istanbul ignore next */\n if (cacheOptions.env !== 'test') {\n console.log(`${chalk.yellow('[redis]')} not connected`);\n }\n return retryInterval;\n }\n });\n const client = redis.createClient(redisOptions);\n\n app.set('redisClient', client);\n\n client.on('ready', () => {\n app.set('redisClient', client);\n /* istanbul ignore next */\n if (cacheOptions.env !== 'test') {\n console.log(`${chalk.green('[redis]')} connected`);\n }\n });\n return this;\n}\n","export default class RedisCache {\n constructor(client) {\n this.client = client;\n }\n\n /**\n * scan the redis index\n */\n // scan() {\n // // starts at 0 if cursor is again 0 it means the iteration is finished\n // let cursor = '0';\n\n // return new Promise((resolve, reject) => {\n // this.client.scan(cursor, 'MATCH', '*', 'COUNT', '100', (err, reply) => {\n // if (err) {\n // reject(err);\n // }\n\n // cursor = reply[0];\n // if (cursor === '0') {\n // resolve(reply[1]);\n // } else {\n // // do your processing\n // // reply[1] is an array of matched keys.\n // // console.log(reply[1]);\n // return this.scan();\n // }\n // return false;\n // });\n\n // });\n // }\n\n /**\n * Async scan of the redis index\n * Do not for get to passin a Set\n * myResults = new Set();\n *\n * scanAsync('0', \"NOC-*[^listen]*\", myResults).map(\n * myResults => { console.log( myResults); }\n * );\n *\n * @param {String} cursor - string '0'\n * @param {String} patern - string '0'\n * @param {Set} returnSet - pass a set to have unique keys\n */\n // scanAsync(cursor, pattern, returnSet) {\n // // starts at 0 if cursor is again 0 it means the iteration is finished\n\n // return new Promise((resolve, reject) => {\n // this.client.scan(cursor, 'MATCH', pattern, 'COUNT', '100', (err, reply) => {\n\n // if (err) {\n // reject(err);\n // }\n\n // cursor = reply[0];\n // const keys = reply[1];\n\n // keys.forEach((key, i) => {\n // returnSet.add(key);\n // });\n\n // if (cursor === '0') {\n // resolve(Array.from(returnSet));\n // }\n\n // return this.scanAsync(cursor, pattern, returnSet);\n // });\n // });\n // }\n\n /**\n * Clean single item from the cache\n * @param {string} key - the key to find in redis\n */\n clearSingle(key) {\n return new Promise((resolve, reject) => {\n this.client.del(`${key}`, (err, reply) => {\n if (err) reject(false);\n if (reply === 1) {\n resolve(true);\n }\n resolve(false);\n });\n });\n }\n\n /**\n * Clear a group\n * @param {string} key - key of the group to clean\n */\n clearGroup(key) {\n return new Promise((resolve, reject) => {\n this.client.lrange(key, 0, -1, (err, reply) => {\n if (err) {\n reject(err);\n }\n this.clearAll(reply).then(\n this.client.del(key, (e, r) => {\n resolve(r === 1);\n })\n );\n });\n });\n }\n\n /**\n * Clear all keys of a redis list\n * @param {Object[]} array\n */\n clearAll(array) {\n return new Promise(resolve => {\n if (!array.length) resolve(false);\n let i = 0;\n\n for (i; i < array.length; i++) {\n this.clearSingle(array[i]).then(r => {\n if (i === array.length - 1) {\n resolve(r);\n }\n });\n }\n });\n }\n};\n","module.exports = require(\"express\");","import express from 'express';\n\nimport RedisCache from './helpers/redis';\n\nconst HTTP_OK = 200;\nconst HTTP_NO_CONTENT = 204;\nconst HTTP_SERVER_ERROR = 500;\nconst HTTP_NOT_FOUND = 404;\n\nfunction routes(app) {\n const router = express.Router();\n const client = app.get('redisClient');\n const h = new RedisCache(client);\n\n router.get('/clear', (req, res) => {\n client.flushall('ASYNC', () => {\n res.status(HTTP_OK).json({\n message: 'Cache cleared',\n status: HTTP_OK\n });\n });\n }); // clear a unique route\n\n // clear a unique route\n router.get('/clear/single/*', (req, res) => {\n let target = decodeURIComponent(req.params[0]);\n // Formated options following ?\n const query = req.query;\n const hasQueryString = (query && (Object.keys(query).length !== 0));\n\n // Target should always be defined as Express router raises 404\n // as route is not handled\n if (target.length) {\n if (hasQueryString) {\n // Keep queries in a single string with the taget\n target = decodeURIComponent(req.url.split('/').slice(3).join('/'));\n }\n\n // Gets the value of a key in the redis client\n client.get(`${target}`, (err, reply) => {\n if (err) {\n res.status(HTTP_SERVER_ERROR).json({\n message: 'something went wrong' + err.message\n });\n } else {\n // If the key existed\n if (reply) {\n // Clear existing cached key\n h.clearSingle(target).then(r => {\n res.status(HTTP_OK).json({\n message: `cache cleared for key (${hasQueryString ?\n 'with' : 'without'} params): ${target}`,\n status: HTTP_OK\n });\n });\n } else {\n /**\n * Empty reply means the key does not exist.\n * Must use HTTP_OK with express as HTTP's RFC stats 204 should not\n * provide a body, message would then be lost.\n */\n res.status(HTTP_OK).json({\n message: `cache already cleared for key (${hasQueryString ?\n 'with' : 'without'} params): ${target}`,\n status: HTTP_NO_CONTENT\n });\n }\n\n }\n });\n } else {\n res.status(HTTP_NOT_FOUND).end();\n }\n });\n\n // clear a group\n router.get('/clear/group/*', (req, res) => {\n let target = decodeURIComponent(req.params[0]);\n\n // Target should always be defined as Express router raises 404\n // as route is not handled\n if (target.length) {\n target = 'group-' + target;\n // Returns elements of the list associated to the target/key 0 being the\n // first and -1 specifying get all till the latest\n client.lrange(target, 0, -1, (err, reply) => {\n if (err) {\n res.status(HTTP_SERVER_ERROR).json({\n message: 'something went wrong' + err.message\n });\n } else {\n // If the list/group existed and contains something\n if (reply && Array.isArray(reply) && (reply.length > 0)) {\n // Clear existing cached group key\n h.clearGroup(target).then(r => {\n res.status(HTTP_OK).json({\n message:\n `cache cleared for the group key: ${decodeURIComponent(req.params[0])}`,\n status: HTTP_OK\n });\n });\n } else {\n /**\n * Empty reply means the key does not exist.\n * Must use HTTP_OK with express as HTTP's RFC stats 204 should not\n * provide a body, message would then be lost.\n */\n res.status(HTTP_OK).json({\n message:\n `cache already cleared for the group key: ${decodeURIComponent(req.params[0])}`,\n status: HTTP_NO_CONTENT\n });\n }\n }\n });\n } else {\n res.status(HTTP_NOT_FOUND).end();\n }\n });\n\n // add route to display cache index\n // this has been removed for performance issues\n // router.get('/index', (req, res) => {\n // let results = new Set();\n\n // h.scanAsync('0', '*', results)\n // .then(data => {\n // res.status(200).json(data);\n // })\n // .catch(err => {\n // res.status(404).json(err);\n // });\n\n // });\n\n return router;\n}\n\nexport default routes;\n","import cacheRoutes from './routes/cache';\nimport redisClient from './redisClient';\nimport { cache as hookCache } from './hooks/cache';\nimport { removeCacheInformation as hookRemoveCacheInformation } from './hooks/cache';\nimport { before as redisBeforeHook} from './hooks/redis';\nimport { after as redisAfterHook} from './hooks/redis';\n\nexport default {\n redisClient,\n cacheRoutes,\n hookCache,\n hookRemoveCacheInformation,\n redisBeforeHook,\n redisAfterHook\n};\n"],"sourceRoot":""} -------------------------------------------------------------------------------- /mocha.opts: -------------------------------------------------------------------------------- 1 | --recursive test/ 2 | --require=env-test -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "feathers-hooks-rediscache", 3 | "description": "Cache any route with redis", 4 | "version": "1.1.4", 5 | "homepage": "https://github.com/idealley/feathers-hooks-rediscache", 6 | "main": "lib/library.min.js", 7 | "nyc": { 8 | "require": [ 9 | "@babel/register" 10 | ], 11 | "reporter": [ 12 | "lcov", 13 | "text" 14 | ], 15 | "sourceMap": false, 16 | "instrument": false 17 | }, 18 | "keywords": [ 19 | "feathers", 20 | "feathers-hooks", 21 | "redis", 22 | "cache" 23 | ], 24 | "license": "MIT", 25 | "repository": { 26 | "type": "git", 27 | "url": "git://github.com/renaudfv/feathers-hooks-rediscache.git" 28 | }, 29 | "author": { 30 | "name": "Samuel Pouyt", 31 | "email": "samuelpouyt@gmail.com" 32 | }, 33 | "contributors": [], 34 | "bugs": { 35 | "url": "https://github.com/idealley/feathers-hooks-rediscache/issues" 36 | }, 37 | "engines": { 38 | "node": ">= 6.0.0" 39 | }, 40 | "scripts": { 41 | "build": "webpack --env build --mode production", 42 | "dev": "webpack --progress --colors --watch --env dev --mode development", 43 | "dev-travis": "webpack --mode production && npm run test", 44 | "mocha": "mocha --compilers js:@babel/register --colors ./test/ --recursive --exit", 45 | "reporter": "NODE_ENV=test nyc npm run mocha", 46 | "test": "npm run reporter", 47 | "test:redis-after": "NODE_ENV=test nyc mocha --require @babel/register --colors ./test/hooks/redis-after.test.js --watch", 48 | "test:redis-before": "NODE_ENV=test nyc mocha --compilers js:@babel/register --colors ./test/hooks/redis-before.test.js --watch", 49 | "test:routes": "NODE_ENV=test nyc mocha --compilers js:@babel/register --colors ./test/routes.test.js --watch", 50 | "test:cache-hook": "NODE_ENV=test nyc mocha --compilers js:@babel/register --colors ./test/cache.test.js --watch", 51 | "test:watch": "mocha --compilers js:@babel/register --colors -w ./test/*.test.js", 52 | "coverage": "nyc report --reporter=text-lcov", 53 | "coveralls": "cat ./coverage/lcov.info | node node_modules/.bin/coveralls", 54 | "codecov": "nyc report --reporter=text-lcov > coverage.lcov && codecov" 55 | }, 56 | "dependencies": { 57 | "chalk": "^2.3.2", 58 | "express": "^4.16.3", 59 | "moment": "^2.21.0", 60 | "qs": "^6.5.1", 61 | "redis": "^2.8.0" 62 | }, 63 | "devDependencies": { 64 | "@babel/cli": "^7.0.0-beta.49", 65 | "@babel/core": "^7.0.0-beta.49", 66 | "@babel/preset-env": "^7.0.0-beta.49", 67 | "@babel/preset-es2015": "^7.0.0-beta.49", 68 | "@babel/register": "^7.0.0-beta.49", 69 | "babel-loader": "8.0.0", 70 | "babel-eslint": "8.2.6", 71 | "babel-plugin-add-module-exports": "0.3.3", 72 | "babel-plugin-istanbul": "^4.1.6", 73 | "body-parser": "^1.18.2", 74 | "chai": "4.1.2", 75 | "coveralls": "^3.0.0", 76 | "eslint": "5.0.1", 77 | "eslint-loader": "2.1.0", 78 | "feathers": "^2.2.4", 79 | "feathers-errors": "^2.9.2", 80 | "feathers-hooks": "^2.1.2", 81 | "feathers-rest": "^1.8.1", 82 | "istanbul": "^0.4.5", 83 | "mocha": "5.0.5", 84 | "nyc": "^12.0.1", 85 | "request-promise": "^4.2.2", 86 | "webpack": "^4.2.0", 87 | "webpack-cli": "^3.0.8", 88 | "yargs": "^12.0.1" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/hooks/cache.js: -------------------------------------------------------------------------------- 1 | /** 2 | * After hook - generates a cache object that is needed 3 | * for the redis hook and the express middelware. 4 | */ 5 | const defaults = { 6 | defaultDuration: 3600 * 24 7 | }; 8 | 9 | export function cache(options) { // eslint-disable-line no-unused-vars 10 | return function (hook) { 11 | const cacheOptions = hook.app.get('redisCache'); 12 | 13 | options = Object.assign({}, defaults, cacheOptions, options); 14 | 15 | if (!hook.result.hasOwnProperty('cache')) { 16 | let cache = {}; 17 | 18 | if (Array.isArray(hook.result)) { 19 | const array = hook.result; 20 | 21 | cache.wrapped = array; 22 | hook.result = {}; 23 | } 24 | 25 | cache = Object.assign({}, cache, { 26 | cached: false, 27 | duration: options.duration || options.defaultDuration 28 | }); 29 | 30 | hook.result.cache = cache; 31 | } 32 | return Promise.resolve(hook); 33 | }; 34 | }; 35 | 36 | export function removeCacheInformation(options) { // eslint-disable-line no-unused-vars 37 | return function (hook) { 38 | if (hook.result.hasOwnProperty('cache')) { 39 | delete hook.result.cache; 40 | } 41 | return Promise.resolve(hook); 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /src/hooks/helpers/path.js: -------------------------------------------------------------------------------- 1 | import qs from 'qs'; 2 | 3 | function parseNestedPath(path, params) { 4 | const re = new RegExp(':([^\\/\\?]+)\\??', 'g'); 5 | let match = null; 6 | 7 | while ((match = re.exec(path)) !== null) { 8 | if (Object.keys(params.route).includes(match[1])) { 9 | path = path.replace(match[0], params.route[match[1]]); 10 | } 11 | } 12 | 13 | return path; 14 | } 15 | 16 | function parsePath(hook, config = {removePathFromCacheKey: false, parseNestedRoutes: false}) { 17 | const q = hook.params.query || {}; 18 | const remove = config.removePathFromCacheKey; 19 | const parseNestedRoutes = config.parseNestedRoutes; 20 | let path = remove && hook.id ? '' : `${hook.path}`; 21 | 22 | if (!remove && parseNestedRoutes) { 23 | path = parseNestedPath(path, hook.params); 24 | } 25 | 26 | if (hook.id) { 27 | if (path.length !== 0 && !remove) { 28 | path += '/'; 29 | } 30 | if (Object.keys(q).length > 0) { 31 | path += `${hook.id}?${qs.stringify(q, { encode: false })}`; 32 | } else { 33 | path += `${hook.id}`; 34 | } 35 | } else { 36 | if (Object.keys(q).length > 0) { 37 | path += `?${qs.stringify(q, { encode: false })}`; 38 | } 39 | } 40 | 41 | return path; 42 | } 43 | 44 | export { parsePath }; 45 | -------------------------------------------------------------------------------- /src/hooks/redis.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import chalk from 'chalk'; 3 | import { parsePath } from './helpers/path'; 4 | 5 | const defaults = { 6 | env: 'production', 7 | defaultDuration: 3600 * 24, 8 | immediateCacheKey: false 9 | }; 10 | 11 | export function before(options) { // eslint-disable-line no-unused-vars 12 | return function (hook) { 13 | const cacheOptions = hook.app.get('redisCache'); 14 | 15 | options = Object.assign({}, defaults, cacheOptions, options); 16 | 17 | return new Promise(resolve => { 18 | const client = hook.app.get('redisClient'); 19 | 20 | if (!client) { 21 | resolve(hook); 22 | } 23 | 24 | const path = parsePath(hook, options); 25 | 26 | client.get(path, (err, reply) => { 27 | if (err !== null) resolve(hook); 28 | if (reply) { 29 | let data = JSON.parse(reply); 30 | const duration = moment(data.cache.expiresOn).format('DD MMMM YYYY - HH:mm:ss'); 31 | 32 | hook.result = data; 33 | resolve(hook); 34 | 35 | /* istanbul ignore next */ 36 | if (options.env !== 'test') { 37 | console.log(`${chalk.cyan('[redis]')} returning cached value for ${chalk.green(path)}.`); 38 | console.log(`> Expires on ${duration}.`); 39 | } 40 | } else { 41 | if (options.immediateCacheKey === true) { 42 | hook.params.cacheKey = path; 43 | } 44 | resolve(hook); 45 | } 46 | }); 47 | }); 48 | }; 49 | }; 50 | 51 | export function after(options) { // eslint-disable-line no-unused-vars 52 | return function (hook) { 53 | const cacheOptions = hook.app.get('redisCache'); 54 | 55 | options = Object.assign({}, defaults, cacheOptions, options); 56 | 57 | return new Promise(resolve => { 58 | if (!hook.result.cache.cached) { 59 | const duration = hook.result.cache.duration || options.defaultDuration; 60 | const client = hook.app.get('redisClient'); 61 | 62 | if (!client) { 63 | resolve(hook); 64 | } 65 | 66 | const path = hook.params.cacheKey || parsePath(hook, options); 67 | 68 | // adding a cache object 69 | Object.assign(hook.result.cache, { 70 | cached: true, 71 | duration: duration, 72 | expiresOn: moment().add(moment.duration(duration, 'seconds')), 73 | parent: hook.path, 74 | group: hook.path ? `group-${hook.path}` : '', 75 | key: path 76 | }); 77 | 78 | client.set(path, JSON.stringify(hook.result)); 79 | client.expire(path, hook.result.cache.duration); 80 | if (hook.path) { 81 | client.rpush(hook.result.cache.group, path); 82 | } 83 | 84 | /* istanbul ignore next */ 85 | if (options.env !== 'test') { 86 | console.log(`${chalk.cyan('[redis]')} added ${chalk.green(path)} to the cache.`); 87 | console.log(`> Expires in ${moment.duration(duration, 'seconds').humanize()}.`); 88 | } 89 | } 90 | 91 | if (hook.result.cache.hasOwnProperty('wrapped')) { 92 | const { wrapped } = hook.result.cache; 93 | 94 | hook.result = wrapped; 95 | } 96 | 97 | resolve(hook); 98 | }); 99 | }; 100 | }; 101 | 102 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import cacheRoutes from './routes/cache'; 2 | import redisClient from './redisClient'; 3 | import { cache as hookCache } from './hooks/cache'; 4 | import { removeCacheInformation as hookRemoveCacheInformation } from './hooks/cache'; 5 | import { before as redisBeforeHook} from './hooks/redis'; 6 | import { after as redisAfterHook} from './hooks/redis'; 7 | 8 | export default { 9 | redisClient, 10 | cacheRoutes, 11 | hookCache, 12 | hookRemoveCacheInformation, 13 | redisBeforeHook, 14 | redisAfterHook 15 | }; 16 | -------------------------------------------------------------------------------- /src/redisClient.js: -------------------------------------------------------------------------------- 1 | import redis from 'redis'; 2 | import chalk from 'chalk'; 3 | 4 | export default function redisClient() { // eslint-disable-line no-unused-vars 5 | const app = this; 6 | const cacheOptions = app.get('redisCache') || {}; 7 | const retryInterval = cacheOptions.retryInterval || 10000; 8 | const redisOptions = Object.assign({}, this.get('redis'), { 9 | retry_strategy: function (options) { // eslint-disable-line camelcase 10 | app.set('redisClient', undefined); 11 | /* istanbul ignore next */ 12 | if (cacheOptions.env !== 'test') { 13 | console.log(`${chalk.yellow('[redis]')} not connected`); 14 | } 15 | return retryInterval; 16 | } 17 | }); 18 | const client = redis.createClient(redisOptions); 19 | 20 | app.set('redisClient', client); 21 | 22 | client.on('ready', () => { 23 | app.set('redisClient', client); 24 | /* istanbul ignore next */ 25 | if (cacheOptions.env !== 'test') { 26 | console.log(`${chalk.green('[redis]')} connected`); 27 | } 28 | }); 29 | return this; 30 | } 31 | -------------------------------------------------------------------------------- /src/routes/cache.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | import RedisCache from './helpers/redis'; 4 | 5 | const HTTP_OK = 200; 6 | const HTTP_NO_CONTENT = 204; 7 | const HTTP_SERVER_ERROR = 500; 8 | const HTTP_NOT_FOUND = 404; 9 | 10 | function routes(app) { 11 | const router = express.Router(); 12 | const client = app.get('redisClient'); 13 | const h = new RedisCache(client); 14 | 15 | router.get('/clear', (req, res) => { 16 | client.flushall('ASYNC', () => { 17 | res.status(HTTP_OK).json({ 18 | message: 'Cache cleared', 19 | status: HTTP_OK 20 | }); 21 | }); 22 | }); // clear a unique route 23 | 24 | // clear a unique route 25 | router.get('/clear/single/*', (req, res) => { 26 | let target = decodeURIComponent(req.params[0]); 27 | // Formated options following ? 28 | const query = req.query; 29 | const hasQueryString = (query && (Object.keys(query).length !== 0)); 30 | 31 | // Target should always be defined as Express router raises 404 32 | // as route is not handled 33 | if (target.length) { 34 | if (hasQueryString) { 35 | // Keep queries in a single string with the taget 36 | target = decodeURIComponent(req.url.split('/').slice(3).join('/')); 37 | } 38 | 39 | // Gets the value of a key in the redis client 40 | client.get(`${target}`, (err, reply) => { 41 | if (err) { 42 | res.status(HTTP_SERVER_ERROR).json({ 43 | message: 'something went wrong' + err.message 44 | }); 45 | } else { 46 | // If the key existed 47 | if (reply) { 48 | // Clear existing cached key 49 | h.clearSingle(target).then(r => { 50 | res.status(HTTP_OK).json({ 51 | message: `cache cleared for key (${hasQueryString ? 52 | 'with' : 'without'} params): ${target}`, 53 | status: HTTP_OK 54 | }); 55 | }); 56 | } else { 57 | /** 58 | * Empty reply means the key does not exist. 59 | * Must use HTTP_OK with express as HTTP's RFC stats 204 should not 60 | * provide a body, message would then be lost. 61 | */ 62 | res.status(HTTP_OK).json({ 63 | message: `cache already cleared for key (${hasQueryString ? 64 | 'with' : 'without'} params): ${target}`, 65 | status: HTTP_NO_CONTENT 66 | }); 67 | } 68 | 69 | } 70 | }); 71 | } else { 72 | res.status(HTTP_NOT_FOUND).end(); 73 | } 74 | }); 75 | 76 | // clear a group 77 | router.get('/clear/group/*', (req, res) => { 78 | let target = decodeURIComponent(req.params[0]); 79 | 80 | // Target should always be defined as Express router raises 404 81 | // as route is not handled 82 | if (target.length) { 83 | target = 'group-' + target; 84 | // Returns elements of the list associated to the target/key 0 being the 85 | // first and -1 specifying get all till the latest 86 | client.lrange(target, 0, -1, (err, reply) => { 87 | if (err) { 88 | res.status(HTTP_SERVER_ERROR).json({ 89 | message: 'something went wrong' + err.message 90 | }); 91 | } else { 92 | // If the list/group existed and contains something 93 | if (reply && Array.isArray(reply) && (reply.length > 0)) { 94 | // Clear existing cached group key 95 | h.clearGroup(target).then(r => { 96 | res.status(HTTP_OK).json({ 97 | message: 98 | `cache cleared for the group key: ${decodeURIComponent(req.params[0])}`, 99 | status: HTTP_OK 100 | }); 101 | }); 102 | } else { 103 | /** 104 | * Empty reply means the key does not exist. 105 | * Must use HTTP_OK with express as HTTP's RFC stats 204 should not 106 | * provide a body, message would then be lost. 107 | */ 108 | res.status(HTTP_OK).json({ 109 | message: 110 | `cache already cleared for the group key: ${decodeURIComponent(req.params[0])}`, 111 | status: HTTP_NO_CONTENT 112 | }); 113 | } 114 | } 115 | }); 116 | } else { 117 | res.status(HTTP_NOT_FOUND).end(); 118 | } 119 | }); 120 | 121 | // add route to display cache index 122 | // this has been removed for performance issues 123 | // router.get('/index', (req, res) => { 124 | // let results = new Set(); 125 | 126 | // h.scanAsync('0', '*', results) 127 | // .then(data => { 128 | // res.status(200).json(data); 129 | // }) 130 | // .catch(err => { 131 | // res.status(404).json(err); 132 | // }); 133 | 134 | // }); 135 | 136 | return router; 137 | } 138 | 139 | export default routes; 140 | -------------------------------------------------------------------------------- /src/routes/helpers/redis.js: -------------------------------------------------------------------------------- 1 | export default class RedisCache { 2 | constructor(client) { 3 | this.client = client; 4 | } 5 | 6 | /** 7 | * scan the redis index 8 | */ 9 | // scan() { 10 | // // starts at 0 if cursor is again 0 it means the iteration is finished 11 | // let cursor = '0'; 12 | 13 | // return new Promise((resolve, reject) => { 14 | // this.client.scan(cursor, 'MATCH', '*', 'COUNT', '100', (err, reply) => { 15 | // if (err) { 16 | // reject(err); 17 | // } 18 | 19 | // cursor = reply[0]; 20 | // if (cursor === '0') { 21 | // resolve(reply[1]); 22 | // } else { 23 | // // do your processing 24 | // // reply[1] is an array of matched keys. 25 | // // console.log(reply[1]); 26 | // return this.scan(); 27 | // } 28 | // return false; 29 | // }); 30 | 31 | // }); 32 | // } 33 | 34 | /** 35 | * Async scan of the redis index 36 | * Do not for get to passin a Set 37 | * myResults = new Set(); 38 | * 39 | * scanAsync('0', "NOC-*[^listen]*", myResults).map( 40 | * myResults => { console.log( myResults); } 41 | * ); 42 | * 43 | * @param {String} cursor - string '0' 44 | * @param {String} patern - string '0' 45 | * @param {Set} returnSet - pass a set to have unique keys 46 | */ 47 | // scanAsync(cursor, pattern, returnSet) { 48 | // // starts at 0 if cursor is again 0 it means the iteration is finished 49 | 50 | // return new Promise((resolve, reject) => { 51 | // this.client.scan(cursor, 'MATCH', pattern, 'COUNT', '100', (err, reply) => { 52 | 53 | // if (err) { 54 | // reject(err); 55 | // } 56 | 57 | // cursor = reply[0]; 58 | // const keys = reply[1]; 59 | 60 | // keys.forEach((key, i) => { 61 | // returnSet.add(key); 62 | // }); 63 | 64 | // if (cursor === '0') { 65 | // resolve(Array.from(returnSet)); 66 | // } 67 | 68 | // return this.scanAsync(cursor, pattern, returnSet); 69 | // }); 70 | // }); 71 | // } 72 | 73 | /** 74 | * Clean single item from the cache 75 | * @param {string} key - the key to find in redis 76 | */ 77 | clearSingle(key) { 78 | return new Promise((resolve, reject) => { 79 | this.client.del(`${key}`, (err, reply) => { 80 | if (err) reject(false); 81 | if (reply === 1) { 82 | resolve(true); 83 | } 84 | resolve(false); 85 | }); 86 | }); 87 | } 88 | 89 | /** 90 | * Clear a group 91 | * @param {string} key - key of the group to clean 92 | */ 93 | clearGroup(key) { 94 | return new Promise((resolve, reject) => { 95 | this.client.lrange(key, 0, -1, (err, reply) => { 96 | if (err) { 97 | reject(err); 98 | } 99 | this.clearAll(reply).then( 100 | this.client.del(key, (e, r) => { 101 | resolve(r === 1); 102 | }) 103 | ); 104 | }); 105 | }); 106 | } 107 | 108 | /** 109 | * Clear all keys of a redis list 110 | * @param {Object[]} array 111 | */ 112 | clearAll(array) { 113 | return new Promise(resolve => { 114 | if (!array.length) resolve(false); 115 | let i = 0; 116 | 117 | for (i; i < array.length; i++) { 118 | this.clearSingle(array[i]).then(r => { 119 | if (i === array.length - 1) { 120 | resolve(r); 121 | } 122 | }); 123 | } 124 | }); 125 | } 126 | }; 127 | -------------------------------------------------------------------------------- /test/cache.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { hookCache } from '../src'; 3 | 4 | const app = { 5 | get: () => ({}) 6 | }; 7 | 8 | describe('Cache Hook', () => { 9 | it('adds a cache object', () => { 10 | const hook = hookCache(); 11 | const mock = { 12 | app, 13 | params: { query: ''}, 14 | path: 'test-route', 15 | result: { 16 | _sys: { 17 | status: 200 18 | }, 19 | cache: { 20 | cached: true, 21 | duration: 86400 22 | } 23 | } 24 | }; 25 | 26 | return hook(mock).then(result => { 27 | const data = result.result; 28 | 29 | expect(data.cache.cached).to.equal(true); 30 | expect(data.cache.duration).to.equal(86400); 31 | }); 32 | }); 33 | 34 | it('uses config options instead of defaults', () => { 35 | const hook = hookCache(); 36 | const mock = { 37 | app: { get: (what) => { 38 | if (what === 'redisCache') { 39 | return { 40 | 'defaultDuration': 12 41 | }; 42 | } 43 | return {}; 44 | }}, 45 | params: { query: ''}, 46 | path: 'test-route', 47 | result: { 48 | _sys: { 49 | status: 200 50 | }, 51 | cache: { 52 | cached: true, 53 | duration: 12 54 | } 55 | } 56 | }; 57 | 58 | return hook(mock).then(result => { 59 | const data = result.result; 60 | 61 | expect(data.cache.cached).to.equal(true); 62 | expect(data.cache.duration).to.equal(12); 63 | }); 64 | }); 65 | 66 | it('does not modify the existing cache object', () => { 67 | const hook = hookCache(); 68 | const mock = { 69 | app, 70 | params: { query: ''}, 71 | path: 'test-route', 72 | result: { 73 | _sys: { 74 | status: 200 75 | } 76 | } 77 | }; 78 | 79 | return hook(mock).then(result => { 80 | const data = result.result; 81 | 82 | expect(data.cache.cached).to.equal(false); 83 | expect(data.cache.duration).to.equal(86400); 84 | expect(data.cache).to.not.have.property('parent'); 85 | expect(data.cache).to.not.have.property('group'); 86 | expect(data.cache).to.not.have.property('expiresOn'); 87 | }); 88 | }); 89 | 90 | it('wraps arrays', () => { 91 | const hook = hookCache(); 92 | const mock = { 93 | app, 94 | params: { query: ''}, 95 | path: 'test-route-array', 96 | result: [ 97 | {title: 'title 1'}, 98 | {title: 'title 2'} 99 | ] 100 | }; 101 | 102 | return hook(mock).then(result => { 103 | const data = result.result; 104 | 105 | expect(data.cache.wrapped).to.be.an('array').that.deep.equals([ 106 | {title: 'title 1'}, 107 | {title: 'title 2'} 108 | ]); 109 | expect(data.cache.cached).to.equal(false); 110 | expect(data.cache.duration).to.equal(86400); 111 | expect(data.cache).to.not.have.property('parent'); 112 | expect(data.cache).to.not.have.property('group'); 113 | expect(data.cache).to.not.have.property('expiresOn'); 114 | }); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /test/client.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import RedisClient from '../src/redisClient'; 3 | 4 | const app = { 5 | get: function(key) { 6 | return this[key]; 7 | }, 8 | set: function(key, val) { 9 | this[key] = val; 10 | }, 11 | configure: function(fn) { 12 | fn.call(this); 13 | } 14 | }; 15 | 16 | describe('redisClient', () => { 17 | it('should not exist if redis not connected', (done) => { 18 | app.set('redis', { port: 1234 }); // force connection error 19 | app.configure(RedisClient); 20 | setTimeout(function () { 21 | expect(app.get('redisClient')).to.not.exist; 22 | done(); 23 | }, 500); 24 | }); 25 | it('should be available if redis connected', (done) => { 26 | app.set('redis', { port: 6379 }); // restore default 27 | app.configure(RedisClient); 28 | setTimeout(function () { 29 | expect(app.get('redisClient')).to.exist; 30 | done(); 31 | }, 500); 32 | }); 33 | }); -------------------------------------------------------------------------------- /test/hooks/redis-after.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import redis from 'redis'; 3 | import { redisAfterHook as a } from '../../src'; 4 | // import moment from 'moment'; 5 | 6 | const client = redis.createClient(); 7 | 8 | describe('Redis After Hook', () => { 9 | it('caches a route', () => { 10 | const hook = a(); 11 | const mock = { 12 | params: { query: ''}, 13 | id: 'test-route', 14 | path: '', 15 | result: { 16 | _sys: { 17 | status: 200 18 | }, 19 | cache: { 20 | cached: false, 21 | duration: 8400 22 | } 23 | }, 24 | app: { 25 | get: (what) => { 26 | return client; 27 | } 28 | } 29 | }; 30 | 31 | return hook(mock).then(result => { 32 | const data = result.result; 33 | 34 | expect(data.cache.cached).to.equal(true); 35 | expect(data.cache.duration).to.equal(8400); 36 | expect(data.cache.parent).to.equal(''); 37 | expect(data.cache.group).to.equal(''); 38 | expect(data.cache.key).to.equal('test-route'); 39 | }); 40 | }); 41 | 42 | it('caches a route', () => { 43 | const hook = a(); 44 | const mock = { 45 | params: { query: ''}, 46 | id: 'test-route', 47 | path: '', 48 | result: { 49 | _sys: { 50 | status: 200 51 | }, 52 | cache: { 53 | cached: false, 54 | duration: 8400 55 | } 56 | }, 57 | app: { 58 | get: (what) => { 59 | return client; 60 | } 61 | } 62 | }; 63 | 64 | return hook(mock).then(result => { 65 | const data = result.result; 66 | 67 | expect(data.cache.cached).to.equal(true); 68 | expect(data.cache.duration).to.equal(8400); 69 | expect(data.cache.parent).to.equal(''); 70 | expect(data.cache.group).to.equal(''); 71 | expect(data.cache.key).to.equal('test-route'); 72 | }); 73 | }); 74 | 75 | it('caches a route using params.cacheKey instead of params.query', () => { 76 | const hook = a(); 77 | const mock = { 78 | params: { 79 | query: { foo: 'bar', lorem: 'ipsum' }, 80 | cacheKey: 'after-cache-key?foo=bar' 81 | }, 82 | id: 'after-cache-key', 83 | path: '', 84 | result: { 85 | _sys: { 86 | status: 200 87 | }, 88 | cache: { 89 | cached: false 90 | } 91 | }, 92 | app: { 93 | get: (what) => { 94 | return client; 95 | } 96 | } 97 | }; 98 | 99 | return hook(mock).then(result => { 100 | const data = result.result; 101 | 102 | expect(data.cache.key).to.equal('after-cache-key?foo=bar'); 103 | }); 104 | }); 105 | 106 | it('caches a parent route that returns an array', () => { 107 | const hook = a(); 108 | const mock = { 109 | params: { query: ''}, 110 | path: 'test-route', 111 | result: { 112 | wrapped: [ 113 | {title: 'title 1'}, 114 | {title: 'title 2'} 115 | ], 116 | cache: { 117 | cached: false, 118 | duration: 8400 119 | } 120 | }, 121 | app: { 122 | get: (what) => { 123 | return client; 124 | } 125 | } 126 | }; 127 | 128 | return hook(mock).then(result => { 129 | const data = result.result; 130 | 131 | expect(data.cache.cached).to.equal(true); 132 | expect(data.cache.duration).to.equal(8400); 133 | expect(data.cache.parent).to.equal('test-route'); 134 | expect(data.cache.group).to.equal('group-test-route'); 135 | expect(data.cache.key).to.equal('test-route'); 136 | }); 137 | }); 138 | 139 | it('caches a parent route with setting to remove path from key...', () => { 140 | const hook = a(); 141 | const mock = { 142 | params: { query: ''}, 143 | path: 'test-route', 144 | result: { 145 | _sys: { 146 | status: 200 147 | }, 148 | cache: { 149 | cached: false, 150 | duration: 8400 151 | } 152 | }, 153 | app: { 154 | get: (what) => { 155 | if (what === 'redisClient') return client; 156 | if (what === 'redisCache') { 157 | const cache = { 158 | defaultDuration: 3600, 159 | removePathFromCacheKey: true 160 | }; 161 | 162 | return cache; 163 | } 164 | return undefined; 165 | } 166 | } 167 | }; 168 | 169 | return hook(mock).then(result => { 170 | const data = result.result; 171 | 172 | expect(data.cache.cached).to.equal(true); 173 | expect(data.cache.duration).to.equal(8400); 174 | expect(data.cache.parent).to.equal('test-route'); 175 | expect(data.cache.group).to.equal('group-test-route'); 176 | expect(data.cache.key).to.equal('test-route'); 177 | }); 178 | }); 179 | 180 | it('caches a parent route with setting to remove path from key...', () => { 181 | const hook = a(); 182 | const mock = { 183 | params: { query: ''}, 184 | path: 'test-route', 185 | result: { 186 | _sys: { 187 | status: 200 188 | }, 189 | cache: { 190 | cached: false, 191 | duration: 8400 192 | } 193 | }, 194 | app: { 195 | get: (what) => { 196 | if (what === 'redisClient') return client; 197 | if (what === 'redisCache') { 198 | const cache = { 199 | defaultDuration: 3600, 200 | removePathFromCacheKey: true 201 | }; 202 | 203 | return cache; 204 | } 205 | return undefined; 206 | } 207 | } 208 | }; 209 | 210 | return hook(mock).then(result => { 211 | const data = result.result; 212 | 213 | expect(data.cache.cached).to.equal(true); 214 | expect(data.cache.duration).to.equal(8400); 215 | expect(data.cache.parent).to.equal('test-route'); 216 | expect(data.cache.group).to.equal('group-test-route'); 217 | expect(data.cache.key).to.equal('test-route'); 218 | }); 219 | }); 220 | 221 | it('caches a nested route with setting to parse it...', () => { 222 | const hook = a(); 223 | const mock = { 224 | params: { 225 | route: { 226 | abcId: 123 227 | }, 228 | query: '' 229 | }, 230 | path: 'test-route/:abcId', 231 | id: 'nested-route', 232 | result: { 233 | _sys: { 234 | status: 200 235 | }, 236 | cache: { 237 | cached: false, 238 | duration: 8400 239 | } 240 | }, 241 | app: { 242 | get: (what) => { 243 | if (what === 'redisClient') return client; 244 | if (what === 'redisCache') { 245 | const cache = { 246 | defaultDuration: 3600, 247 | parseNestedRoutes: true 248 | }; 249 | 250 | return cache; 251 | } 252 | return undefined; 253 | } 254 | } 255 | }; 256 | 257 | return hook(mock).then(result => { 258 | const data = result.result; 259 | 260 | expect(data.cache.cached).to.equal(true); 261 | expect(data.cache.duration).to.equal(8400); 262 | expect(data.cache.parent).to.equal('test-route/:abcId'); 263 | expect(data.cache.group).to.equal('group-test-route/:abcId'); 264 | expect(data.cache.key).to.equal('test-route/123/nested-route'); 265 | }); 266 | }); 267 | 268 | it('caches a nested route with optional params set', () => { 269 | const hook = a(); 270 | const mock = { 271 | params: { 272 | route: { 273 | abcId: 123 274 | }, 275 | query: '' 276 | }, 277 | path: 'test-route/:abcId?', 278 | id: 'nested-route', 279 | result: { 280 | _sys: { 281 | status: 200 282 | }, 283 | cache: { 284 | cached: false, 285 | duration: 8400 286 | } 287 | }, 288 | app: { 289 | get: (what) => { 290 | if (what === 'redisClient') return client; 291 | if (what === 'redisCache') { 292 | const cache = { 293 | defaultDuration: 3600, 294 | parseNestedRoutes: true 295 | }; 296 | 297 | return cache; 298 | } 299 | return undefined; 300 | } 301 | } 302 | }; 303 | 304 | return hook(mock).then(result => { 305 | const data = result.result; 306 | 307 | expect(data.cache.cached).to.equal(true); 308 | expect(data.cache.duration).to.equal(8400); 309 | expect(data.cache.parent).to.equal('test-route/:abcId?'); 310 | expect(data.cache.group).to.equal('group-test-route/:abcId?'); 311 | expect(data.cache.key).to.equal('test-route/123/nested-route'); 312 | }); 313 | }); 314 | 315 | it('caches a route with optional params set,', () => { 316 | const hook = a(); 317 | const mock = { 318 | params: { 319 | route: { 320 | abcId: 123 321 | }, 322 | query: '' 323 | }, 324 | path: 'test-route/:abcId?', 325 | id: '', 326 | result: { 327 | _sys: { 328 | status: 200 329 | }, 330 | cache: { 331 | cached: false, 332 | duration: 8400 333 | } 334 | }, 335 | app: { 336 | get: (what) => { 337 | if (what === 'redisClient') return client; 338 | if (what === 'redisCache') { 339 | const cache = { 340 | defaultDuration: 3600, 341 | parseNestedRoutes: true 342 | }; 343 | 344 | return cache; 345 | } 346 | return undefined; 347 | } 348 | } 349 | }; 350 | 351 | return hook(mock).then(result => { 352 | const data = result.result; 353 | 354 | expect(data.cache.cached).to.equal(true); 355 | expect(data.cache.duration).to.equal(8400); 356 | expect(data.cache.parent).to.equal('test-route/:abcId?'); 357 | expect(data.cache.group).to.equal('group-test-route/:abcId?'); 358 | expect(data.cache.key).to.equal('test-route/123'); 359 | }); 360 | }); 361 | 362 | it('caches a parent with params', () => { 363 | const hook = a(); 364 | const mock = { 365 | params: { query: {full: true}}, 366 | id: '', 367 | path: 'parent', 368 | result: { 369 | _sys: { 370 | status: 200 371 | }, 372 | cache: { 373 | cached: false, 374 | duration: 8400 375 | } 376 | }, 377 | app: { 378 | get: (what) => { 379 | return client; 380 | } 381 | } 382 | }; 383 | 384 | return hook(mock).then(result => { 385 | const data = result.result; 386 | 387 | expect(data.cache.cached).to.equal(true); 388 | expect(data.cache.duration).to.equal(8400); 389 | expect(data.cache.parent).to.equal('parent'); 390 | expect(data.cache.group).to.equal('group-parent'); 391 | expect(data.cache.key).to.equal('parent?full=true'); 392 | }); 393 | }); 394 | 395 | it('caches a route with a parent', () => { 396 | const hook = a(); 397 | const mock = { 398 | params: { query: ''}, 399 | id: 'test-route', 400 | path: 'parent', 401 | result: { 402 | _sys: { 403 | status: 200 404 | }, 405 | cache: { 406 | cached: false, 407 | duration: 8400 408 | } 409 | }, 410 | app: { 411 | get: (what) => { 412 | return client; 413 | } 414 | } 415 | }; 416 | 417 | return hook(mock).then(result => { 418 | const data = result.result; 419 | 420 | expect(data.cache.cached).to.equal(true); 421 | expect(data.cache.duration).to.equal(8400); 422 | expect(data.cache.parent).to.equal('parent'); 423 | expect(data.cache.group).to.equal('group-parent'); 424 | expect(data.cache.key).to.equal('parent/test-route'); 425 | }); 426 | }); 427 | 428 | it('caches a route without a parent in the cache key', () => { 429 | const hook = a(); 430 | const mock = { 431 | params: { query: ''}, 432 | id: 'test-route', 433 | path: 'parent', 434 | result: { 435 | _sys: { 436 | status: 200 437 | }, 438 | cache: { 439 | cached: false, 440 | duration: 8400 441 | } 442 | }, 443 | app: { 444 | get: (what) => { 445 | if (what === 'redisClient') return client; 446 | if (what === 'redisCache') { 447 | const cache = { 448 | defaultDuration: 3600, 449 | removePathFromCacheKey: true 450 | }; 451 | 452 | return cache; 453 | } 454 | return undefined; 455 | } 456 | } 457 | }; 458 | 459 | return hook(mock).then(result => { 460 | const data = result.result; 461 | 462 | expect(data.cache.cached).to.equal(true); 463 | expect(data.cache.duration).to.equal(8400); 464 | expect(data.cache.parent).to.equal('parent'); 465 | expect(data.cache.group).to.equal('group-parent'); 466 | expect(data.cache.key).to.equal('test-route'); 467 | }); 468 | }); 469 | 470 | it('caches a route with a parent and params', () => { 471 | const hook = a(); 472 | const mock = { 473 | params: { query: {full: true}}, 474 | id: 'test-route', 475 | path: 'parent', 476 | result: { 477 | _sys: { 478 | status: 200 479 | }, 480 | cache: { 481 | cached: false, 482 | duration: 8400 483 | } 484 | }, 485 | app: { 486 | get: (what) => { 487 | return client; 488 | } 489 | } 490 | }; 491 | 492 | return hook(mock).then(result => { 493 | const data = result.result; 494 | 495 | expect(data.cache.cached).to.equal(true); 496 | expect(data.cache.duration).to.equal(8400); 497 | expect(data.cache.parent).to.equal('parent'); 498 | expect(data.cache.group).to.equal('group-parent'); 499 | expect(data.cache.key).to.equal('parent/test-route?full=true'); 500 | }); 501 | }); 502 | 503 | it('caches a route with a parent and a nested param', () => { 504 | const hook = a(); 505 | const mock = { 506 | params: { query: { id: { '$nin': '1' }}}, 507 | id: 'test-route', 508 | path: 'parent', 509 | result: { 510 | _sys: { 511 | status: 200 512 | }, 513 | cache: { 514 | cached: false, 515 | duration: 8400 516 | } 517 | }, 518 | app: { 519 | get: (what) => { 520 | return client; 521 | } 522 | } 523 | }; 524 | 525 | return hook(mock).then(result => { 526 | const data = result.result; 527 | 528 | expect(data.cache.cached).to.equal(true); 529 | expect(data.cache.duration).to.equal(8400); 530 | expect(data.cache.parent).to.equal('parent'); 531 | expect(data.cache.group).to.equal('group-parent'); 532 | expect(data.cache.key).to.equal('parent/test-route?id[$nin]=1'); 533 | }); 534 | }); 535 | 536 | it('caches a route with a parent and nested params', () => { 537 | const hook = a(); 538 | const mock = { 539 | params: { query: { id: { '$nin': '1', test: '2' }}}, 540 | id: 'test-route', 541 | path: 'parent', 542 | result: { 543 | _sys: { 544 | status: 200 545 | }, 546 | cache: { 547 | cached: false, 548 | duration: 8400 549 | } 550 | }, 551 | app: { 552 | get: (what) => { 553 | return client; 554 | } 555 | } 556 | }; 557 | 558 | return hook(mock).then(result => { 559 | const data = result.result; 560 | 561 | expect(data.cache.cached).to.equal(true); 562 | expect(data.cache.duration).to.equal(8400); 563 | expect(data.cache.parent).to.equal('parent'); 564 | expect(data.cache.group).to.equal('group-parent'); 565 | expect(data.cache.key).to.equal('parent/test-route?id[$nin]=1&id[test]=2'); 566 | }); 567 | }); 568 | 569 | it('caches a route without a parent in the cache key but with params', () => { 570 | const hook = a(); 571 | const mock = { 572 | params: { query: {full: true}}, 573 | id: 'test-route', 574 | path: 'parent', 575 | result: { 576 | _sys: { 577 | status: 200 578 | }, 579 | cache: { 580 | cached: false, 581 | duration: 8400 582 | } 583 | }, 584 | app: { 585 | get: (what) => { 586 | if (what === 'redisClient') return client; 587 | if (what === 'redisCache') { 588 | const cache = { 589 | defaultDuration: 3600, 590 | removePathFromCacheKey: true 591 | }; 592 | 593 | return cache; 594 | } 595 | return undefined; 596 | } 597 | } 598 | }; 599 | 600 | return hook(mock).then(result => { 601 | const data = result.result; 602 | 603 | expect(data.cache.cached).to.equal(true); 604 | expect(data.cache.duration).to.equal(8400); 605 | expect(data.cache.parent).to.equal('parent'); 606 | expect(data.cache.group).to.equal('group-parent'); 607 | expect(data.cache.key).to.equal('test-route?full=true'); 608 | }); 609 | }); 610 | 611 | it('adds default cache duration', () => { 612 | const hook = a(); 613 | const mock = { 614 | params: { query: {full: true}}, 615 | id: 'test-route', 616 | path: 'parent', 617 | result: { 618 | _sys: { 619 | status: 200 620 | }, 621 | cache: { 622 | cached: false 623 | } 624 | }, 625 | app: { 626 | get: (what) => { 627 | return client; 628 | } 629 | } 630 | }; 631 | 632 | return hook(mock).then(result => { 633 | const data = result.result; 634 | 635 | expect(data.cache.cached).to.equal(true); 636 | expect(data.cache.duration).to.equal(3600 * 24); 637 | }); 638 | }); 639 | 640 | it('changes the environement', () => { 641 | const hook = a(); 642 | const mock = { 643 | params: { query: { full: true }}, 644 | path: 'env-test', 645 | id: '', 646 | result: { 647 | _sys: { 648 | status: 200 649 | }, 650 | cache: { 651 | cached: true, 652 | duration: 8400 653 | } 654 | }, 655 | app: { 656 | get: (what) => { 657 | return what === 'redisCache' 658 | ? {env: 'test'} 659 | : client; 660 | } 661 | } 662 | }; 663 | 664 | return hook(mock).then(result => { 665 | expect(result.app.get('redisCache').env).to.equal('test'); 666 | }); 667 | }); 668 | 669 | it ('does not save anything in cache if redisClient offline', () => { 670 | const hook = a(); 671 | const clientDummy = { 672 | keys: [], 673 | set: function(key, val) { 674 | this.keys.push(key); 675 | }, 676 | expire: function() {}, 677 | rpush: function() {}, 678 | }; 679 | const mock = { 680 | params: { query: '' }, 681 | path: 'dummy', 682 | id: 'do-not-save', 683 | result: { 684 | cache: {} 685 | }, 686 | app: { 687 | get: (what) => { 688 | // comment in to emulate redisClient online (will cause test fail) 689 | // if (what === 'redisClient') { 690 | // return clientDummy; 691 | // } 692 | } 693 | } 694 | }; 695 | const prevKeys = clientDummy.keys.join(); 696 | 697 | return hook(mock).then(result => { 698 | const currKeys = clientDummy.keys.join(); 699 | 700 | expect(currKeys).to.equal(prevKeys); 701 | }); 702 | }); 703 | 704 | // after(() => { 705 | // client.del('parent'); 706 | // client.del('parent?full=true'); 707 | // client.del('test-route'); 708 | // client.del('test-route?full=true'); 709 | // client.del('group-test-route'); 710 | // client.del('group-parent'); 711 | // }); 712 | 713 | }); 714 | -------------------------------------------------------------------------------- /test/hooks/redis-before.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import redis from 'redis'; 3 | import moment from 'moment'; 4 | import { redisBeforeHook as b, redisAfterHook as a } from '../../src'; 5 | 6 | const client = redis.createClient(); 7 | 8 | describe('Redis Before Hook', () => { 9 | before(() => { 10 | client.set('before-test-route', JSON.stringify( 11 | { 12 | _sys: { 13 | status: 200 14 | }, 15 | cache: { 16 | cached: true, 17 | duration: 3600 * 24 * 7, 18 | expiresOn: moment().add(moment.duration(3600 * 24 * 7, 'seconds')) 19 | } 20 | } 21 | )); 22 | 23 | client.set('before-test-route?full=true', JSON.stringify( 24 | { 25 | _sys: { 26 | status: 200 27 | }, 28 | cache: { 29 | cached: true, 30 | duration: 3600 * 24 * 7, 31 | expiresOn: moment().add(moment.duration(3600 * 24 * 7, 'seconds')) 32 | } 33 | } 34 | )); 35 | 36 | client.set('before-parent-route', JSON.stringify( 37 | { 38 | _sys: { 39 | status: 200 40 | }, 41 | cache: { 42 | cached: true, 43 | duration: 3600 * 24 * 7, 44 | expiresOn: moment().add(moment.duration(3600 * 24 * 7, 'seconds')) 45 | } 46 | } 47 | )); 48 | 49 | client.set('before-parent-route?full=true', JSON.stringify( 50 | { 51 | _sys: { 52 | status: 200 53 | }, 54 | cache: { 55 | cached: true, 56 | duration: 3600 * 24 * 7, 57 | expiresOn: moment().add(moment.duration(3600 * 24 * 7, 'seconds')) 58 | } 59 | } 60 | )); 61 | 62 | client.set('before-wrapped', JSON.stringify( 63 | { 64 | cache: { 65 | wrapped: [ 66 | {title: 'title 1'}, 67 | {title: 'title 2'} 68 | ], 69 | cached: true, 70 | duration: 3600 * 24 * 7, 71 | expiresOn: moment().add(moment.duration(3600 * 24 * 7, 'seconds')) 72 | } 73 | } 74 | )); 75 | }); 76 | 77 | it('sets cacheKey in before hook if immediateCacheKey true', () => { 78 | const hook = b({ immediateCacheKey: true }); 79 | const mock = { 80 | params: { query: { foo: 'bar' }}, 81 | path: '', 82 | id: 'before-cache-key', 83 | app: { 84 | get: (what) => { 85 | return client; 86 | } 87 | } 88 | }; 89 | 90 | return hook(mock).then(result => { 91 | expect(result.params.cacheKey).to.be.equal('before-cache-key?foo=bar'); 92 | }); 93 | }); 94 | 95 | it('retrives a cached object', () => { 96 | const hook = b(); 97 | const mock = { 98 | params: { query: ''}, 99 | path: '', 100 | id: 'before-test-route', 101 | app: { 102 | get: (what) => { 103 | return client; 104 | } 105 | } 106 | }; 107 | 108 | return hook(mock).then(result => { 109 | const data = result.result; 110 | 111 | expect(data.cache.cached).to.equal(true); 112 | }); 113 | }); 114 | 115 | it('retrives a cached object with params', () => { 116 | const hook = b(); 117 | const mock = { 118 | params: { query: { full: true }}, 119 | path: '', 120 | id: 'before-test-route', 121 | app: { 122 | get: (what) => { 123 | return client; 124 | } 125 | } 126 | }; 127 | 128 | return hook(mock).then(result => { 129 | const data = result.result; 130 | 131 | expect(data.cache.cached).to.equal(true); 132 | }); 133 | }); 134 | 135 | it('retrives a wrapped array', () => { 136 | const hook = b(); 137 | const after = a(); 138 | const mock = { 139 | params: { query: ''}, 140 | path: '', 141 | id: 'before-wrapped', 142 | app: { 143 | get: (what) => { 144 | return client; 145 | } 146 | } 147 | }; 148 | 149 | return hook(mock).then(result => { 150 | after(result).then(result => { 151 | const data = result.result; 152 | 153 | expect(data).to.be.an('array').that.deep.equals([ 154 | {title: 'title 1'}, 155 | {title: 'title 2'} 156 | ]); 157 | expect(data.cache).to.equal(undefined); 158 | }); 159 | }); 160 | }); 161 | 162 | it('retrives a cached parent object', () => { 163 | const hook = b(); 164 | const mock = { 165 | params: { query: ''}, 166 | path: 'before-parent-route', 167 | id: '', 168 | app: { 169 | get: (what) => { 170 | return client; 171 | } 172 | } 173 | }; 174 | 175 | return hook(mock).then(result => { 176 | const data = result.result; 177 | 178 | expect(data.cache.cached).to.equal(true); 179 | }); 180 | }); 181 | 182 | it('retrives a cached parent object with params', () => { 183 | const hook = b(); 184 | const mock = { 185 | params: { query: { full: true }}, 186 | path: 'before-parent-route', 187 | id: '', 188 | app: { 189 | get: (what) => { 190 | return client; 191 | } 192 | } 193 | }; 194 | 195 | return hook(mock).then(result => { 196 | const data = result.result; 197 | 198 | expect(data.cache.cached).to.equal(true); 199 | }); 200 | }); 201 | 202 | it('does not do anything', () => { 203 | const hook = b(); 204 | const mock = { 205 | params: { query: { full: true }}, 206 | path: 'does-nothing', 207 | id: '', 208 | app: { 209 | get: (what) => { 210 | return client; 211 | } 212 | } 213 | }; 214 | 215 | return hook(mock).then(result => { 216 | const data = result; 217 | 218 | expect(data.path).to.equal('does-nothing'); 219 | expect(data).to.not.have.a.property('result'); 220 | }); 221 | }); 222 | 223 | it('changes the environement', () => { 224 | const hook = b(); 225 | const mock = { 226 | params: { query: { full: true }}, 227 | path: 'does-nothing', 228 | id: '', 229 | app: { 230 | get: (what) => { 231 | return what === 'redisCache' 232 | ? {env: 'test'} 233 | : client; 234 | } 235 | } 236 | }; 237 | 238 | return hook(mock).then(result => { 239 | expect(result.app.get('redisCache').env).to.equal('test'); 240 | }); 241 | }); 242 | 243 | it('does not return any result if redisClient offline', () => { 244 | const hook = b(); 245 | const mock = { 246 | params: { query: ''}, 247 | path: '', 248 | id: '', 249 | app: { 250 | get: (what) => {} 251 | } 252 | }; 253 | 254 | return hook(mock).then(result => { 255 | const data = result.result; 256 | 257 | expect(data).to.be.undefined; 258 | }); 259 | }); 260 | 261 | after(() => { 262 | client.del('before-test-route'); 263 | client.del('before-test-route?full=true'); 264 | client.del('before-parent-route'); 265 | client.del('before-wrapped'); 266 | client.del('before-parent-route?full=true'); 267 | client.del('env-test'); 268 | }); 269 | }); 270 | -------------------------------------------------------------------------------- /test/hooks/redis-remove-hook.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import redis from 'redis'; 3 | import moment from 'moment'; 4 | import { hookRemoveCacheInformation as r } from '../../src'; 5 | 6 | const client = redis.createClient(); 7 | 8 | describe('Redis Remove Cache Object Hook', () => { 9 | 10 | it('removes the cache object', () => { 11 | const hook = r(); 12 | const mock = { 13 | params: { query: ''}, 14 | path: '', 15 | id: 'remove-cache-test-route', 16 | result: { 17 | cache: { 18 | cached: true, 19 | duration: 3600 * 24 * 7, 20 | expiresOn: moment().add(moment.duration(3600 * 24 * 7, 'seconds')) 21 | } 22 | }, 23 | app: { 24 | get: (what) => { 25 | return client; 26 | } 27 | } 28 | }; 29 | 30 | return hook(mock).then(result => { 31 | const data = result.result; 32 | 33 | expect(data).not.to.have.property('cache'); 34 | }); 35 | }); 36 | 37 | it('does not remove anything else thant the cache object', () => { 38 | const hook = r(); 39 | const mock = { 40 | params: { query: ''}, 41 | path: '', 42 | id: 'remove-cache-test-route', 43 | result: { 44 | property: 'test' 45 | }, 46 | app: { 47 | get: (what) => { 48 | return client; 49 | } 50 | } 51 | }; 52 | 53 | return hook(mock).then(result => { 54 | const data = result.result; 55 | 56 | expect(data).to.deep.equal({property: 'test'}); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import * as lib from '../src'; 3 | 4 | describe('feathers-hooks-rediscache', () => { 5 | it('loads Redis Client', () => { 6 | expect(lib).to.respondTo('redisClient'); 7 | }); 8 | 9 | it('loads routes', () => { 10 | expect(lib).to.respondTo('cacheRoutes'); 11 | }); 12 | 13 | it('loads the after Redis Cache hook', () => { 14 | expect(lib).to.respondTo('redisAfterHook'); 15 | }); 16 | 17 | it('loads the before Redis Cache hook', () => { 18 | expect(lib).to.respondTo('redisBeforeHook'); 19 | }); 20 | 21 | it('loads the remove Redis Cache Object hook', () => { 22 | expect(lib).to.respondTo('hookRemoveCacheInformation'); 23 | }); 24 | 25 | it('loads the cache hook', () => { 26 | expect(lib).to.respondTo('hookCache'); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/routes-functions.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { promisify } from 'util'; 3 | import redis from 'redis'; 4 | import RedisCache from '../src/routes/helpers/redis'; 5 | 6 | const client = redis.createClient(); 7 | const h = new RedisCache(client); 8 | const getAsync = promisify(client.get).bind(client); 9 | const setAsync = promisify(client.set).bind(client); 10 | const rpushAsync = promisify(client.rpush).bind(client); 11 | const lrangeAsync = promisify(client.lrange).bind(client); 12 | const delAsync = promisify(client.del).bind(client); 13 | 14 | describe('Cache functions', () => { 15 | before(async () => { 16 | await setAsync('cache-test-key', 'value'); 17 | await setAsync('path-1', 'value-1'); 18 | await setAsync('path-2', 'value-2'); 19 | await setAsync('path-3', 'value-3'); 20 | await rpushAsync('group-test-key', ['path-1', 'path-2', 'path-3']); 21 | }); 22 | 23 | // it('scans the index', () => { 24 | // return h.scan().then(data => { 25 | // expect(data).to.include( 26 | // 'cache-test-key', 27 | // 'group-test-key', 28 | // 'path-1', 29 | // 'path-2', 30 | // 'path-3' 31 | // ); 32 | // }); 33 | // }); 34 | 35 | // it('Async scan the index', () => { 36 | // let myResult = new Set(); 37 | 38 | // return h.scanAsync('0', '*', myResult).then(data => { 39 | // expect(data).to.include( 40 | // 'cache-test-key', 41 | // 'group-test-key', 42 | // 'path-1', 43 | // 'path-2', 44 | // 'path-3' 45 | // ); 46 | // }); 47 | // }); 48 | 49 | it('removes an item from the cache', async () => { 50 | const reply = await getAsync('cache-test-key'); 51 | 52 | expect(reply).to.equal('value'); 53 | return h.clearSingle('cache-test-key').then(data => { 54 | expect(data).to.equal(true); 55 | }); 56 | }); 57 | 58 | it('returns false when an item does not exist', () => { 59 | return h.clearSingle('cache-does-not-exist').then(data => { 60 | expect(data).to.equal(false); 61 | }); 62 | }); 63 | 64 | it('removed an item from the cache', async () => { 65 | const reply = await getAsync('cache-test-key'); 66 | 67 | expect(reply).to.equal(null); 68 | }); 69 | 70 | it('removes all the item from a redis list array', () => { 71 | return h.clearGroup('group-test-key').then(data => { 72 | expect(data).to.equal(true); 73 | }); 74 | }); 75 | 76 | it('removes all the item from a redis list array', () => { 77 | return h.clearGroup('group-does-not-exist').then(data => { 78 | expect(data).to.equal(false); 79 | }); 80 | }); 81 | 82 | it('really removed keys in a group', () => { 83 | client.get('path-2', reply => { 84 | expect(reply).to.be.equal(null); 85 | }); 86 | }); 87 | 88 | it('really emptied the group', async () => { 89 | const reply = await lrangeAsync('group-test-key', 0, -1); 90 | 91 | expect(reply).to.be.an('array').to.be.empty; 92 | }); 93 | 94 | it('removes the group key from redis', async () => { 95 | const reply = await delAsync('group-test-key'); 96 | 97 | expect(reply).to.equal(0); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /test/routes-http.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { promisify } from 'util'; 3 | import request from 'request-promise'; 4 | 5 | // Mocking app modules 6 | import feathers from 'feathers'; 7 | import rest from 'feathers-rest'; 8 | import hooks from 'feathers-hooks'; 9 | import bodyParser from 'body-parser'; 10 | import errorHandler from 'feathers-errors/handler'; 11 | 12 | import redis from 'redis'; 13 | import routes from '../src/routes/cache'; 14 | 15 | // To be used with the mocked app 16 | import redisClient from '../src/redisClient'; 17 | // To be used with direct functions 18 | const client = redis.createClient(); 19 | const getAsync = promisify(client.get).bind(client); 20 | const setAsync = promisify(client.set).bind(client); 21 | const rpushAsync = promisify(client.rpush).bind(client); 22 | const lrangeAsync = promisify(client.lrange).bind(client); 23 | 24 | const PORT = 3030; 25 | const serverUrl = `http://0.0.0.0:${PORT}`; 26 | 27 | const HTTP_OK = 200; 28 | const HTTP_NO_CONTENT = 204; 29 | const HTTP_NOT_FOUND = 404; 30 | 31 | describe('Cache clearing http routes', () => { 32 | before(async () => { 33 | await setAsync('cache-test-key', 'value'); 34 | await setAsync('path-1', 'value-1'); 35 | await setAsync('path-2', 'value-2'); 36 | await setAsync('path-3', 'value-3'); 37 | await setAsync('path-with-query?test=true', 'value-query'); 38 | await rpushAsync('group-test-key', ['path-1', 'path-2', 'path-3']); 39 | 40 | // Create an express server asynchronously before tests 41 | const serverPromise = new Promise(resolve => { 42 | const app = feathers(); 43 | 44 | app.configure(rest()); 45 | app.configure(hooks()); 46 | // configure the redis client 47 | app.configure(redisClient); 48 | // Needed for parsing bodies (login) 49 | app.use(bodyParser.json()); 50 | app.use(bodyParser.urlencoded({ extended: true })) 51 | // add the cache routes (endpoints) to the app 52 | app.use('/cache', routes(app)); 53 | app.use(errorHandler()); 54 | 55 | app.listen(PORT, () => { 56 | resolve(); 57 | }); 58 | }); 59 | 60 | await serverPromise; 61 | }); 62 | 63 | it('gets an item from the cache', async () => { 64 | const reply = await getAsync('cache-test-key'); 65 | 66 | expect(reply).to.equal('value'); 67 | }); 68 | 69 | it('sends requests to the server', async () => { 70 | const uri = serverUrl; 71 | 72 | try { 73 | // Getting root gives 404 as it is not handled 74 | await request(uri); 75 | } catch (err) { 76 | expect(!!err).to.equal(true); 77 | expect(err.statusCode).to.equal(HTTP_NOT_FOUND); 78 | } 79 | }); 80 | 81 | it('returns OK when it removes an item from the cache', async () => { 82 | const options = { 83 | uri: serverUrl + '/cache/clear/single/cache-test-key', 84 | json: true 85 | }; 86 | 87 | try { 88 | const response = await request(options); 89 | 90 | expect(!!response).to.equal(true); 91 | expect(response.status).to.equal(HTTP_OK); 92 | expect(response.message).to.equal('cache cleared for key ' + 93 | '(without params): cache-test-key'); 94 | } catch (err) { 95 | throw new Error(err); 96 | } 97 | }); 98 | 99 | it('returns No Content when the trying to delete the same item again', 100 | async () => { 101 | const options = { 102 | uri: serverUrl + '/cache/clear/single/cache-test-key', 103 | json: true 104 | }; 105 | 106 | try { 107 | const response = await request(options); 108 | 109 | expect(!!response).to.equal(true); 110 | expect(response.status).to.equal(HTTP_NO_CONTENT); 111 | expect(response.message).to.equal('cache already cleared for key ' + 112 | '(without params): cache-test-key'); 113 | } catch (err) { 114 | throw new Error(err); 115 | } 116 | }); 117 | 118 | it('returns No Content when an item does not exist', async () => { 119 | const options = { 120 | uri: serverUrl + '/cache/clear/single/cache-does-not-exist', 121 | json: true 122 | }; 123 | 124 | try { 125 | const response = await request(options); 126 | 127 | expect(!!response).to.equal(true); 128 | // Should no raise errors but status No content 129 | expect(response.status).to.equal(HTTP_NO_CONTENT); 130 | expect(response.message).to.equal('cache already cleared for key ' + 131 | '(without params): cache-does-not-exist'); 132 | } catch (err) { 133 | throw new Error(err); 134 | } 135 | }); 136 | 137 | it('returns OK when it removes an item with query from the cache', async () => { 138 | const options = { 139 | uri: serverUrl + '/cache/clear/single/path-with-query?test=true', 140 | json: true 141 | }; 142 | 143 | try { 144 | const response = await request(options); 145 | 146 | expect(!!response).to.equal(true); 147 | expect(response.status).to.equal(HTTP_OK); 148 | expect(response.message).to.equal('cache cleared for key ' + 149 | '(with params): path-with-query?test=true'); 150 | } catch (err) { 151 | throw new Error(err); 152 | } 153 | }); 154 | 155 | it('removes all the item from a redis list array', async () => { 156 | const options = { 157 | uri: serverUrl + '/cache/clear/group/test-key', 158 | json: true 159 | }; 160 | 161 | try { 162 | const response = await request(options); 163 | 164 | expect(!!response).to.equal(true); 165 | expect(response.status).to.equal(HTTP_OK); 166 | expect(response.message).to.equal('cache cleared for the group key: ' + 167 | 'test-key'); 168 | 169 | // Make sure using the funciton 170 | const reply = await lrangeAsync('test-key', 0, -1); 171 | 172 | expect(reply).to.be.an('array').to.be.empty; 173 | } catch (err) { 174 | throw new Error(err); 175 | } 176 | }); 177 | 178 | it('returns No content when the trying to delete the same group again', 179 | async () => { 180 | const options = { 181 | uri: serverUrl + '/cache/clear/group/test-key', 182 | json: true 183 | }; 184 | 185 | try { 186 | const response = await request(options); 187 | 188 | expect(!!response).to.equal(true); 189 | expect(response.status).to.equal(HTTP_NO_CONTENT); 190 | expect(response.message).to.equal('cache already cleared for the ' + 191 | 'group key: test-key'); 192 | } catch (err) { 193 | throw new Error(err); 194 | } 195 | }); 196 | 197 | it('really removed keys in a group', () => { 198 | client.get('path-2', reply => { 199 | expect(reply).to.be.equal(null); 200 | }); 201 | }); 202 | 203 | it('returns route not found if no single target provided', async () => { 204 | const options = { 205 | uri: serverUrl + '/cache/clear/single', 206 | json: true 207 | }; 208 | 209 | try { 210 | await request(options); 211 | } catch (err) { 212 | expect(err.statusCode).to.equal(HTTP_NOT_FOUND); 213 | } 214 | }); 215 | 216 | it('returns route not found if no group target provided', async () => { 217 | const options = { 218 | uri: serverUrl + '/cache/clear/group/', 219 | json: true 220 | }; 221 | 222 | try { 223 | await request(options); 224 | } catch (err) { 225 | expect(err.statusCode).to.equal(HTTP_NOT_FOUND); 226 | } 227 | }); 228 | 229 | it('should allow clearing everything', async () => { 230 | const options = { 231 | uri: serverUrl + '/cache/clear', 232 | json: true 233 | }; 234 | 235 | try { 236 | const response = await request(options); 237 | 238 | expect(!!response).to.equal(true); 239 | expect(response.status).to.equal(HTTP_OK); 240 | expect(response.message).to.equal('Cache cleared'); 241 | } catch (err) { 242 | throw new Error(err); 243 | } 244 | }); 245 | }); 246 | -------------------------------------------------------------------------------- /test/run-last/redis-client.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { redisClient } from '../../src'; 3 | 4 | describe('Redis Before Hook', () => { 5 | 6 | it('It loads and responds', () => { 7 | expect(typeof redisClient).to.equal('function'); 8 | }); 9 | 10 | it('Loads the client with custom settings', () => { 11 | const c = redisClient.bind({ 12 | set: function (key, value) { 13 | this[key] = value; 14 | }, 15 | get: function (key) { 16 | return { 17 | host: 'my-redis-service.example.com', 18 | port: 1234 19 | }; 20 | } 21 | }); 22 | const result = c(); 23 | 24 | expect(result.redisClient.address).to.equal('my-redis-service.example.com:1234'); 25 | }); 26 | 27 | it('Loads the client with defaults', () => { 28 | const c = redisClient.bind({ 29 | set: function (key, value) { 30 | this[key] = value; 31 | }, 32 | get: function (key) { 33 | return null; 34 | } 35 | }); 36 | const result = c(); 37 | 38 | expect(result.redisClient.address).to.equal('127.0.0.1:6379'); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* global __dirname, require, module*/ 2 | 3 | // const webpack = require('webpack'); 4 | const fs = require('fs'); 5 | // const UglifyJsPlugin = webpack.optimize.UglifyJsPlugin; 6 | const path = require('path'); 7 | const env = require('yargs').argv.env; // use --env with webpack 2 8 | 9 | let libraryName = 'library'; 10 | 11 | let plugins = [], outputFile; 12 | 13 | if (env === 'build') { 14 | // plugins.push(new UglifyJsPlugin({ minimize: true })); 15 | outputFile = libraryName + '.min.js'; 16 | } else { 17 | outputFile = libraryName + '.js'; 18 | } 19 | 20 | let nodeModules = {}; 21 | 22 | fs.readdirSync('node_modules') 23 | .filter(function (x) { 24 | return ['.bin'].indexOf(x) === -1; 25 | }) 26 | .forEach(function (mod) { 27 | nodeModules[mod] = 'commonjs ' + mod; 28 | }); 29 | 30 | const config = { 31 | entry: __dirname + '/src/index.js', 32 | devtool: 'source-map', 33 | target: 'node', 34 | externals: nodeModules, 35 | output: { 36 | path: __dirname + '/lib', 37 | filename: outputFile, 38 | library: libraryName, 39 | libraryTarget: 'umd', 40 | umdNamedDefine: true 41 | }, 42 | module: { 43 | rules: [ 44 | { 45 | test: /(\.jsx|\.js)$/, 46 | loader: 'babel-loader', 47 | exclude: /(node_modules|bower_components)/ 48 | }, 49 | { 50 | test: /(\.jsx|\.js)$/, 51 | loader: 'eslint-loader', 52 | exclude: /node_modules/ 53 | } 54 | ] 55 | }, 56 | resolve: { 57 | modules: [path.resolve('./src')], 58 | extensions: ['.json', '.js'] 59 | }, 60 | plugins: plugins 61 | }; 62 | 63 | module.exports = config; 64 | --------------------------------------------------------------------------------