├── .gitignore ├── package.json ├── LICENSE ├── index.js ├── README.md └── test └── expire-tests.js /.gitignore: -------------------------------------------------------------------------------- 1 | # 2 | # Node 3 | # 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directory 31 | node_modules 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-persist-transform-expire", 3 | "version": "0.0.2", 4 | "description": "Add expiration to your persisted store", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha ./test", 8 | "lint": "semistandard" 9 | }, 10 | "keywords": [ 11 | "redux", 12 | "redux-persist", 13 | "redux-persist-transform", 14 | "expire" 15 | ], 16 | "author": "Gabriel Cebrian", 17 | "license": "MIT", 18 | "devDependencies": { 19 | "expect.js": "^0.3.1", 20 | "mocha": "^2.5.3", 21 | "moment": "^2.13.0", 22 | "redux-persist": "^3.2.2", 23 | "semistandard": "^8.0.0" 24 | }, 25 | "peerDependencies": { 26 | "redux-persist": ">=3.0" 27 | }, 28 | "dependencies": { 29 | "traverse": "^0.6.6" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Gabriel Cebrian 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 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var reduxPersist = require('redux-persist'); 2 | var traverse = require('traverse'); 3 | 4 | var PERSIST_EXPIRE_DEFAULT_KEY = 'persistExpiresAt'; 5 | 6 | module.exports = function (config) { 7 | config = config || {}; 8 | config.expireKey = config.expireKey || PERSIST_EXPIRE_DEFAULT_KEY; 9 | config.defaultState = config.defaultState || {}; 10 | 11 | function dateToUnix (date) { 12 | return +(date.getTime() / 1000).toFixed(0); 13 | } 14 | 15 | function inbound (state) { 16 | if (!state) return state; 17 | 18 | return state; 19 | } 20 | 21 | function outbound (state) { 22 | if (!state) return state; 23 | 24 | var validState = traverse(state).forEach(function (value) { 25 | if (!value || typeof value !== 'object') { 26 | return; 27 | } 28 | 29 | if (!value.hasOwnProperty(config.expireKey)) { 30 | return; 31 | } 32 | 33 | var expireDate = value[config.expireKey]; 34 | 35 | if (!expireDate) { 36 | return; 37 | } 38 | 39 | if (dateToUnix(new Date(expireDate)) < dateToUnix(new Date())) { 40 | this.update(config.defaultState); 41 | } 42 | }); 43 | 44 | return validState; 45 | } 46 | 47 | return reduxPersist.createTransform(inbound, outbound); 48 | }; 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redux-persist-transform-expire 2 | 3 | [![npm](https://img.shields.io/npm/v/redux-persist-transform-expire.svg?maxAge=2592000&style=flat-square)](https://www.npmjs.com/package/redux-persist-transform-expire) 4 | 5 | Add expiration to your persisted store. 6 | 7 | ## Usage 8 | 9 | ```js 10 | import createExpirationTransform from 'redux-persist-transform-expire'; 11 | 12 | const expireTransform = createExpirationTransform({ 13 | expireKey: 'customExpiresAt', 14 | defaultState: { 15 | custom: 'values' 16 | } 17 | }); 18 | 19 | persistStore(store, { 20 | transforms: [expireTransform] 21 | }); 22 | 23 | ``` 24 | Your expires key should be present in each reducer, which should be expired. E.g. 25 | ``` 26 | // top most reducer 27 | { 28 | reducerOne: { 29 | persistExpiresAt: '2017-04-11T15:46:54.338Z' 30 | }, 31 | reducerTwo: { 32 | persistExpiresAt: '2017-04-11T15:46:54.338Z' 33 | } 34 | } 35 | ``` 36 | 37 | 38 | ## Configuration 39 | 40 | | Attr | Type | Default | Notes | 41 | | ------------ | ------ | ------------------ | --------------------------------------------------- | 42 | | expireKey | String | 'persistExpiresAt' | Name of the attribute holding the expire date value | 43 | | defaultState | Any | {} | Shape of the state after expirations happen | 44 | -------------------------------------------------------------------------------- /test/expire-tests.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | 3 | var expect = require('expect.js'); 4 | var createExpireTransform = require('../index'); 5 | var moment = require('moment'); 6 | 7 | describe('Redux persists transform expire', function () { 8 | it('should do nothing if the state is an empty object', function (done) { 9 | var state = {}; 10 | var transform = createExpireTransform(); 11 | 12 | var inboundOutputState = transform.in(state); 13 | var outboundOutputState = transform.out(state); 14 | 15 | expect(inboundOutputState).to.eql(state); 16 | expect(outboundOutputState).to.eql(state); 17 | 18 | done(); 19 | }); 20 | 21 | it('should return the same if the state doesnt have an expire date prop', function (done) { 22 | var state = { 23 | app: { 24 | data: [1, 2] 25 | } 26 | }; 27 | 28 | var transform = createExpireTransform(); 29 | 30 | var inboundOutputState = transform.in(state); 31 | var outboundOutputState = transform.out(state); 32 | 33 | expect(inboundOutputState).to.eql(state); 34 | expect(outboundOutputState).to.eql(state); 35 | 36 | done(); 37 | }); 38 | 39 | it('should return the same state if it contains an expire prop that is not expired', function (done) { 40 | var state = { 41 | app: { 42 | reducer: { 43 | data: { 44 | values: [1, 2], 45 | persistExpiresAt: moment().add(1, 'hour').toDate() 46 | } 47 | } 48 | } 49 | }; 50 | 51 | var transform = createExpireTransform(); 52 | 53 | var inboundOutputState = transform.in(state); 54 | var outboundOutputState = transform.out(state); 55 | 56 | expect(inboundOutputState).to.eql(state); 57 | expect(outboundOutputState).to.eql(state); 58 | 59 | done(); 60 | }); 61 | 62 | it('should return expire a node if it contains an expire prop that is expired', function (done) { 63 | var state = { 64 | app: { 65 | reducer: { 66 | data: { 67 | values: [1, 2], 68 | persistExpiresAt: moment().subtract(1, 'hour').toDate() 69 | } 70 | } 71 | } 72 | }; 73 | 74 | var transform = createExpireTransform(); 75 | 76 | var inboundOutputState = transform.in(state); 77 | var outboundOutputState = transform.out(state); 78 | 79 | expect(inboundOutputState).to.eql(state); 80 | expect(outboundOutputState).to.eql({ 81 | app: { 82 | reducer: { 83 | data: { 84 | 85 | } 86 | } 87 | } 88 | }); 89 | 90 | done(); 91 | }); 92 | 93 | it('should return expire a node if it contains an expire prop that is expired on an array', function (done) { 94 | var notExpiredDate = moment().add(1, 'hour').toDate(); 95 | 96 | var state = { 97 | app: { 98 | reducer: { 99 | data: [{ 100 | values: [1, 2], 101 | persistExpiresAt: moment().subtract(1, 'hour').toDate() 102 | }, { 103 | values: [5, 6], 104 | persistExpiresAt: notExpiredDate 105 | }] 106 | } 107 | } 108 | }; 109 | 110 | var transform = createExpireTransform(); 111 | 112 | var inboundOutputState = transform.in(state); 113 | var outboundOutputState = transform.out(state); 114 | 115 | expect(inboundOutputState).to.eql(state); 116 | expect(outboundOutputState).to.eql({ 117 | app: { 118 | reducer: { 119 | data: [{ 120 | }, 121 | { 122 | values: [5, 6], 123 | persistExpiresAt: notExpiredDate 124 | }] 125 | } 126 | } 127 | }); 128 | 129 | done(); 130 | }); 131 | 132 | it('should return an empty state if the root element has an expire prop that is expired', function (done) { 133 | var state = { 134 | app: { 135 | persistExpiresAt: moment().subtract(1, 'hour').toDate(), 136 | reducer: { 137 | data: [1, 2] 138 | } 139 | } 140 | }; 141 | 142 | var transform = createExpireTransform(); 143 | 144 | var inboundOutputState = transform.in(state); 145 | var outboundOutputState = transform.out(state); 146 | 147 | expect(inboundOutputState).to.eql(state); 148 | expect(outboundOutputState).to.eql({ app: {} }); 149 | 150 | done(); 151 | }); 152 | 153 | it('should ignore the expire prop if it doesnt contain a valid date', function (done) { 154 | var state = { 155 | app: { 156 | persistExpiresAt: 'invalid-date', 157 | reducer: { 158 | data: [1, 2] 159 | } 160 | } 161 | }; 162 | 163 | var transform = createExpireTransform(); 164 | 165 | var inboundOutputState = transform.in(state); 166 | var outboundOutputState = transform.out(state); 167 | 168 | expect(inboundOutputState).to.eql(state); 169 | expect(outboundOutputState).to.eql(state); 170 | 171 | done(); 172 | }); 173 | 174 | it('should allow a user to override the expire prop key', function (done) { 175 | var state = { 176 | app: { 177 | reducer: { 178 | data: { 179 | values: [1, 2], 180 | myExpireKey: moment().subtract(1, 'hour').toDate() 181 | } 182 | } 183 | } 184 | }; 185 | 186 | var config = { 187 | expireKey: 'myExpireKey' 188 | }; 189 | 190 | var transform = createExpireTransform(config); 191 | 192 | var inboundOutputState = transform.in(state); 193 | var outboundOutputState = transform.out(state); 194 | 195 | expect(inboundOutputState).to.eql(state); 196 | expect(outboundOutputState).to.eql({ 197 | app: { 198 | reducer: { 199 | data: { 200 | 201 | } 202 | } 203 | } 204 | }); 205 | 206 | done(); 207 | }); 208 | 209 | it('should allow a user to override the default state', function (done) { 210 | var state = { 211 | app: { 212 | reducer: { 213 | data: { 214 | values: [1, 2], 215 | persistExpiresAt: moment().subtract(1, 'hour').toDate() 216 | } 217 | } 218 | } 219 | }; 220 | 221 | var config = { 222 | defaultState: { 223 | values: [3] 224 | } 225 | }; 226 | 227 | var transform = createExpireTransform(config); 228 | 229 | var inboundOutputState = transform.in(state); 230 | var outboundOutputState = transform.out(state); 231 | 232 | expect(inboundOutputState).to.eql(state); 233 | expect(outboundOutputState).to.eql({ 234 | app: { 235 | reducer: { 236 | data: { 237 | values: [3] 238 | } 239 | } 240 | } 241 | }); 242 | 243 | done(); 244 | }); 245 | }); 246 | --------------------------------------------------------------------------------