├── .babelrc ├── .editorconfig ├── .gitignore ├── .jshintrc ├── .travis.yml ├── LICENSE.txt ├── Makefile ├── README.md ├── package.json └── src ├── __tests__ └── index-test.js └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "modern-node/0.12", "stage-2" ] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [package.json] 12 | indent_size = 2 13 | 14 | [Makefile] 15 | indent_style = tab 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build artefacts 2 | /build/ 3 | 4 | # npm 5 | /node_modules/ 6 | /npm-debug.log 7 | /redux-storage-*.tgz 8 | 9 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext": true 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5.5" 4 | - "5.4" 5 | - "5.3" 6 | - "5.2" 7 | - "5.1" 8 | - "5.0" 9 | - "4.2" 10 | - "4.1" 11 | - "0.12" 12 | - "0.10" 13 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Michael Contento 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN = ./node_modules/.bin 2 | NPM = npm --loglevel=error 3 | 4 | # 5 | # INSTALL 6 | # 7 | 8 | install: node_modules/ 9 | 10 | node_modules/: package.json 11 | echo "> Installing ..." 12 | $(NPM) --ignore-scripts install > /dev/null 13 | touch node_modules/ 14 | 15 | # 16 | # CLEAN 17 | # 18 | 19 | clean: 20 | echo "> Cleaning ..." 21 | rm -rf build/ 22 | 23 | mrproper: clean 24 | echo "> Cleaning deep ..." 25 | rm -rf node_modules/ 26 | 27 | # 28 | # BUILD 29 | # 30 | 31 | build: clean install 32 | echo "> Building ..." 33 | $(BIN)/babel src/ --out-dir build/ 34 | 35 | build-watch: clean install 36 | echo "> Building forever ..." 37 | $(BIN)/babel src/ --out-dir build/ --watch 38 | 39 | # 40 | # TEST 41 | # 42 | 43 | lint: install 44 | echo "> Linting ..." 45 | $(BIN)/eslint src/ 46 | 47 | test: install 48 | echo "> Testing ..." 49 | $(BIN)/mocca 50 | 51 | test-watch: install 52 | echo "> Testing forever ..." 53 | $(BIN)/mocca --watch 54 | 55 | # 56 | # PUBLISH 57 | # 58 | 59 | _publish : NODE_ENV ?= production 60 | _publish: lint test build 61 | 62 | publish-fix: _publish 63 | $(BIN)/release-it --increment patch 64 | 65 | publish-feature: _publish 66 | $(BIN)/release-it --increment minor 67 | 68 | publish-breaking: _publish 69 | $(BIN)/release-it --increment major 70 | 71 | # 72 | # MAKEFILE 73 | # 74 | 75 | .PHONY: \ 76 | install \ 77 | clean mrproper \ 78 | build build-watch \ 79 | lint test test-watch \ 80 | publish-fix publish-feature publish-breaking 81 | 82 | .SILENT: 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [redux-storage-decorator-filter][] 2 | 3 | [![build](https://travis-ci.org/michaelcontento/redux-storage-decorator-filter.svg?branch=master)](https://travis-ci.org/michaelcontento/redux-storage-decorator-filter) 4 | [![dependencies](https://david-dm.org/michaelcontento/redux-storage-decorator-filter.svg)](https://david-dm.org/michaelcontento/redux-storage-decorator-filter) 5 | [![devDependencies](https://david-dm.org/michaelcontento/redux-storage-decorator-filter/dev-status.svg)](https://david-dm.org/michaelcontento/redux-storage-decorator-filter#info=devDependencies) 6 | 7 | [![license](https://img.shields.io/npm/l/redux-storage-decorator-filter.svg?style=flat-square)](https://www.npmjs.com/package/redux-storage-decorator-filter) 8 | [![npm version](https://img.shields.io/npm/v/redux-storage-decorator-filter.svg?style=flat-square)](https://www.npmjs.com/package/redux-storage-decorator-filter) 9 | [![npm downloads](https://img.shields.io/npm/dm/redux-storage-decorator-filter.svg?style=flat-square)](https://www.npmjs.com/package/redux-storage-decorator-filter) 10 | 11 | Filter decorator for [redux-storage][] to only store a subset of the whole 12 | state tree. 13 | 14 | # Moved to the react-stack organisation 15 | 16 | My focus has left the node / react ecosystem and this module has got a new home 17 | over at [react-stack](https://github.com/react-stack/redux-storage-decorator-filter)! 18 | 19 | ## Installation 20 | 21 | npm install --save redux-storage-decorator-filter 22 | 23 | ## Usage 24 | 25 | Simply wrap your engine in this decorator, whitelist all keys that should 26 | be passed through and blacklist the keys that shouldn't. 27 | 28 | ```js 29 | import filter from 'redux-storage-decorator-filter' 30 | 31 | engine = filter(engine, [ 32 | 'whitelisted-key', 33 | ['nested', 'key'], 34 | ['another', 'very', 'nested', 'key'] 35 | ], [ 36 | 'blacklisted-key', 37 | ['nested', 'blacklisted-key'] 38 | ]); 39 | ``` 40 | 41 | ## License 42 | 43 | The MIT License (MIT) 44 | 45 | Copyright (c) 2015 Michael Contento 46 | 47 | Permission is hereby granted, free of charge, to any person obtaining a copy of 48 | this software and associated documentation files (the "Software"), to deal in 49 | the Software without restriction, including without limitation the rights to 50 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 51 | the Software, and to permit persons to whom the Software is furnished to do so, 52 | subject to the following conditions: 53 | 54 | The above copyright notice and this permission notice shall be included in all 55 | copies or substantial portions of the Software. 56 | 57 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 58 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 59 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 60 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 61 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 62 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 63 | 64 | [redux-storage]: https://github.com/michaelcontento/redux-storage 65 | [redux-storage-decorator-filter]: https://github.com/michaelcontento/redux-storage-decorator-filter 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-storage-decorator-filter", 3 | "version": "1.1.6", 4 | "description": "Filter decorator for redux-storage", 5 | "main": "build/index.js", 6 | "scripts": { 7 | "test": "make lint test build" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/michaelcontento/redux-storage-decorator-filter.git" 12 | }, 13 | "homepage": "https://github.com/michaelcontento/redux-storage-decorator-filter", 14 | "keywords": [ 15 | "redux", 16 | "redux-storage", 17 | "redux-storage-decorator" 18 | ], 19 | "author": "Michael Contento ", 20 | "files": [ 21 | "build/", 22 | "src", 23 | "!**/__tests__/**" 24 | ], 25 | "eslintConfig": { 26 | "extends": "michaelcontento" 27 | }, 28 | "license": "MIT", 29 | "devDependencies": { 30 | "babel-cli": "^6.4.0", 31 | "babel-core": "^6.4.0", 32 | "babel-polyfill": "^6.3.14", 33 | "babel-preset-modern-node": "^2.1.0", 34 | "babel-preset-stage-2": "^6.5.0", 35 | "eslint": "^1.10.3", 36 | "eslint-config-michaelcontento": "^1.1.1", 37 | "eslint-plugin-mocha-only": "0.0.3", 38 | "immutable": "^3.7.6", 39 | "mocca": "^1.0.3", 40 | "release-it": "^2.3.1" 41 | }, 42 | "dependencies": { 43 | "lodash.get": "^4.1.2", 44 | "lodash.isfunction": "^3.0.7", 45 | "lodash.isobject": "^3.0.2", 46 | "lodash.reduce": "^4.3.0", 47 | "lodash.set": "^4.0.0", 48 | "lodash.unset": "^4.1.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/__tests__/index-test.js: -------------------------------------------------------------------------------- 1 | import { Map as map } from 'immutable'; 2 | 3 | import filter from '../'; 4 | 5 | describe('decorator', () => { 6 | it('should proxy load to engine.load', () => { 7 | const load = sinon.spy(); 8 | const engine = filter({ load }); 9 | 10 | engine.load(); 11 | 12 | load.should.have.been.called; 13 | }); 14 | 15 | it('should have a empty whitelist by default', () => { 16 | const save = sinon.spy(); 17 | const engine = filter({ save }); 18 | 19 | engine.save({ key: 'value' }); 20 | 21 | save.should.have.been.calledWith({}); 22 | }); 23 | 24 | it('should save whitelisted keys', () => { 25 | const save = sinon.spy(); 26 | const engine = filter({ save }, ['key']); 27 | 28 | engine.save({ key: 'value', ignored: true }); 29 | 30 | save.should.have.been.calledWith({ key: 'value' }); 31 | }); 32 | 33 | it('should save whitelisted keys paths', () => { 34 | const save = sinon.spy(); 35 | const engine = filter({ save }, [['some', 'key']]); 36 | 37 | engine.save({ some: { key: 'value' }, ignored: true }); 38 | 39 | save.should.have.been.calledWith({ some: { key: 'value' } }); 40 | }); 41 | 42 | it('should ignore not existing but whitelisted keys', () => { 43 | const save = sinon.spy(); 44 | const engine = filter({ save }, ['key']); 45 | 46 | engine.save({ ignored: true }); 47 | 48 | save.should.have.been.calledWith({}); 49 | }); 50 | 51 | it('should ignore not existing but whitelisted key paths', () => { 52 | const save = sinon.spy(); 53 | const engine = filter({ save }, [['a', 'nonExistingProp']]); 54 | 55 | engine.save({ a: { existingProp: true } }); 56 | 57 | save.should.have.been.calledWith({}); 58 | }); 59 | 60 | it('should support whitelisting an immutable', () => { 61 | const save = sinon.spy(); 62 | const engine = filter({ save }, [['some', 'key']]); 63 | 64 | engine.save({ some: map({ key: 42 }) }); 65 | 66 | save.should.have.been.calledWith({ some: { key: 42 } }); 67 | }); 68 | 69 | it('should handle null values (PR #64)', () => { 70 | const save = sinon.spy(); 71 | const engine = filter({ save }, [['a', 'first']]); 72 | 73 | engine.save({ a: { first: null, second: 2 } }); 74 | 75 | save.should.have.been.calledWith({ a: { first: null } }); 76 | }); 77 | 78 | it('should handle null values (PR #64) - immutable', () => { 79 | const save = sinon.spy(); 80 | const engine = filter({ save }, [['a', 'first']]); 81 | 82 | engine.save(map({ a: { first: null, second: 2 } })); 83 | 84 | save.should.have.been.calledWith({ a: { first: null } }); 85 | }); 86 | 87 | it('should have a empty blacklist by default', () => { 88 | const save = sinon.spy(); 89 | const engine = filter({ save }, ['key']); 90 | 91 | engine.save({ key: 'value' }); 92 | 93 | save.should.have.been.calledWith({ key: 'value' }); 94 | }); 95 | 96 | it('should not save blacklisted keys', () => { 97 | const save = sinon.spy(); 98 | const engine = filter({ save }, ['ignored'], ['ignored']); 99 | 100 | engine.save({ key: 'value', ignored: true }); 101 | 102 | save.should.have.been.calledWith({}); 103 | }); 104 | 105 | it('should not save blacklisted keys paths', () => { 106 | const save = sinon.spy(); 107 | const engine = filter({ save }, ['key', 'some'], [['some', 'ignored']]); 108 | 109 | engine.save({ key: 'value', some: { ignored: true } }); 110 | 111 | save.should.have.been.calledWith({ key: 'value', some: {} }); 112 | }); 113 | 114 | it('should NOT change the passed state with a blacklist (issue #13)', () => { 115 | const save = sinon.spy(); 116 | const engine = filter({ save }, null, [['some', 'a']]); 117 | 118 | const state = { some: { a: 1, b: 2 } }; 119 | engine.save(state); 120 | 121 | state.should.deep.equal({ some: { a: 1, b: 2 } }); 122 | }); 123 | 124 | it('should ignore not existing but blacklisted keys', () => { 125 | const save = sinon.spy(); 126 | const engine = filter({ save }, ['key'], ['ignored']); 127 | 128 | engine.save({ key: 'value' }); 129 | 130 | save.should.have.been.calledWith({ key: 'value' }); 131 | }); 132 | 133 | it('should ignore not existing but blacklisted key paths', () => { 134 | const save = sinon.spy(); 135 | const engine = filter({ save }, ['key'], [['some', 'ignored']]); 136 | 137 | engine.save({ key: 'value' }); 138 | 139 | save.should.have.been.calledWith({ key: 'value' }); 140 | }); 141 | 142 | it('should support blacklisting an immutable', () => { 143 | const save = sinon.spy(); 144 | const engine = filter({ save }, [['some', 'key']], [['some', 'key']]); 145 | 146 | engine.save({ some: map({ key: 42 }) }); 147 | 148 | save.should.have.been.calledWith({ some: {} }); 149 | }); 150 | 151 | it('should handle null values (PR #64)', () => { 152 | const save = sinon.spy(); 153 | const engine = filter({ save }, [['a', 'first']], [['a', 'first']]); 154 | 155 | engine.save({ a: { first: null, second: 2 } }); 156 | 157 | save.should.have.been.calledWith({ a: {} }); 158 | }); 159 | 160 | it('should handle null values (PR #64) - immutable', () => { 161 | const save = sinon.spy(); 162 | const engine = filter({ save }, [['a', 'first']], [['a', 'first']]); 163 | 164 | engine.save(map({ a: { first: null, second: 2 } })); 165 | 166 | save.should.have.been.calledWith({ a: {} }); 167 | }); 168 | 169 | it('should support blacklist only', () => { 170 | const save = sinon.spy(); 171 | const engine = filter({ save }, [], ['b']); 172 | 173 | engine.save({ a: 1, b: 2 }); 174 | 175 | save.should.have.been.calledWith({ a: 1 }); 176 | }); 177 | 178 | it('should support blacklist only - explicit null whitelist', () => { 179 | const save = sinon.spy(); 180 | const engine = filter({ save }, null, ['b']); 181 | 182 | engine.save({ a: 1, b: 2 }); 183 | 184 | save.should.have.been.calledWith({ a: 1 }); 185 | }); 186 | 187 | it('should support multiple blacklist with partially similar path', () => { 188 | const save = sinon.spy(); 189 | const engine = filter({ save }, null, [['a', '1'], ['a', '2']]); 190 | 191 | engine.save({ a: { '1': 1, '2': 2, '3': 3 } }); 192 | 193 | save.should.have.been.calledWith({ a: { '3': 3 } }); 194 | }); 195 | 196 | it('should support multiple blacklist with partially similar path - immutable', () => { 197 | const save = sinon.spy(); 198 | const engine = filter({ save }, ['a'], [['a', '1'], ['a', '2']]); 199 | 200 | engine.save({ a: map({ '1': 1, '2': 2 }) }); 201 | 202 | save.should.have.been.calledWith({ a: map({ }) }); 203 | }); 204 | 205 | it('should support empty blacklist', () => { 206 | const save = sinon.spy(); 207 | const engine = filter({ save }, null, []); 208 | 209 | engine.save({ a: { '1': 1, '2': 2 } }); 210 | 211 | save.should.have.been.calledWith({ a: { '1': 1, '2': 2 } }); 212 | }); 213 | }); 214 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import get from 'lodash.get'; 2 | import reduce from 'lodash.reduce'; 3 | import isFunction from 'lodash.isfunction'; 4 | import isObject from 'lodash.isobject'; 5 | import set from 'lodash.set'; 6 | import unset from 'lodash.unset'; 7 | 8 | export default (engine, whitelist = null, blacklist = null) => { 9 | whitelist = whitelist || []; // eslint-disable-line no-param-reassign 10 | 11 | return { 12 | ...engine, 13 | 14 | save(state) { 15 | let saveState = {}; 16 | 17 | // Copy the whole state if we're about to blacklist only 18 | if (whitelist.length === 0 && blacklist !== null) { 19 | saveState = { ...state }; 20 | } 21 | 22 | blacklist = blacklist || []; // eslint-disable-line no-param-reassign 23 | 24 | whitelist.forEach((key) => { 25 | // Support strings for one-level paths 26 | if (typeof key === 'string') { 27 | key = [key]; // eslint-disable-line no-param-reassign 28 | } 29 | 30 | const value = reduce(key, (result, keyPart) => { 31 | if (result) { 32 | // Support immutable structures 33 | if (isFunction(result.has) && isFunction(result.get)) { 34 | return result.get(keyPart); 35 | } 36 | 37 | if (result.hasOwnProperty(keyPart)) { 38 | return result[keyPart]; 39 | } 40 | } 41 | }, state); 42 | 43 | if (value !== undefined) { 44 | // done to support null values 45 | set(saveState, key, value); 46 | } 47 | }); 48 | 49 | blacklist.forEach((key) => { 50 | // Support strings for one-level paths 51 | if (typeof key === 'string') { 52 | key = [key]; // eslint-disable-line no-param-reassign 53 | } 54 | 55 | // Support immutable structures 56 | const value = state[key[0]]; 57 | const blacklistedState = saveState[key[0]] || value; 58 | 59 | if (blacklistedState && isFunction(blacklistedState.deleteIn)) { 60 | // Handle multiple blacklist path with same key 61 | saveState[key[0]] = blacklistedState.deleteIn(key.slice(1)); 62 | return; 63 | } 64 | 65 | // If we're a nested path ... 66 | if (key.length > 1) { 67 | // ... and inside a object ... 68 | const myKey = key.slice(0, -1); 69 | const subValue = get(saveState, myKey); 70 | if (isObject(subValue)) { 71 | // ... clone it, as we don't want to change the state! 72 | set(saveState, myKey, { ...subValue }); 73 | } 74 | } 75 | unset(saveState, key); 76 | }); 77 | 78 | return engine.save(saveState); 79 | } 80 | }; 81 | }; 82 | --------------------------------------------------------------------------------