├── .babelrc ├── index.js ├── .gitignore ├── package.json ├── LICENSE ├── README.md └── test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { normalize } from 'normalizr'; 2 | import assign from 'lodash/assign'; 3 | 4 | export default function normalizrMiddleware() { 5 | return store => next => action => { 6 | const schema = action.meta && action.meta.schema; 7 | 8 | if (schema && action.payload && !action.error) { 9 | const normalized = normalize(action.payload, schema); 10 | action = assign({}, action, { payload: normalized }); 11 | } 12 | 13 | return next(action); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib.js 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-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directory 28 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 29 | node_modules 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-normalizr-middleware", 3 | "version": "2.0.0", 4 | "description": "Combines redux middleware and normalizr to make flattening nested data a snap", 5 | "main": "lib.js", 6 | "repository": "github:wbinnssmith/redux-normalizr-middleware", 7 | "files": [ 8 | "README.md", 9 | "lib.js" 10 | ], 11 | "scripts": { 12 | "build": "babel index.js -o lib.js", 13 | "prepublish": "npm test && npm run build", 14 | "test": "babel-node test.js |faucet" 15 | }, 16 | "keywords": [ 17 | "normalizr", 18 | "redux", 19 | "redux-middleware" 20 | ], 21 | "author": "Will Binns-Smith ", 22 | "license": "MIT", 23 | "devDependencies": { 24 | "babel-cli": "^6.4.5", 25 | "babel-preset-es2015": "^6.3.13", 26 | "faucet": "0.0.1", 27 | "redux": "^3.0.5", 28 | "tap": "^5.1.1" 29 | }, 30 | "dependencies": { 31 | "normalizr": "^2.0.0", 32 | "lodash": "^4.0.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Will Binns-Smith 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 | # redux-normalizr-middleware 2 | Combines the power of redux middleware and [@gaearon](https://github.com/gaearon)'s 3 | [normalizr](https://github.com/gaearon/normalizr) to make flattening 4 | relational, nested data a snap. 5 | 6 | Use with [redux-thunk](https://github.com/gaearon/redux-thunk) or 7 | [redux-promise-middleware](https://github.com/pburtchaell/redux-promise-middleware) 8 | to easily request and store your API's response in a database-like fashion in your redux apps! 9 | 10 | For an example of a more manual implementation, check out [the 11 | real-world example in redux](https://github.com/rackt/redux/tree/master/examples/real-world) 12 | 13 | ## Installation 14 | `npm install --save redux-normalizr-middleware` 15 | 16 | ## Usage 17 | Place this middleware before anything that expects flattened 18 | data, and after anything that makes the nested data available (so before 19 | something like redux-thunk or redux-promise-middleware). 20 | 21 | redux-normalizr-middleware assumes that your actions comply with FSA and 22 | that your nested data is available as the `payload` property in your 23 | action, and will normalize and store the flattened data in the same 24 | `payload` property. Opt into redux-normalizr-middleware by supplying a 25 | [normalizr schema](https://github.com/gaearon/normalizr#usage) as 26 | `schema` in your action's `meta` object. 27 | 28 | ## Example 29 | ```js 30 | import normalizrMiddleware from 'redux-normalizr-middleware'; 31 | 32 | // import a schema defined using normalizr's `Schema`s to apply 33 | // to the response 34 | import todoSchema from './todo-schema'; 35 | 36 | const createStoreWithNormalizr = 37 | applyMiddleware(normalizrMiddleware())(createStore); 38 | 39 | // See the redux real-world example for this reducer pattern 40 | const store = createStoreWithNormalizr({ 41 | entitiesReducer: () => {}, 42 | todosByAuthor: () => {} 43 | }); 44 | 45 | // This could be dispatched from redux-thunk or redux-promise-middleware 46 | store.dispatch({ 47 | type: 'TODO_RECEIVED', 48 | payload: nestedTodoResponse, 49 | meta: { 50 | schema: todoSchema 51 | } 52 | }); 53 | ``` 54 | 55 | In the above, middlware following redux-normalizr-middleware and reducers 56 | connected to the redux store will receive the action payload as normalized, 57 | flattened data with `entities` and `results`! 58 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import tap, { test } from 'tap'; 2 | import { createStore, applyMiddleware } from 'redux'; 3 | import { Schema, arrayOf } from 'normalizr'; 4 | 5 | import normalizrMiddleware from './index'; 6 | 7 | function mergeReducer(state = {}, action) { 8 | return action.payload; 9 | } 10 | 11 | // from the normalizr readme 12 | const article = new Schema('articles'); 13 | const user = new Schema('users'); 14 | const collection = new Schema('collections'); 15 | 16 | article.define({ 17 | author: user, 18 | collections: arrayOf(collection) 19 | }); 20 | 21 | collection.define({ 22 | curator: user 23 | }); 24 | 25 | const schema = { 26 | articles: arrayOf(article) 27 | }; 28 | 29 | const response = { 30 | articles: [{ 31 | id: 1, 32 | title: 'Some Article', 33 | author: { 34 | id: 7, 35 | name: 'Dan' 36 | } 37 | }, { 38 | id: 2, 39 | title: 'Another Article', 40 | author: { 41 | id: 9, 42 | name: 'Will' 43 | } 44 | }] 45 | }; 46 | 47 | const expected = { 48 | result: { 49 | articles: [1, 2] 50 | }, 51 | entities: { 52 | articles: { 53 | 1: { 54 | id: 1, 55 | title: 'Some Article', 56 | author: 7, 57 | }, 58 | 2: { 59 | id: 2, 60 | title: 'Another Article', 61 | author: 9, 62 | } 63 | }, 64 | users: { 65 | 7: { 66 | id: 7, 67 | name: 'Dan' 68 | }, 69 | 9: { 70 | id: 9, 71 | name: 'Will' 72 | } 73 | } 74 | } 75 | }; 76 | 77 | test('normalizes payload with FSA defaults', t => { 78 | const createStoreWithNormalizr = 79 | applyMiddleware(normalizrMiddleware())(createStore); 80 | 81 | const store = createStoreWithNormalizr(mergeReducer); 82 | store.dispatch({ 83 | type: 'FOO', 84 | payload: response, 85 | meta: { 86 | schema 87 | } 88 | }) 89 | 90 | tap.deepEqual(store.getState(), expected); 91 | t.done(); 92 | }) 93 | 94 | test('action is unmodified before middleware', t => { 95 | const toDispatch = { 96 | type: 'FOO', 97 | payload: response, 98 | meta: { 99 | schema 100 | } 101 | }; 102 | 103 | const beforeMiddleware = store => next => action => { 104 | tap.equal(action, toDispatch); 105 | next(action); 106 | t.done() 107 | } 108 | 109 | const store = 110 | applyMiddleware( 111 | beforeMiddleware, 112 | normalizrMiddleware() 113 | )(createStore)(mergeReducer); 114 | 115 | store.dispatch(toDispatch) 116 | }) 117 | 118 | test('preserves other action properties when normalizing', t => { 119 | const toDispatch = { 120 | type: 'FOO', 121 | payload: response, 122 | meta: { 123 | schema, 124 | some: 'other', 125 | meta: 'data' 126 | } 127 | }; 128 | 129 | const afterMiddleware = store => next => action => { 130 | tap.notEqual(action, toDispatch); 131 | tap.notEqual(action.payload, toDispatch.payload); 132 | tap.equal(action.type, toDispatch.type); 133 | tap.equal(action.meta, toDispatch.meta); 134 | next(action); 135 | t.done() 136 | } 137 | 138 | const store = 139 | applyMiddleware( 140 | normalizrMiddleware(), 141 | afterMiddleware 142 | )(createStore)(mergeReducer); 143 | 144 | store.dispatch(toDispatch) 145 | }) 146 | --------------------------------------------------------------------------------