├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── .vscode └── settings.json ├── .yarnclean ├── CHANGELOG.md ├── README.md ├── examples ├── README.md ├── complete │ ├── .babelrc │ ├── package.json │ ├── public │ │ └── index.html │ ├── src │ │ ├── App.js │ │ ├── api.js │ │ ├── index.js │ │ ├── middlewares.js │ │ ├── store.js │ │ ├── todos │ │ │ ├── actions.js │ │ │ ├── components │ │ │ │ ├── Footer.js │ │ │ │ ├── Link.js │ │ │ │ ├── PagingTodos.js │ │ │ │ ├── Todo.js │ │ │ │ └── TodoList.js │ │ │ ├── containers │ │ │ │ ├── ActionLink.js │ │ │ │ ├── AddTodo.js │ │ │ │ ├── CompletedTodos.js │ │ │ │ ├── FilterLink.js │ │ │ │ ├── IncompleteTodos.js │ │ │ │ └── VisibleTodoList.js │ │ │ ├── reducer.js │ │ │ └── selectors.js │ │ └── user │ │ │ ├── actions.js │ │ │ ├── components │ │ │ └── Login.js │ │ │ ├── containers │ │ │ └── Login.js │ │ │ ├── reducer.js │ │ │ └── selectors.js │ └── webpack.config.js ├── minimal-features │ ├── .babelrc │ ├── package.json │ ├── public │ │ └── index.html │ ├── src │ │ └── index.js │ └── webpack.config.js ├── minimal │ ├── .babelrc │ ├── package.json │ ├── public │ │ └── index.html │ ├── src │ │ └── index.js │ └── webpack.config.js └── server │ ├── .editorconfig │ ├── .eslintignore │ ├── .eslintrc │ ├── .gitignore │ ├── .yo-rc.json │ ├── README.md │ ├── client │ └── README.md │ ├── common │ └── models │ │ ├── comment.json │ │ ├── todo.js │ │ └── todo.json │ ├── package.json │ └── server │ ├── boot │ ├── authentication.js │ ├── dummy-data.js │ └── root.js │ ├── component-config.json │ ├── config.json │ ├── datasources.json │ ├── middleware.development.json │ ├── middleware.json │ ├── model-config.json │ └── server.js ├── package.json ├── src ├── adapter │ ├── ApiAdapter.js │ └── RequestAdapter.js ├── constants.js ├── createReducer.js ├── hoc │ └── paging.js ├── index.js ├── list │ ├── ListActions.js │ ├── index.js │ ├── listReducer.js │ └── listSelectors.js └── model │ ├── ModelActions.js │ ├── index.js │ ├── modelReducer.js │ └── modelSelectors.js └── tests ├── integration └── smoke.js └── unit ├── list ├── reducers │ └── list.test.js └── selectors │ └── list.test.js └── model ├── actions.test.js ├── model.test.js ├── reducers ├── error.test.js ├── instance.test.js └── request.test.js └── selectors └── model.test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-0"] 3 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "standard", 5 | "standard-react" 6 | ], 7 | "plugins": [ 8 | "babel", 9 | "react", 10 | "promise" 11 | ], 12 | "env": { 13 | "browser": true 14 | }, 15 | "globals": { 16 | "__DEV__": false, 17 | "__TEST__": false, 18 | "__PROD__": false, 19 | "__COVERAGE__": false 20 | }, 21 | "rules": { 22 | "key-spacing": 0, 23 | "jsx-quotes": [ 24 | 2, 25 | "prefer-single" 26 | ], 27 | "max-len": [ 28 | 1, 29 | 120, 30 | 2 31 | ], 32 | "object-curly-spacing": [ 33 | 1, 34 | "always" 35 | ], 36 | "comma-dangle": [ 37 | 1, 38 | { 39 | "arrays": "only-multiline", 40 | "objects": "only-multiline", 41 | "imports": "never", 42 | "exports": "never", 43 | "functions": "ignore" 44 | } 45 | ], 46 | "space-before-function-paren": [ 47 | 1, 48 | "never" 49 | ] 50 | } 51 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | dist 4 | yarn.lock 5 | *.log 6 | coverage -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | !lib 2 | .babelrc -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7" 4 | cache: 5 | directories: 6 | - node_modules 7 | after_success: 8 | - bash <(curl -s https://codecov.io/bash) -e TRAVIS_NODE_VERSION -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | } -------------------------------------------------------------------------------- /.yarnclean: -------------------------------------------------------------------------------- 1 | # test directories 2 | __tests__ 3 | test 4 | tests 5 | powered-test 6 | 7 | # asset directories 8 | docs 9 | doc 10 | website 11 | images 12 | assets 13 | 14 | # examples 15 | example 16 | examples 17 | 18 | # code coverage directories 19 | coverage 20 | .nyc_output 21 | 22 | # build scripts 23 | Makefile 24 | Gulpfile.js 25 | Gruntfile.js 26 | 27 | # configs 28 | .tern-project 29 | .gitattributes 30 | .editorconfig 31 | .*ignore 32 | .eslintrc 33 | .jshintrc 34 | .flowconfig 35 | .documentup.json 36 | .yarn-metadata.json 37 | .*.yml 38 | *.yml 39 | 40 | # misc 41 | *.gz 42 | *.md 43 | 44 | # asset directories 45 | !istanbul-reports/lib/html/assets 46 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Change Log 2 | 3 | ### 0.4.0 (2017/08/19 03:08 +00:00) 4 | - [5969173](https://github.com/nachiket-p/rest-redux/commit/5969173a5d2e6f5a1671a4a1f1bed505e4285b8f) Merge branch 'dev' (@nachiket-p) 5 | - [#10](https://github.com/nachiket-p/rest-redux/pull/10) Added Model action test (@agarwalraghav01) 6 | - [47e5714](https://github.com/nachiket-p/rest-redux/commit/47e57141e88ede50b8607c2a30d2ec05c9b025ec) Added Model action test (@agarwalraghav01) 7 | 8 | ### v0.3.1 (2017/07/24 12:31 +00:00) 9 | - [9c56d49](https://github.com/nachiket-p/rest-redux/commit/9c56d4907c0d234a98ef9fcdb7c9584f62d8c226) 0.3.1 (@nachiket-p) 10 | - [#9](https://github.com/nachiket-p/rest-redux/pull/9) Corrected list selector test (@agarwalraghav01) 11 | - [669c661](https://github.com/nachiket-p/rest-redux/commit/669c661ba20fa09f029edb7ac469431fb9440725) Bugfix: offset was not getting set properly (@nachiket-p) 12 | - [0609204](https://github.com/nachiket-p/rest-redux/commit/0609204d91fce629fd27c441c4516dd6a7439b4d) Added test for hasPrev and hasNext in list selector (@agarwalraghav01) 13 | - [22e17d0](https://github.com/nachiket-p/rest-redux/commit/22e17d065b538319d3f8bcb588d38f9a565cdbd0) Added .yarnclean file (@nachiket-p) 14 | - [7c41853](https://github.com/nachiket-p/rest-redux/commit/7c418533b46b1fb894cff69d02d8c3498b670e4f) Merge branch 'dev' of https://github.com/nachiket-p/rest-redux into dev (@agarwalraghav01) 15 | - [0ccf7b1](https://github.com/nachiket-p/rest-redux/commit/0ccf7b199ca0c7f308570c93d6aa7c1e483b6b5c) Added some action tests & Removed unsed tests (@nachiket-p) 16 | - [edacabe](https://github.com/nachiket-p/rest-redux/commit/edacabea73a2d9b2b7140ae3e282c4f4f58e6405) corrected package deps (@nachiket-p) 17 | - [b423784](https://github.com/nachiket-p/rest-redux/commit/b4237841cf12e974f29d5c037e5719feb9a5b45e) removed unnecessary test from list selector (@agarwalraghav01) 18 | - [ca007ed](https://github.com/nachiket-p/rest-redux/commit/ca007edc8d5e0eecccb6c53e7c58a5697e824c31) changes made in hasNext and hasPrev in list selector (@agarwalraghav01) 19 | - [dceff35](https://github.com/nachiket-p/rest-redux/commit/dceff3509e12702ff046289146a317d271cea142) 0.3.0 (@nachiket-p) 20 | - [#8](https://github.com/nachiket-p/rest-redux/pull/8) Completed selector test (@agarwalraghav01) 21 | - [03ada1f](https://github.com/nachiket-p/rest-redux/commit/03ada1f45222a74b728ca998312fdb77d5882014) completed list selector test (@agarwalraghav01) 22 | - [77d4308](https://github.com/nachiket-p/rest-redux/commit/77d4308e8d42c7f1c98fa3c04e9529e7905a8616) selector test for list and model (@agarwalraghav01) 23 | - [8c996fc](https://github.com/nachiket-p/rest-redux/commit/8c996fc386a881e05772f2eda9d9b3ba6fadcede) Added some tests for Model Selectors (@nachiket-p) 24 | - [a145d60](https://github.com/nachiket-p/rest-redux/commit/a145d60a0202fa924944b157c6d496de1bf95107) corrected server code & corrected complete example to support API changes (@nachiket-p) 25 | - [ab709c5](https://github.com/nachiket-p/rest-redux/commit/ab709c5497210c187b119677a7ce6b1c1069a61b) added new example with feature rich minimal code (@nachiket-p) 26 | - [fd476b0](https://github.com/nachiket-p/rest-redux/commit/fd476b012431fe097954a23c19598dc7574a3f8f) changed List API to support routeParams API changes (@nachiket-p) 27 | - [3fe70c8](https://github.com/nachiket-p/rest-redux/commit/3fe70c86dceee897fdc751286b7c79649772bdab) Implemented routeParams in actions only. selector & reducer remaining. (@nachiket-p) 28 | - [a02e499](https://github.com/nachiket-p/rest-redux/commit/a02e499f66d1b3fa73447955787ee3b9c71a5517) Merge branch 'dev' into route-params (@nachiket-p) 29 | - [18bc1e5](https://github.com/nachiket-p/rest-redux/commit/18bc1e5afd7150c9302becdf86281597cfcb4f14) route params support idea, WIP (@nachiket-p) 30 | - [#7](https://github.com/nachiket-p/rest-redux/pull/7) Completed list reducers test (@agarwalraghav01) 31 | - [216655f](https://github.com/nachiket-p/rest-redux/commit/216655f9168aad765c676589fd7670cae425666c) Modified Complete List Reducer (@agarwalraghav01) 32 | - [cd9ad7d](https://github.com/nachiket-p/rest-redux/commit/cd9ad7dd78b3576262ff1e76c4ec441a0e3db2e2) removed unnecessary test cases (@agarwalraghav01) 33 | - [e3bc6a1](https://github.com/nachiket-p/rest-redux/commit/e3bc6a16aad83daa29082a3afe5e0213a47c9665) Test for list Reducers (@agarwalraghav01) 34 | - [abba5b1](https://github.com/nachiket-p/rest-redux/commit/abba5b140e75326f208df0f11f7146a8dbdfaec7) removed unnecessary lib from package.json (@agarwalraghav01) 35 | - [12d7413](https://github.com/nachiket-p/rest-redux/commit/12d7413d2c54c4754f738b98ba37b0b5b0b786d6) completed list reducers test (@agarwalraghav01) 36 | - [010483b](https://github.com/nachiket-p/rest-redux/commit/010483bda390d06cd78158901fe7e0082618a493) list reducers test (@agarwalraghav01) 37 | - [#6](https://github.com/nachiket-p/rest-redux/pull/6) Added test for Model Reducers (@agarwalraghav01) 38 | - [e9105c2](https://github.com/nachiket-p/rest-redux/commit/e9105c245d3ec11c650653113e4908650dbf1ed3) added model tests] (@agarwalraghav01) 39 | - [cdcc214](https://github.com/nachiket-p/rest-redux/commit/cdcc214852382d75bb332482cf2850495ea7bdda) Merge branch 'dev' into testing (@agarwalraghav01) 40 | - [b71c301](https://github.com/nachiket-p/rest-redux/commit/b71c301e2923f7da220e8ab35f12212e2eb6bff3) Test for model reducer (@agarwalraghav01) 41 | - [00ce7a9](https://github.com/nachiket-p/rest-redux/commit/00ce7a990fbdc2412c5036e6be03a24c4ce46c86) Test for Model Reducers (@agarwalraghav01) 42 | - [2e2cf74](https://github.com/nachiket-p/rest-redux/commit/2e2cf749a4f08bb8b33e4611e54bb544e4e6ca97) complex example testing (@nachiket-p) 43 | - [344b176](https://github.com/nachiket-p/rest-redux/commit/344b1768953b9ff1a4bb70655a71b0ea751e17be) custom API Path issue fixed (@nachiket-p) 44 | - [34b95c8](https://github.com/nachiket-p/rest-redux/commit/34b95c887178cff490cdd985bc1a5e6554570da6) Created RequestAdapter for extending (@nachiket-p) 45 | 46 | ### v0.2.1 (2017/07/03 12:19 +00:00) 47 | - [b1cc326](https://github.com/nachiket-p/rest-redux/commit/b1cc326942b009ddc587dd2c65a47bff6c619cb8) 0.2.1 (@nachiket-p) 48 | - [ff67c94](https://github.com/nachiket-p/rest-redux/commit/ff67c94701ec4c74afce9078807ea17c4f366da9) added babelrc (@nachiket-p) 49 | 50 | ### v0.2.0 (2017/07/03 12:06 +00:00) 51 | - [d939674](https://github.com/nachiket-p/rest-redux/commit/d9396741416dac0ff63722bd82cff1263179b176) 0.2.0 (@nachiket-p) 52 | - [ca0bed9](https://github.com/nachiket-p/rest-redux/commit/ca0bed9c5eee8a1b6e6eb52c28cbd899d0bea789) changelog output now prints commit as well (@nachiket-p) 53 | - [446d514](https://github.com/nachiket-p/rest-redux/commit/446d5148aa01561a7a572e7e2aead8826a430e0f) removed .babelrc for npm publish (@nachiket-p) 54 | - [64a3556](https://github.com/nachiket-p/rest-redux/commit/64a355675c0fc7ff627007b755dcb582d949ec9c) Changed react-redux & redux dep (@nachiket-p) 55 | - [dc370f3](https://github.com/nachiket-p/rest-redux/commit/dc370f32cb1f9e21b4a03db8e3d4566ebd74fcb7) Removed Thunk dependency, Must be available as peer (@nachiket-p) 56 | - [497d694](https://github.com/nachiket-p/rest-redux/commit/497d694f7ccdfeff80eabbaa622ec813f5511a42) Added Minimal Example (@nachiket-p) 57 | - [8e83cab](https://github.com/nachiket-p/rest-redux/commit/8e83cab86657ea07785e1169b22d6955bcfa711a) Example: Added completed todo in initial data (@nachiket-p) 58 | - [7fbb62c](https://github.com/nachiket-p/rest-redux/commit/7fbb62c61218c739f5511663cf75b5911d77f14f) Examples restructure (@nachiket-p) 59 | 60 | ### v0.1.8 (2017/06/29 12:56 +00:00) 61 | - [869e25e](https://github.com/nachiket-p/rest-redux/commit/869e25e3a675ae68ce8686b07f28bc3532c2ab43) 0.1.8 (@nachiket-p) 62 | - [de1b1e8](https://github.com/nachiket-p/rest-redux/commit/de1b1e8cd4d037908a011cb5f407cdf52de6f6e1) added npmignore to add lib in npm (@nachiket-p) 63 | - [3c5d62a](https://github.com/nachiket-p/rest-redux/commit/3c5d62a31a7b900b638e1c36f2f88d1635f652d2) Merge branch 'dev' of github.com:nachiket-p/rest-redux into dev (@nachiket-p) 64 | - [#3](https://github.com/nachiket-p/rest-redux/pull/3) Remove unused paging attribute (@jariwalabhavesh) 65 | 66 | ### v0.1.7 (2017/06/29 12:22 +00:00) 67 | - [d2c38ea](https://github.com/nachiket-p/rest-redux/commit/d2c38ea8e506a15db45ab6a1a7ed9cba8e52096c) 0.1.7 (@nachiket-p) 68 | - [dc5d820](https://github.com/nachiket-p/rest-redux/commit/dc5d82045aad8d6e417f4974f6319cee8eeb3415) corrected main path (@nachiket-p) 69 | 70 | ### v0.1.6 (2017/06/29 11:42 +00:00) 71 | - [7eecfa1](https://github.com/nachiket-p/rest-redux/commit/7eecfa1472eb701581651bcf0f0a0c9d6ca1bbc8) 0.1.6 (@nachiket-p) 72 | - [9b2e7ff](https://github.com/nachiket-p/rest-redux/commit/9b2e7ffddc7456afa9f3030f900ddc0be51d4d78) corrected main path (@nachiket-p) 73 | - [6a11902](https://github.com/nachiket-p/rest-redux/commit/6a1190294d748ba74665d87957dedf4ebb415e43) 1.Readme modified 74 | - [a8b8dad](https://github.com/nachiket-p/rest-redux/commit/a8b8dad9a9406ac9b03f93f282f6b8924d8fc705) Remove unused paging attribute 75 | 76 | ### v0.1.5 (2017/06/29 06:35 +00:00) 77 | - [9af5593](https://github.com/nachiket-p/rest-redux/commit/9af5593021960fff3aae3620b45730c56c44ce4a) 0.1.5 (@nachiket-p) 78 | - [b9b88b7](https://github.com/nachiket-p/rest-redux/commit/b9b88b748674457632fb74af2431a52d6f1dbbaf) corrected deps in package.json, fixed major bug (@nachiket-p) 79 | - [2d7bf4f](https://github.com/nachiket-p/rest-redux/commit/2d7bf4f5821bdbf7c7232ae16d4d8933669744ce) added codecov & coverage settings (@nachiket-p) 80 | - [8408d11](https://github.com/nachiket-p/rest-redux/commit/8408d114a0414031d59c9824ed52db34a20507e3) added build & coverage badges (@nachiket-p) 81 | 82 | ### v0.1.4 (2017/06/28 12:38 +00:00) 83 | - [9d14de2](https://github.com/nachiket-p/rest-redux/commit/9d14de2aff2b3e1f6d941cbe2d5f7c9be0408b51) 0.1.4 (@nachiket-p) 84 | - [311499a](https://github.com/nachiket-p/rest-redux/commit/311499abc74207284b46226cc0029de4d63fcf95) setup changelog command properly (@nachiket-p) 85 | - [281df62](https://github.com/nachiket-p/rest-redux/commit/281df6275bf257dc0cbdb4bed2cea78844bad62e) capital cased changelog & readme (@nachiket-p) 86 | - [61fb642](https://github.com/nachiket-p/rest-redux/commit/61fb642d9821d362b061ad8b4ce1643c56c7d795) added empty changelog & added repository to package.json (@nachiket-p) 87 | - [6b4038b](https://github.com/nachiket-p/rest-redux/commit/6b4038b8039d305a91d1e32177bf1acdf0f9c168) added version scripts to package.json (@nachiket-p) 88 | 89 | ### v0.1.1 (2017/06/28 11:42 +00:00) 90 | - [a6388bf](https://github.com/nachiket-p/rest-redux/commit/a6388bf9eb435af67e6a0e7c192f732ef25a139e) 0.1.1 (@nachiket-p) 91 | - [252348a](https://github.com/nachiket-p/rest-redux/commit/252348a7e98bad048dc4fa6bc6d66a04c86e7c02) added generate-changelog dev dep (@nachiket-p) 92 | - [4eaa25f](https://github.com/nachiket-p/rest-redux/commit/4eaa25f1aa1435732968a91eda2feb7ec28adf19) Test fixed (@nachiket-p) 93 | - [f6f977d](https://github.com/nachiket-p/rest-redux/commit/f6f977d31873c887748687e125ed00ea4d39b138) Updated example with clean method (@nachiket-p) 94 | - [7df1f7c](https://github.com/nachiket-p/rest-redux/commit/7df1f7cb90d0ab5d977bb36a076f23c4d7ca2223) Implemented clear method to clean up state (@nachiket-p) 95 | - [#2](https://github.com/nachiket-p/rest-redux/pull/2) Paging action validations (@jariwalabhavesh) 96 | - [b495dbf](https://github.com/nachiket-p/rest-redux/commit/b495dbfa58748aa1e8c77c09a60b98bc74d77194) Merge branch 'dev' into paging-validation (@nachiket-p) 97 | - [5e7961c](https://github.com/nachiket-p/rest-redux/commit/5e7961c3f99f9039f72001a24816451dc78918ba) Example updated (@nachiket-p) 98 | - [7269f8a](https://github.com/nachiket-p/rest-redux/commit/7269f8a44a9e31a953256b51b73bdc1c6169bba4) Bugfix: hoc props passing (@nachiket-p) 99 | - [f06bac3](https://github.com/nachiket-p/rest-redux/commit/f06bac38b775036a0010f26ce15fa6316f7102fd) Paging action validation done. 100 | - [93ae45c](https://github.com/nachiket-p/rest-redux/commit/93ae45ce9b9181694a11e5d46aa4e1b2f896861a) Implemented paging HOC example (@nachiket-p) 101 | - [81b437d](https://github.com/nachiket-p/rest-redux/commit/81b437de38ff6117c9ea7d6bfa0ce0bcc0d3a97c) bugfix: list reducer was updating all list states (@nachiket-p) 102 | - [4bc1d21](https://github.com/nachiket-p/rest-redux/commit/4bc1d21f3a4e5fd3b6e0f1b78f1e42e0066a8701) Implemented paging HOC (@nachiket-p) 103 | - [da2d80f](https://github.com/nachiket-p/rest-redux/commit/da2d80fb14896c5d64c13979d363db59c2968f0b) Removed unused file (@nachiket-p) 104 | - [0148fd7](https://github.com/nachiket-p/rest-redux/commit/0148fd7e03355b1b1dc1cd2fbf54008fa10947ee) Added travis config (@nachiket-p) 105 | - [4acef65](https://github.com/nachiket-p/rest-redux/commit/4acef65b16e8754f3e0b131f7598c6453a4341bf) Bugfix: listname was not passed to action (@nachiket-p) 106 | - [12a968c](https://github.com/nachiket-p/rest-redux/commit/12a968ca014410701a1f264f14d21d3cede95a38) Renamed to rest-redux from redux-loopback (@nachiket-p) 107 | - [3c72364](https://github.com/nachiket-p/rest-redux/commit/3c723641c8a8c2168a48949f2ee0821ff58a960e) Implemented in example (@nachiket-p) 108 | - [e4b2b92](https://github.com/nachiket-p/rest-redux/commit/e4b2b92244d0414084fc0acc0cb7e4c3e67b1517) Implemented basic working List support (@nachiket-p) 109 | - [2b26e49](https://github.com/nachiket-p/rest-redux/commit/2b26e4976cc9d9d49950c8d9be373f4221a5f33e) Fixed prepublish without global babel dep issue (@nachiket-p) 110 | - [654e761](https://github.com/nachiket-p/rest-redux/commit/654e761c98f919fdbcc2511ed7b268fe73447663) added running example section in readme (@nachiket-p) 111 | - [8489033](https://github.com/nachiket-p/rest-redux/commit/8489033aa7fd6d6e98455af8075227dbfd727f2b) Merge branch 'master' of github.com:nachiket-p/redux-loopback (@nachiket-p) 112 | - [37d52b7](https://github.com/nachiket-p/rest-redux/commit/37d52b72b999fbf86ba08468d0fa6292e850ec15) Updated readme (@nachiket-p) 113 | - [15ea04e](https://github.com/nachiket-p/rest-redux/commit/15ea04e54fc4c24edd94fd592fa6ec8bf6021af4) updated build commands (@nachiket-p) 114 | - [d57c366](https://github.com/nachiket-p/rest-redux/commit/d57c36659285acdc54753fc54ea1d38d05fc662f) WIP, List support (@nachiket-p) 115 | - [966c613](https://github.com/nachiket-p/rest-redux/commit/966c6132828dc280eff503dcf23695a87a380070) removed debugger (@nachiket-p) 116 | - [6aae182](https://github.com/nachiket-p/rest-redux/commit/6aae182ec692bf25a37d160f425bdcf9ddbe7287) WIP: List feature & APIs (@nachiket-p) 117 | - [7c47f5e](https://github.com/nachiket-p/rest-redux/commit/7c47f5e81774331bc60644dc303f5eea81a94fd5) added possible API for lists (@nachiket-p) 118 | - [65dd787](https://github.com/nachiket-p/rest-redux/commit/65dd787705e8f645135446dbdb03c56d18cc4962) Refactored Model code & added dummy List API (@nachiket-p) 119 | - [#1](https://github.com/nachiket-p/rest-redux/pull/1) moved to master as example/dev is working well. (@nachiket-p) 120 | - [26a216f](https://github.com/nachiket-p/rest-redux/commit/26a216f0d25df1e6aef86e07577b9d4f276057fe) added WIP in readme (@nachiket-p) 121 | - [0b7dfbe](https://github.com/nachiket-p/rest-redux/commit/0b7dfbef613622ebd1b0e0e9e90920476ac47420) Example, added login (@nachiket-p) 122 | - [938cb5d](https://github.com/nachiket-p/rest-redux/commit/938cb5dfde49957d361d547d05178a0cd1c809df) continuing catch chain for fetch (@nachiket-p) 123 | - [72c270a](https://github.com/nachiket-p/rest-redux/commit/72c270a230fa6524cc51c33fc25bcb10f54765f7) example fix: update count after deleteAll (@nachiket-p) 124 | - [d85d40f](https://github.com/nachiket-p/rest-redux/commit/d85d40f5912ca87d3a750624adbeef5dd59a9969) Added todos (@nachiket-p) 125 | - [e6ebcaf](https://github.com/nachiket-p/rest-redux/commit/e6ebcaf7172c2c13ec7cc433b2645092c2a0596b) ApiAdapter introduced, API related call separated (@nachiket-p) 126 | - [555a3a1](https://github.com/nachiket-p/rest-redux/commit/555a3a1f872b736f737aa796775e06f0bb8756be) added ACL in example (@nachiket-p) 127 | - [3783610](https://github.com/nachiket-p/rest-redux/commit/3783610c502fa63f4c5565ccb872a576bfeea91a) updated for globalOptions (@nachiket-p) 128 | - [4866e97](https://github.com/nachiket-p/rest-redux/commit/4866e97623e303248333f17acd14182a3f0d61df) Added option for globalOptions for Auth & other config (@nachiket-p) 129 | - [28fcef1](https://github.com/nachiket-p/rest-redux/commit/28fcef19715dc59c55b3bed500b9c79b3bc92e99) added dummy data in example server (@nachiket-p) 130 | - [b0a062b](https://github.com/nachiket-p/rest-redux/commit/b0a062b1120ac76942e97985829741da2dfb2588) Restructure files (@nachiket-p) 131 | - [8a84477](https://github.com/nachiket-p/rest-redux/commit/8a8447726094931ded2ea52e622288b83736360a) Removed unused method (@nachiket-p) 132 | - [2453f09](https://github.com/nachiket-p/rest-redux/commit/2453f09d569e327ed258c2aa03056d2f5710261c) Implemented Delete reducer & multiple/single response handing (@nachiket-p) 133 | - [bfc9941](https://github.com/nachiket-p/rest-redux/commit/bfc9941778152c11d3340de4ed3a4206ad213837) Using Object spread operator instead of _.merge now (@nachiket-p) 134 | - [11babfc](https://github.com/nachiket-p/rest-redux/commit/11babfc6ddf67af250613a7d8f384f5e0b63704c) Update example to new find method getFound & related changes (@nachiket-p) 135 | - [4024426](https://github.com/nachiket-p/rest-redux/commit/4024426de654a8ea052f5d2bf4633f51da46d305) Implemented custom method example (@nachiket-p) 136 | - [bbc75e3](https://github.com/nachiket-p/rest-redux/commit/bbc75e3ed5fec4fbb717947da8404a566feccf32) Implemented custom API call code (@nachiket-p) 137 | - [4c8d4b6](https://github.com/nachiket-p/rest-redux/commit/4c8d4b6b84163a252534c59fd1efb4a2fb6c89ad) Renamed to DELETE_BY_ID from DELETE & Renamed to DELETE from DELETE_ALL (@nachiket-p) 138 | - [629be32](https://github.com/nachiket-p/rest-redux/commit/629be32c181579369dc0ae8604db9790ac8ea79f) Added selector EXAMPLE in library (@nachiket-p) 139 | - [83bc0d1](https://github.com/nachiket-p/rest-redux/commit/83bc0d1cdf248336aec0b8546f9a7e06294998c7) Added basic selector in library (@nachiket-p) 140 | - [e291929](https://github.com/nachiket-p/rest-redux/commit/e2919291a25fc1905f7ec1f684046ef08abef2dd) Refatored code & removed reselect use for simplification (@nachiket-p) 141 | - [f305b1a](https://github.com/nachiket-p/rest-redux/commit/f305b1a93f15068772fe8a4987cfe9112f2dd5da) restructured exposing APIs (@nachiket-p) 142 | - [b1aee75](https://github.com/nachiket-p/rest-redux/commit/b1aee751cf9393bfda8849bc0c5ba8850a573b21) updated example with callbacks & count method (@nachiket-p) 143 | - [0433976](https://github.com/nachiket-p/rest-redux/commit/0433976c7ce92bcdcab8f7434b615a81939d7964) Implemented callbacks & count method (@nachiket-p) 144 | - [07fb481](https://github.com/nachiket-p/rest-redux/commit/07fb481fde76cd54fe629fce6aa7bc24a49ced42) Implemented different events approach with RECEIVED for data instances (@nachiket-p) 145 | - [6bb8c65](https://github.com/nachiket-p/rest-redux/commit/6bb8c65a8620f3a809929e9fd031038f729790a7) WIP: readme started (@nachiket-p) 146 | - [577d716](https://github.com/nachiket-p/rest-redux/commit/577d7163ffbf5a31b7840161c7990aac3e80f0bd) Error handler updated to handle all cases (@nachiket-p) 147 | - [b6dbd02](https://github.com/nachiket-p/rest-redux/commit/b6dbd02480d3bd6761db99fd0a879c271974c66a) changed dist to public for gitignore issue (@nachiket-p) 148 | - [6b494da](https://github.com/nachiket-p/rest-redux/commit/6b494da7736531597ff24fea0bc46c629212a200) WIP, more events and selection logic, code refactored (@nachiket-p) 149 | - [4f85073](https://github.com/nachiket-p/rest-redux/commit/4f8507360d5d4f4b182b2fb17cbd5033a517fd57) warning fix (@nachiket-p) 150 | - [131994e](https://github.com/nachiket-p/rest-redux/commit/131994eaa6130b71c197606dc176618446cd66cd) code refactored, some test setup (@nachiket-p) 151 | - [2ae9a5a](https://github.com/nachiket-p/rest-redux/commit/2ae9a5a9cb65cd128335c870185fd650fa46129c) delete method implemented (@nachiket-p) 152 | - [87a000d](https://github.com/nachiket-p/rest-redux/commit/87a000d9436462be9013a101ae0999ea16a7f066) code refactoring, and configure function WIP (@nachiket-p) 153 | - [1502f6d](https://github.com/nachiket-p/rest-redux/commit/1502f6d1c9bb82b465cb467c3cf67cd600270ec7) more WIP implementation separated entity reducers (@nachiket-p) 154 | - [a05e68f](https://github.com/nachiket-p/rest-redux/commit/a05e68fed94fa4045d14c88f5c104596d3e4187a) added test setup, WIP dummy call to server, WIP code structure ideas (@nachiket-p) 155 | - [f3cabb5](https://github.com/nachiket-p/rest-redux/commit/f3cabb57ff5c1295fdbcf0da1a833a081e1fed06) WIP: Adding library API interface in example (@nachiket-p) 156 | - [6e6cb8b](https://github.com/nachiket-p/rest-redux/commit/6e6cb8b021d0bf8e52df3d0612850acc8a49f0d0) WIP: example code setup, example server setup (@nachiket-p) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rest-redux 2 | Provides actions & reducers to communicate with REST API Backend. 3 | 4 | [![Build Status](https://travis-ci.org/nachiket-p/rest-redux.svg?branch=master)](https://travis-ci.org/nachiket-p/rest-redux) 5 | [![Coverage](https://codecov.io/gh/nachiket-p/rest-redux/branch/master/graph/badge.svg)](https://codecov.io/gh/nachiket-p/rest-redux) 6 | [![npm version](https://badge.fury.io/js/rest-redux.svg)](https://badge.fury.io/js/rest-redux) 7 | 8 | #### NOTE: WIP, Under active development. And will support [Loopback](http://loopback.io) API out of the box, as its intended to use on that internally 9 | 10 | ## Summary 11 | rest-redux makes communication with REST API very easy. It reduces lot of boilerplate code. 12 | It manages normalized redux state for apis & provides easy to use actions/selectors. 13 | 14 | ## Installation 15 | Add rest-redux to your package.json dependencies. 16 | 17 | ```npm install rest-redux --save``` 18 | 19 | 20 | ## Code 21 | 22 | ### Setup rest-redux 23 | 24 | ```javascript 25 | import { createStore, combineReducers, applyMiddleware } from 'redux' 26 | import thunk from 'redux-thunk' //rest-redux requires this. 27 | import RestRedux from 'rest-redux' 28 | 29 | const restRedux = new RestRedux({ 30 | basePath: 'http://localhost:3000/api', 31 | globalOptions: { //global options, you can set headers & params 32 | headers: { 33 | 'Accept': 'application/json', 34 | 'Content-Type': 'application/json' 35 | } 36 | }, 37 | models: [{ 38 | modelName: 'todos', 39 | lists: [ //List allow to fetch paging data 40 | {name:'my', options:{pageSize: 5}}, 41 | {name:'public'} 42 | ], 43 | schema: { //Uses normalizr.js (https://github.com/paularmstrong/normalizr) 44 | definition: {}, 45 | options: {} 46 | } 47 | }, { 48 | modelName: 'users' 49 | },] 50 | }) 51 | 52 | let reducer = combineReducers({ 53 | rest: restRedux.reducer, 54 | otherReducers: ... 55 | }) 56 | 57 | //IMPORTANT: thunk middleware is required for rest-redux to function. 58 | //And it should come before restRedux in middleware chain 59 | const middlewares = applyMiddleware( 60 | thunk, 61 | restRedux.middleware 62 | ); 63 | 64 | let store = createStore( 65 | reducer, 66 | middlewares 67 | ) 68 | 69 | // create actions/selectors for each model using following API 70 | export const todo = restRedux.get('todos') 71 | export const user = restRedux.get('users') 72 | 73 | const todoActions = todo.actions 74 | const todoSelectors = todo.selectors 75 | 76 | ``` 77 | 78 | ### Using Actions 79 | **Available methods:** 80 | create, update, updateAll, deleteById, find, findById, 81 | 82 | ```javascript 83 | const todoActions = restRedux.get('todos').actions 84 | 85 | //create Todo 86 | dispatch(todoActions.create({text:'This is new todo'})) 87 | 88 | //update Todo 89 | dispatch(todoActions.update(1, {completed:true})) 90 | 91 | //delete todo 92 | dispatch(todoActions.deleteById(1)) 93 | ``` 94 | 95 | ### Using Selectors 96 | **Available methods:** 97 | getInstances, isLoading, getCount, getFound 98 | 99 | ```javascript 100 | const todoSelectors = restRedux.get('todos').selectors 101 | //get All available instances 102 | todoSelectors.getInstances(state) 103 | 104 | //get All last find result instances 105 | todoSelectors.getFound(state) 106 | 107 | //get Count API result 108 | todoSelectors.getCount(state) 109 | 110 | //get loading state, true when any API is executing on the Model 111 | todoSelectors.isLoading(state) 112 | 113 | ``` 114 | 115 | ### Using Lists (At Concept stage ) 116 | List feature provides easy way to manage multiple find/filter REST requests with paging for any model. 117 | 118 | #### Actions & Selectors 119 | ```javascript 120 | const todos = restRedux.get('todos') 121 | //each list instance has actions & selectors 122 | const myTodosList = todos.lists.my 123 | 124 | dispatch(myTodosList.actions.setOptions({params: {userId: myId} })) 125 | dispatch(myTodosList.actions.page(2)) 126 | 127 | //Get all instances found in this list 128 | myTodosList.selectors.getInstances(state) 129 | 130 | //Returns the current page 131 | myTodosList.selectors.getCurrentPage(state) 132 | 133 | //Returns list of page numbers 134 | myTodosList.selectors.getPages(state) 135 | 136 | //Returns total number of pages available 137 | myTodosList.selectors.getTotal(state) 138 | 139 | //Returns whether previous page is avaliable or not 140 | myTodosList.selectors.hasPrev(state) 141 | 142 | //Returns whether next page is avaliable or not 143 | myTodosList.selectors.hasNext(state) 144 | ``` 145 | 146 | #### List HOC (Higher Order Component) 147 | ```javascript 148 | import listHoc from 'rest-redux' 149 | 150 | listHoc('my')(MyTodoView) 151 | const MyTodoView => ({instances, pages, total, hasNext, hasPrev}) { 152 | return
153 | .... 154 |
155 | } 156 | ``` 157 | 158 | 159 | ### Running Example 160 | It works directly with src folder (using Webpack alias). 161 | you need to do npm install in /, /example & /example/server before starting 162 | 163 | #### Start Backend Server (with in memory DB & dummy Data) 164 | ```bash 165 | cd example/server 166 | npm start 167 | ``` 168 | 169 | #### Start Front end App Development Server 170 | ```bash 171 | cd example 172 | npm start 173 | ``` 174 | 175 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | ### Examples 2 | 3 | #### Minimal 4 | Minimal configuration example for getting basics of rest-redux 5 | 6 | #### Minimal-Features 7 | Minimal App code with many features of rest-redux 8 | 9 | #### Complete 10 | This example covers all the features & functions of rest-redux 11 | 12 | ### SERVER 13 | Contains common server code, which serves API. You need to run this to run any of the above example. 14 | 15 | -------------------------------------------------------------------------------- /examples/complete/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["es2015", { "loose" : true, "modules": false }], "react", "stage-0"] 3 | } -------------------------------------------------------------------------------- /examples/complete/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rest-redux-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --progress --colors --config ./webpack.config.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "babel": { 14 | "presets": [ 15 | "es2015", 16 | "react", 17 | "stage-2" 18 | ] 19 | }, 20 | "devDependencies": { 21 | "babel-core": "^6.23.1", 22 | "babel-loader": "^6.3.2", 23 | "babel-preset-es2015": "^6.22.0", 24 | "babel-preset-react": "^6.23.0", 25 | "babel-preset-stage-0": "^6.24.1", 26 | "babel-preset-stage-2": "^6.22.0", 27 | "css-loader": "^0.28.4", 28 | "react-hot-loader": "3.0.0-beta.6", 29 | "style-loader": "^0.18.2", 30 | "webpack": "^2.2.1", 31 | "webpack-dev-server": "^2.4.1" 32 | }, 33 | "dependencies": { 34 | "purecss": "^1.0.0", 35 | "react": "^15.4.2", 36 | "react-dom": "^15.4.2", 37 | "react-redux": "^5.0.5", 38 | "redux": "^3.6.0", 39 | "redux-thunk": "^2.2.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/complete/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Rest Redux Demo 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/complete/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Provider } from 'react-redux' 3 | import 'purecss/build/pure.css' 4 | 5 | import Footer from './todos/components/Footer' 6 | import AddTodo from './todos/containers/AddTodo' 7 | import VisibleTodoList from './todos/containers/VisibleTodoList' 8 | import IncompleteTodos from './todos/containers/IncompleteTodos' 9 | import CompletedTodos from './todos/containers/CompletedTodos' 10 | 11 | import Login from './user/containers/Login' 12 | 13 | export default ({ store }) => { 14 | 15 | const App = () => { 16 | return
17 |

TODOS

18 | 19 |
25 | } 26 | 27 | return 28 | 29 | 30 | } -------------------------------------------------------------------------------- /examples/complete/src/api.js: -------------------------------------------------------------------------------- 1 | import RestRedux from 'rest-redux'; 2 | 3 | const restRedux = new RestRedux({ 4 | basePath: 'http://localhost:3000/api', 5 | globalOptions: { 6 | headers: { 7 | 'Accept': 'application/json', 8 | 'Content-Type': 'application/json' 9 | } 10 | }, 11 | models: [{ 12 | modelName: 'todos', 13 | lists: [ 14 | {name:'personal', options:{pageSize: 5}}, 15 | {name:'incomplete', options: { 16 | pageSize: 3, 17 | params: { 18 | where:{completed:false} 19 | } 20 | }}, 21 | {name:'completed', options: { 22 | pageSize: 3, 23 | params: { 24 | where:{completed:true} 25 | } 26 | }}, 27 | ], 28 | schema: { //Uses normalizr.js (https://github.com/paularmstrong/normalizr) 29 | definition: {}, 30 | options: {} 31 | } 32 | }, { 33 | modelName: 'users' 34 | },] 35 | }) 36 | 37 | export default restRedux 38 | //TODO: Should I use action instead?? 39 | //TODO: Implement with Login 40 | 41 | export const restReduxReducer = restRedux.reducer 42 | export const restReduxMiddleware = restRedux.middleware 43 | 44 | export const todo = restRedux.get('todos') 45 | export const user = restRedux.get('users') 46 | 47 | export const completedTodos = todo.lists.completed 48 | export const incompleteTodos = todo.lists.incomplete 49 | 50 | 51 | // actions.next() //page 52 | // actions.prev() 53 | // actions.first() 54 | // actions.last() 55 | 56 | // selectors.getInstances() 57 | // selectors.getTotal() 58 | // selectors.getPages() 59 | // selectors.isFirst() 60 | // selectors.isLast() 61 | 62 | //myTodosList.setOptions({pageSize, startPage}) 63 | 64 | //HOC: 65 | // list(name, filter) => {instances, pages, total, isFirst, isLast} 66 | -------------------------------------------------------------------------------- /examples/complete/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { AppContainer } from 'react-hot-loader' 4 | import App from './App'; 5 | import store from './store' 6 | 7 | const render = Component => { 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById('root') 13 | ) 14 | } 15 | 16 | render(App) 17 | 18 | if (module.hot) { 19 | module.hot.accept('./App', () => { render(App) }) 20 | } -------------------------------------------------------------------------------- /examples/complete/src/middlewares.js: -------------------------------------------------------------------------------- 1 | import { LOGIN_SUCCESS, LOGOUT_SUCCESS } from './user/actions' 2 | import restRedux, { todo, completedTodos, incompleteTodos } from './api' 3 | 4 | export const authEventsMiddleware = store => next => action => { 5 | switch (action.type) { 6 | case LOGIN_SUCCESS: 7 | restRedux.updateGlobal({ 8 | headers: { 9 | 'Authorization': action.payload.token.id 10 | } 11 | }) 12 | store.dispatch(todo.actions().find({})) 13 | store.dispatch(completedTodos.actions().refresh()) 14 | store.dispatch(incompleteTodos.actions().refresh()) 15 | break 16 | case LOGOUT_SUCCESS: 17 | restRedux.updateGlobal({headers: { 18 | 'Authorization': null 19 | }}) 20 | restRedux.clear(store.dispatch) 21 | break 22 | default: 23 | 24 | } 25 | next(action) 26 | }; -------------------------------------------------------------------------------- /examples/complete/src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers, applyMiddleware } from 'redux' 2 | import todoReducer from './todos/reducer' 3 | import userReducer from './user/reducer' 4 | import {authEventsMiddleware} from './middlewares' 5 | import {restReduxReducer, restReduxMiddleware} from './api'; 6 | import thunk from 'redux-thunk' 7 | let reducer = combineReducers({ 8 | todos: todoReducer, 9 | user: userReducer, 10 | rest: restReduxReducer 11 | }) 12 | 13 | const middlewares = applyMiddleware( 14 | thunk, 15 | restReduxMiddleware, 16 | authEventsMiddleware 17 | ); 18 | 19 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 20 | 21 | let store = createStore( 22 | reducer, 23 | composeEnhancers(middlewares) 24 | ) 25 | export default store -------------------------------------------------------------------------------- /examples/complete/src/todos/actions.js: -------------------------------------------------------------------------------- 1 | import { todo } from '../api' 2 | 3 | const todoActions = todo.actions() 4 | export const ADD_TODO = 'ADD_TODO' 5 | export const TOGGLE_TODO = 'TOGGLE_TODO' 6 | export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER' 7 | 8 | export const VisibilityFilters = { 9 | SHOW_ALL: 'SHOW_ALL', 10 | SHOW_COMPLETED: 'SHOW_COMPLETED', 11 | SHOW_ACTIVE: 'SHOW_ACTIVE' 12 | } 13 | 14 | export function setVisibilityFilter(filter) { 15 | return { type: SET_VISIBILITY_FILTER, filter } 16 | } 17 | 18 | export const ActionLinks = { 19 | //DELETE_ALL: () => (dispatch) => dispatch(todoActions.delete({})), 20 | DELETE_ALL: () => (dispatch) => dispatch(todoActions.custom('DELETE_ALL', 'deleteTodos', 'POST')).then(response => { 21 | dispatch(todoActions.find({})) 22 | dispatch(todoActions.count({})) 23 | }), 24 | COMPLETE_ALL: () => (dispatch) => { 25 | dispatch(todoActions.updateAll({ completed: false }, { completed: true })).then(response => { 26 | dispatch(todoActions.find({})) 27 | }) 28 | }, 29 | UNCOMPLETE_ALL: () => (dispatch) => { 30 | dispatch(todoActions.updateAll({ completed: true }, { completed: false })).then(response => { 31 | dispatch(todoActions.find({})) 32 | }) 33 | } 34 | } -------------------------------------------------------------------------------- /examples/complete/src/todos/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import FilterLink from '../containers/FilterLink' 3 | import ActionLink from '../containers/ActionLink' 4 | 5 | const Footer = () => ( 6 |
7 |

8 | Show: 9 | {" "} 10 | 11 | All 12 | 13 | {", "} 14 | 15 | Active 16 | 17 | {", "} 18 | 19 | Completed 20 | 21 |

22 | 23 |

24 | Actions: 25 | {" "} 26 | 27 | Delete All 28 | 29 | {", "} 30 | 31 | Mark All Complete 32 | 33 | {", "} 34 | 35 | Mark All Uncomplete 36 | 37 |

38 |
39 | ) 40 | 41 | export default Footer -------------------------------------------------------------------------------- /examples/complete/src/todos/components/Link.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const Link = ({ active, children, onClick }) => { 5 | if (active) { 6 | return {children} 7 | } 8 | 9 | return ( 10 | { 12 | e.preventDefault() 13 | onClick() 14 | }} 15 | > 16 | {children} 17 | 18 | ) 19 | } 20 | 21 | Link.propTypes = { 22 | children: PropTypes.node.isRequired, 23 | onClick: PropTypes.func.isRequired 24 | } 25 | 26 | export default Link -------------------------------------------------------------------------------- /examples/complete/src/todos/components/PagingTodos.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Todo from './Todo' 4 | 5 | const DemoList = ({title, instances, pages, total, hasNext, hasPrev, 6 | gotoPage, next, prev, first, last, refresh }) => { 7 | const loadingEl = null;//loading ? Loading ... : null 8 | console.log('rendering demolist', instances, pages, total) 9 | return ( 10 |
11 |

{title}

12 |
    13 | {instances.map(todo => { 14 | return {}} 18 | onDelete={() => {}} 19 | /> 20 | } 21 | )} 22 |
23 | {loadingEl} 24 |

Total: {total}

25 |

26 | refresh()}> Refresh 27 |

28 |

29 | hasPrev && first()}> First 30 | hasPrev && prev()}> {hasPrev?'prev':'no-prev'} Prev 31 | {pages.map((page) => gotoPage(page)} key={page}> {page} )} 32 | hasNext && next()}> {hasNext?'next':'no-next'} Next 33 | hasNext && last()}> Last 34 |

35 |
36 | ) 37 | } 38 | 39 | 40 | export default DemoList -------------------------------------------------------------------------------- /examples/complete/src/todos/components/Todo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const Todo = ({ onClick, completed, text, onDelete }) => ( 5 |
  • 11 | {text} 12 | x 13 |
  • 14 | ) 15 | 16 | Todo.propTypes = { 17 | onClick: PropTypes.func.isRequired, 18 | completed: PropTypes.bool.isRequired, 19 | text: PropTypes.string.isRequired 20 | } 21 | 22 | export default Todo -------------------------------------------------------------------------------- /examples/complete/src/todos/components/TodoList.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Todo from './Todo' 4 | 5 | const TodoList = ({ todos, todosCount, loading, onTodoClick, deleteTodo }) => { 6 | const loadingEl = loading ? Loading ... : null 7 | return ( 8 |
    9 |
      10 | {todos.map(todo => 11 | onTodoClick(todo)} 15 | onDelete={() => deleteTodo(todo)} 16 | /> 17 | )} 18 |
    19 | {loadingEl} 20 |

    Total: {todosCount}

    21 |
    22 | ) 23 | } 24 | 25 | TodoList.propTypes = { 26 | todos: PropTypes.arrayOf(PropTypes.shape({ 27 | id: PropTypes.string.isRequired, 28 | completed: PropTypes.bool.isRequired, 29 | text: PropTypes.string.isRequired 30 | }).isRequired).isRequired, 31 | onTodoClick: PropTypes.func.isRequired 32 | } 33 | 34 | export default TodoList -------------------------------------------------------------------------------- /examples/complete/src/todos/containers/ActionLink.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import {ActionLinks} from '../actions' 3 | import Link from '../components/Link' 4 | 5 | const mapStateToProps = (state, ownProps) => { 6 | return {} 7 | } 8 | 9 | const mapDispatchToProps = (dispatch, ownProps) => { 10 | return { 11 | onClick: () => { 12 | dispatch(ActionLinks[ownProps.action](ownProps.filter)) 13 | } 14 | } 15 | } 16 | 17 | const ActionLink = connect( 18 | mapStateToProps, 19 | mapDispatchToProps 20 | )(Link) 21 | 22 | export default ActionLink -------------------------------------------------------------------------------- /examples/complete/src/todos/containers/AddTodo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { addTodo } from '../actions' 4 | import { todo } from '../../api' 5 | 6 | const todoActions = todo.actions() 7 | console.log( todoActions) 8 | let AddTodo = ({ dispatch }) => { 9 | let input 10 | 11 | return ( 12 |
    13 |
    { 14 | e.preventDefault() 15 | if (!input.value.trim()) { 16 | return 17 | } 18 | dispatch(todoActions.create({text:input.value})).then(response => { 19 | dispatch(todoActions.find()) 20 | dispatch(todoActions.count()) 21 | }) 22 | input.value = '' 23 | }}> 24 | { 25 | input = node 26 | }} /> 27 | 30 |
    31 |
    32 | ) 33 | } 34 | AddTodo = connect()(AddTodo) 35 | 36 | export default AddTodo -------------------------------------------------------------------------------- /examples/complete/src/todos/containers/CompletedTodos.js: -------------------------------------------------------------------------------- 1 | import { paging } from 'rest-redux' 2 | import PagingTodos from '../components/PagingTodos' 3 | import { completedTodos } from '../../api' 4 | 5 | export default paging(completedTodos, {}, { 6 | title: 'Completed Todos' 7 | })(PagingTodos) -------------------------------------------------------------------------------- /examples/complete/src/todos/containers/FilterLink.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { setVisibilityFilter } from '../actions' 3 | import Link from '../components/Link' 4 | 5 | const mapStateToProps = (state, ownProps) => { 6 | return { 7 | active: ownProps.filter === state.visibilityFilter 8 | } 9 | } 10 | 11 | const mapDispatchToProps = (dispatch, ownProps) => { 12 | return { 13 | onClick: () => { 14 | dispatch(setVisibilityFilter(ownProps.filter)) 15 | } 16 | } 17 | } 18 | 19 | const FilterLink = connect( 20 | mapStateToProps, 21 | mapDispatchToProps 22 | )(Link) 23 | 24 | export default FilterLink -------------------------------------------------------------------------------- /examples/complete/src/todos/containers/IncompleteTodos.js: -------------------------------------------------------------------------------- 1 | import { paging } from 'rest-redux' 2 | import PagingTodos from '../components/PagingTodos' 3 | import { incompleteTodos } from '../../api' 4 | 5 | export default paging(incompleteTodos, {}, { 6 | title: 'Incomplete Todos' 7 | })(PagingTodos) -------------------------------------------------------------------------------- /examples/complete/src/todos/containers/VisibleTodoList.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import TodoList from '../components/TodoList' 3 | import {connectModel} from 'rest-redux' 4 | import {getVisibleTodos, isTodosLoading, getTodosCount} from '../selectors' 5 | import { todo } from '../../api' 6 | 7 | const todoActions = todo.actions() 8 | const mapStateToProps = (state) => { 9 | return { 10 | todos: getVisibleTodos(state), 11 | todosCount: getTodosCount(state), 12 | loading: isTodosLoading(state) 13 | } 14 | } 15 | 16 | const mapDispatchToProps = (dispatch) => { 17 | return { 18 | onTodoClick: (todo) => { 19 | dispatch(todoActions.update(todo.id, {completed: !todo.completed})) 20 | }, 21 | deleteTodo: (todo) => { 22 | dispatch(todoActions.deleteById(todo.id)).then(response => { 23 | dispatch(todoActions.find()) 24 | dispatch(todoActions.count()) 25 | }) 26 | } 27 | } 28 | } 29 | 30 | const VisibleTodoList = connect( 31 | mapStateToProps, 32 | mapDispatchToProps 33 | )(TodoList) 34 | 35 | export default connectModel('todos')(VisibleTodoList) -------------------------------------------------------------------------------- /examples/complete/src/todos/reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import {VisibilityFilters, ADD_TODO, SET_VISIBILITY_FILTER, SHOW_ALL, TOGGLE_TODO} from './actions' 3 | 4 | function visibilityFilter(state = VisibilityFilters.SHOW_ALL, action) { 5 | switch (action.type) { 6 | case SET_VISIBILITY_FILTER: 7 | return action.filter 8 | default: 9 | return state 10 | } 11 | } 12 | 13 | export default combineReducers({ 14 | visibilityFilter 15 | }) 16 | -------------------------------------------------------------------------------- /examples/complete/src/todos/selectors.js: -------------------------------------------------------------------------------- 1 | // import { createSelector } from 'reselect' 2 | import _ from 'lodash' 3 | import {todo} from '../api' 4 | const todoSelectors = todo.selectors() 5 | 6 | export const isTodosLoading = todoSelectors.isLoading 7 | export const getTodosCount = todoSelectors.getCount 8 | 9 | export const getFilter = state => state.todos.visibilityFilter 10 | 11 | export const getVisibleTodos = state => { 12 | const todos = todoSelectors.getFound(state) 13 | const filter = getFilter(state) 14 | switch (filter) { 15 | case 'SHOW_ALL': 16 | return todos 17 | case 'SHOW_COMPLETED': 18 | return todos.filter(t => t.completed) 19 | case 'SHOW_ACTIVE': 20 | return todos.filter(t => !t.completed) 21 | } 22 | } 23 | 24 | 25 | // export const getVisibleTodos = createSelector( 26 | // getTodos, getFilter, (todos, filter) => { 27 | // switch (filter) { 28 | // case 'SHOW_ALL': 29 | // return todos 30 | // case 'SHOW_COMPLETED': 31 | // return todos.filter(t => t.completed) 32 | // case 'SHOW_ACTIVE': 33 | // return todos.filter(t => !t.completed) 34 | // } 35 | // } 36 | // ) 37 | 38 | -------------------------------------------------------------------------------- /examples/complete/src/user/actions.js: -------------------------------------------------------------------------------- 1 | import { user } from '../api' 2 | 3 | const userActions = user.actions() 4 | export const LOGIN_SUCCESS = 'USER/LOGIN_SUCCESS' 5 | export const LOGIN_FAILED = 'USER/LOGIN_FAILED' 6 | export const LOGOUT_SUCCESS = 'USER/LOGOUT_SUCCESS' 7 | 8 | export const login = (email, password) => (dispatch) => { 9 | const options = { body: { email, password } } 10 | dispatch(userActions.custom('LOGIN', 'login', 'POST', options)) 11 | .then(response => { 12 | console.log('LOGIN SUCCESS', response) 13 | dispatch(loginSuccess({email}, response)) 14 | }) 15 | .catch(error => { 16 | console.log('LOGIN FAILED', error) 17 | if(error.response.status == 401) { 18 | //TODO: Implement inline / toast message 19 | alert('Please enter correct username & password') 20 | dispatch({type: LOGIN_FAILED, payload: error }) 21 | } 22 | }) 23 | } 24 | 25 | export const loginSuccess = (profile, token) => ({type: LOGIN_SUCCESS, payload: {profile, token}}) 26 | 27 | export const logout = () => ({type: LOGOUT_SUCCESS, payload: {}}) 28 | 29 | -------------------------------------------------------------------------------- /examples/complete/src/user/components/Login.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const LoggedInView = ({profile, logout}) =>

    {profile.email} |

    5 | 6 | export default ({ login, logout, isLoggedIn, profile, error }) => { 7 | let email, password 8 | console.log('login status in component', isLoggedIn) 9 | const errorView = error?

    ERROR: Wrong email or password

    :null 10 | 11 | const loginForm =
    12 |
    { 13 | e.preventDefault() 14 | if (!email.value.trim() || !password.value.trim()) { 15 | return 16 | } 17 | login(email.value, password.value) 18 | email.value = '' 19 | password.value = '' 20 | }}> 21 | { email = node }} /> 22 | { password = node }} /> 23 | 24 |
    25 |
    use john@doe.com / jane@doe.com with password 'gotthemilk'
    26 | {errorView} 27 |
    28 | 29 | return isLoggedIn ? : loginForm 30 | 31 | } -------------------------------------------------------------------------------- /examples/complete/src/user/containers/Login.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | import Login from '../components/Login' 5 | import { login, logout } from '../actions' 6 | import {isLoggedIn, getProfile, getError} from '../selectors' 7 | 8 | 9 | const mapStateToProps = (state) => ({ 10 | isLoggedIn: isLoggedIn(state), 11 | profile: getProfile(state), 12 | error: getError(state) 13 | }) 14 | 15 | const mapDispatchToProps = { 16 | login, 17 | logout 18 | } 19 | 20 | export default connect(mapStateToProps, mapDispatchToProps)(Login) -------------------------------------------------------------------------------- /examples/complete/src/user/reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { LOGIN_SUCCESS, LOGIN_FAILED, LOGOUT_SUCCESS } from './actions' 3 | 4 | const DEFAULT = { 5 | profile: null, 6 | token: null, 7 | error: null 8 | } 9 | function auth(state = DEFAULT, action) { 10 | const {payload} = action 11 | switch (action.type) { 12 | case LOGIN_SUCCESS: 13 | return { 14 | profile: payload.profile, 15 | token: payload.token, 16 | } 17 | case LOGIN_FAILED: 18 | return { ...DEFAULT, error: payload } 19 | case LOGOUT_SUCCESS: 20 | return DEFAULT 21 | default: 22 | return state 23 | } 24 | } 25 | 26 | export default combineReducers({ 27 | auth 28 | }) 29 | -------------------------------------------------------------------------------- /examples/complete/src/user/selectors.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | export const isLoggedIn = state => !!state.user.auth.profile 4 | export const getProfile = state => state.user.auth.profile 5 | export const getError = state => state.user.auth.error 6 | 7 | 8 | 9 | // export const getVisibleTodos = createSelector( 10 | // getTodos, getFilter, (todos, filter) => { 11 | // switch (filter) { 12 | // case 'SHOW_ALL': 13 | // return todos 14 | // case 'SHOW_COMPLETED': 15 | // return todos.filter(t => t.completed) 16 | // case 'SHOW_ACTIVE': 17 | // return todos.filter(t => !t.completed) 18 | // } 19 | // } 20 | // ) 21 | 22 | -------------------------------------------------------------------------------- /examples/complete/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs') 3 | var webpack = require('webpack'); 4 | 5 | module.exports = { 6 | // context: path.join(__dirname, '..') , 7 | entry: [ 8 | 'react-hot-loader/patch', 9 | 'webpack-dev-server/client?http://localhost:8080', 10 | 'webpack/hot/only-dev-server', 11 | './src/index.js' 12 | ], 13 | module: { 14 | loaders: [ 15 | { 16 | test: /\.js?$/, 17 | exclude: /node_modules/, 18 | // loader: 'react-hot-loader!babel-loader' 19 | loaders: ['react-hot-loader/webpack', 'babel-loader'] 20 | }, 21 | { 22 | test: /\.css$/, 23 | use: ['style-loader', 'css-loader'] 24 | } 25 | ] 26 | }, 27 | plugins: [ 28 | new webpack.HotModuleReplacementPlugin(), 29 | // enable HMR globally 30 | 31 | new webpack.NamedModulesPlugin(), 32 | // prints more readable module names in the browser console on HMR updates 33 | 34 | new webpack.NoEmitOnErrorsPlugin(), 35 | // do not emit compiled assets that include errors 36 | ], 37 | resolve: { 38 | extensions: ['*', '.js', '.jsx'], 39 | alias: { 40 | 'rest-redux': path.resolve(__dirname, '..', '..', 'src') 41 | }, 42 | // modules: [ 43 | // path.resolve(__dirname), 44 | // path.resolve(__dirname, '..', 'src') 45 | // ] 46 | }, 47 | output: { 48 | path: __dirname + '/public', 49 | publicPath: '/', 50 | filename: 'bundle.js' 51 | }, 52 | devServer: { 53 | contentBase: './public', 54 | hot: true 55 | }, 56 | devtool: 'eval-source-map' 57 | }; 58 | -------------------------------------------------------------------------------- /examples/minimal-features/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["es2015", { "loose" : true, "modules": false }], "react", "stage-0"] 3 | } -------------------------------------------------------------------------------- /examples/minimal-features/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rest-redux-minimal-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --progress --colors --config ./webpack.config.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "babel": { 14 | "presets": [ 15 | "es2015", 16 | "react", 17 | "stage-2" 18 | ] 19 | }, 20 | "devDependencies": { 21 | "babel-core": "^6.23.1", 22 | "babel-loader": "^6.3.2", 23 | "babel-preset-es2015": "^6.22.0", 24 | "babel-preset-react": "^6.23.0", 25 | "babel-preset-stage-0": "^6.24.1", 26 | "babel-preset-stage-2": "^6.22.0", 27 | "css-loader": "^0.28.4", 28 | "react-hot-loader": "3.0.0-beta.6", 29 | "style-loader": "^0.18.2", 30 | "webpack": "^2.2.1", 31 | "webpack-dev-server": "^2.4.1" 32 | }, 33 | "dependencies": { 34 | "purecss": "^1.0.0", 35 | "react": "^15.4.2", 36 | "react-dom": "^15.4.2", 37 | "react-redux": "^5.0.5", 38 | "redux": "^3.6.0", 39 | "redux-thunk": "^2.2.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/minimal-features/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Rest Redux Demo 8 | 9 | 10 |
    11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/minimal-features/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { createStore, combineReducers, applyMiddleware } from 'redux' 4 | import RestRedux from 'rest-redux'; 5 | import { connect } from 'react-redux' 6 | import thunk from 'redux-thunk'; 7 | 8 | // There are more options, which are important, e.g. schema & lists for models. 9 | const restRedux = new RestRedux({ 10 | basePath: 'http://localhost:3000/api', 11 | globalOptions: { 12 | headers: { 13 | 'Accept': 'application/json', 14 | 'Content-Type': 'application/json' 15 | } 16 | }, 17 | models: [ 18 | { modelName: 'todos', apiPath: '/todos' }, 19 | { modelName: 'todoComments', apiPath: '/todos/{id}/comments' }, 20 | { modelName: 'users' }, 21 | { modelName: 'comments'} 22 | ] 23 | }) 24 | 25 | let reducer = combineReducers({ 26 | rest: restRedux.reducer 27 | }) 28 | 29 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 30 | 31 | let store = createStore( 32 | reducer, 33 | composeEnhancers(applyMiddleware( 34 | thunk, 35 | restRedux.middleware 36 | )) 37 | ) 38 | const todos = restRedux.get('todos') 39 | const todoComments = restRedux.get('todoComments') 40 | const users = restRedux.get('users') 41 | 42 | //Stateless View Component 43 | const ListComponent = ({ todos, todoComments }) =>
    44 |

    Todos

    45 | 50 | 51 |

    Comments of a First Todo

    52 | 55 |
    56 | 57 | //First TODO 58 | const TODO_ID = 1; 59 | 60 | //Redux Connect 61 | const App = connect((state) => ({ 62 | todos: todos.selectors().getFound(state), 63 | todoComments: todoComments.selectors({id: TODO_ID}).getFound(state) 64 | }), null)(ListComponent) 65 | 66 | //RENDER APP 67 | ReactDOM.render( 68 | , 69 | document.getElementById('root') 70 | ) 71 | 72 | //Dispatch Custom Login Action on Users 73 | const options = { body: { email: 'john@doe.com', password: 'gotthemilk' } } 74 | store.dispatch(users.actions().custom('LOGIN', 'login', 'POST', options)) 75 | .then(response => { 76 | console.log('LOGIN SUCCESS', response) 77 | //Apply Headers with rest-redux 78 | restRedux.updateGlobal({ 79 | headers: { 80 | 'Authorization': response.id 81 | } 82 | }) 83 | //Dispatch Fetch Action 84 | store.dispatch(todos.actions().find({})) 85 | //Returns todos of URL with TODO_ID 86 | store.dispatch(todoComments.actions({id: TODO_ID}).find({})) 87 | }) 88 | -------------------------------------------------------------------------------- /examples/minimal-features/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs') 3 | var webpack = require('webpack'); 4 | 5 | module.exports = { 6 | // context: path.join(__dirname, '..') , 7 | entry: [ 8 | 'react-hot-loader/patch', 9 | 'webpack-dev-server/client?http://localhost:8080', 10 | 'webpack/hot/only-dev-server', 11 | './src/index.js' 12 | ], 13 | module: { 14 | loaders: [ 15 | { 16 | test: /\.js?$/, 17 | exclude: /node_modules/, 18 | // loader: 'react-hot-loader!babel-loader' 19 | loaders: ['react-hot-loader/webpack', 'babel-loader'] 20 | }, 21 | { 22 | test: /\.css$/, 23 | use: ['style-loader', 'css-loader'] 24 | } 25 | ] 26 | }, 27 | plugins: [ 28 | new webpack.HotModuleReplacementPlugin(), 29 | // enable HMR globally 30 | 31 | new webpack.NamedModulesPlugin(), 32 | // prints more readable module names in the browser console on HMR updates 33 | 34 | new webpack.NoEmitOnErrorsPlugin(), 35 | // do not emit compiled assets that include errors 36 | ], 37 | resolve: { 38 | extensions: ['*', '.js', '.jsx'], 39 | alias: { 40 | 'rest-redux': path.resolve(__dirname, '..', '..', 'src') 41 | }, 42 | // modules: [ 43 | // path.resolve(__dirname), 44 | // path.resolve(__dirname, '..', 'src') 45 | // ] 46 | }, 47 | output: { 48 | path: __dirname + '/public', 49 | publicPath: '/', 50 | filename: 'bundle.js' 51 | }, 52 | devServer: { 53 | contentBase: './public', 54 | hot: true 55 | }, 56 | devtool: 'eval-source-map' 57 | }; 58 | -------------------------------------------------------------------------------- /examples/minimal/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["es2015", { "loose" : true, "modules": false }], "react", "stage-0"] 3 | } -------------------------------------------------------------------------------- /examples/minimal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rest-redux-minimal-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --progress --colors --config ./webpack.config.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "babel": { 14 | "presets": [ 15 | "es2015", 16 | "react", 17 | "stage-2" 18 | ] 19 | }, 20 | "devDependencies": { 21 | "babel-core": "^6.23.1", 22 | "babel-loader": "^6.3.2", 23 | "babel-preset-es2015": "^6.22.0", 24 | "babel-preset-react": "^6.23.0", 25 | "babel-preset-stage-0": "^6.24.1", 26 | "babel-preset-stage-2": "^6.22.0", 27 | "css-loader": "^0.28.4", 28 | "react-hot-loader": "3.0.0-beta.6", 29 | "style-loader": "^0.18.2", 30 | "webpack": "^2.2.1", 31 | "webpack-dev-server": "^2.4.1" 32 | }, 33 | "dependencies": { 34 | "purecss": "^1.0.0", 35 | "react": "^15.4.2", 36 | "react-dom": "^15.4.2", 37 | "react-redux": "^5.0.5", 38 | "redux": "^3.6.0", 39 | "redux-thunk": "^2.2.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/minimal/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Rest Redux Demo 8 | 9 | 10 |
    11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/minimal/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { createStore, combineReducers, applyMiddleware } from 'redux' 4 | import RestRedux from 'rest-redux'; 5 | import { connect } from 'react-redux' 6 | import thunk from 'redux-thunk'; 7 | 8 | // There are more options, which are important, e.g. schema & lists for models. 9 | const restRedux = new RestRedux({ 10 | basePath: 'http://localhost:3000/api', 11 | globalOptions: { 12 | headers: { 13 | 'Accept': 'application/json', 14 | 'Content-Type': 'application/json' 15 | } 16 | }, 17 | models: [{ modelName: 'todos' }, { modelName: 'users' }] 18 | }) 19 | 20 | let reducer = combineReducers({ 21 | rest: restRedux.reducer 22 | }) 23 | 24 | let store = createStore( 25 | reducer, 26 | applyMiddleware( 27 | thunk, 28 | restRedux.middleware 29 | ) 30 | ) 31 | const todos = restRedux.get('todos') 32 | const users = restRedux.get('users') 33 | 34 | //Stateless View Component 35 | const ListComponent = ({todos}) =>
    36 |

    Todos

    37 |
      38 | {todos.map(todo =>
    • {todo.text}
    • )} 41 |
    42 |
    43 | 44 | //Redux Connect 45 | const App = connect((state) => ({ 46 | todos: todos.selectors().getFound(state) 47 | }), null)(ListComponent) 48 | 49 | //RENDER APP 50 | ReactDOM.render( 51 | , 52 | document.getElementById('root') 53 | ) 54 | 55 | //Dispatch Custom Login Action on Users 56 | const options = { body: { email: 'john@doe.com', password: 'gotthemilk' } } 57 | store.dispatch(users.actions().custom('LOGIN', 'login', 'POST', options)) 58 | .then(response => { 59 | console.log('LOGIN SUCCESS', response) 60 | //Apply Headers with rest-redux 61 | restRedux.updateGlobal({ 62 | headers: { 63 | 'Authorization': response.id 64 | } 65 | }) 66 | //Dispatch Fetch Action 67 | store.dispatch(todos.actions().find({})) 68 | }) -------------------------------------------------------------------------------- /examples/minimal/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs') 3 | var webpack = require('webpack'); 4 | 5 | module.exports = { 6 | // context: path.join(__dirname, '..') , 7 | entry: [ 8 | 'react-hot-loader/patch', 9 | 'webpack-dev-server/client?http://localhost:8080', 10 | 'webpack/hot/only-dev-server', 11 | './src/index.js' 12 | ], 13 | module: { 14 | loaders: [ 15 | { 16 | test: /\.js?$/, 17 | exclude: /node_modules/, 18 | // loader: 'react-hot-loader!babel-loader' 19 | loaders: ['react-hot-loader/webpack', 'babel-loader'] 20 | }, 21 | { 22 | test: /\.css$/, 23 | use: ['style-loader', 'css-loader'] 24 | } 25 | ] 26 | }, 27 | plugins: [ 28 | new webpack.HotModuleReplacementPlugin(), 29 | // enable HMR globally 30 | 31 | new webpack.NamedModulesPlugin(), 32 | // prints more readable module names in the browser console on HMR updates 33 | 34 | new webpack.NoEmitOnErrorsPlugin(), 35 | // do not emit compiled assets that include errors 36 | ], 37 | resolve: { 38 | extensions: ['*', '.js', '.jsx'], 39 | alias: { 40 | 'rest-redux': path.resolve(__dirname, '..', '..', 'src') 41 | }, 42 | // modules: [ 43 | // path.resolve(__dirname), 44 | // path.resolve(__dirname, '..', 'src') 45 | // ] 46 | }, 47 | output: { 48 | path: __dirname + '/public', 49 | publicPath: '/', 50 | filename: 'bundle.js' 51 | }, 52 | devServer: { 53 | contentBase: './public', 54 | hot: true 55 | }, 56 | devtool: 'eval-source-map' 57 | }; 58 | -------------------------------------------------------------------------------- /examples/server/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | indent_style = space 9 | indent_size = 2 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | -------------------------------------------------------------------------------- /examples/server/.eslintignore: -------------------------------------------------------------------------------- 1 | /client/ -------------------------------------------------------------------------------- /examples/server/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "loopback" 3 | } -------------------------------------------------------------------------------- /examples/server/.gitignore: -------------------------------------------------------------------------------- 1 | *.csv 2 | *.dat 3 | *.iml 4 | *.log 5 | *.out 6 | *.pid 7 | *.seed 8 | *.sublime-* 9 | *.swo 10 | *.swp 11 | *.tgz 12 | *.xml 13 | .DS_Store 14 | .idea 15 | .project 16 | .strong-pm 17 | coverage 18 | node_modules 19 | npm-debug.log 20 | -------------------------------------------------------------------------------- /examples/server/.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-loopback": {} 3 | } -------------------------------------------------------------------------------- /examples/server/README.md: -------------------------------------------------------------------------------- 1 | # My Application 2 | 3 | The project is generated by [LoopBack](http://loopback.io). -------------------------------------------------------------------------------- /examples/server/client/README.md: -------------------------------------------------------------------------------- 1 | ## Client 2 | 3 | This is the place for your application front-end files. 4 | -------------------------------------------------------------------------------- /examples/server/common/models/comment.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Comment", 3 | "base": "PersistedModel", 4 | "idInjection": true, 5 | "options": { 6 | "validateUpsert": true 7 | }, 8 | "properties": { 9 | "text": { 10 | "type": "string", 11 | "required": true 12 | }, 13 | "todoId": { 14 | "type": "string", 15 | "required": true 16 | }, 17 | "addedBy": { 18 | "type": "string", 19 | "required": true 20 | } 21 | }, 22 | "validations": [], 23 | "relations": { 24 | "todo": { 25 | "type": "belongsTo", 26 | "model": "Todo", 27 | "foreignKey": "todoId" 28 | } 29 | }, 30 | "acls": [ 31 | { 32 | "accessType": "*", 33 | "principalType": "ROLE", 34 | "principalId": "$everyone", 35 | "permission": "DENY" 36 | }, 37 | { 38 | "accessType": "*", 39 | "principalType": "ROLE", 40 | "principalId": "$authenticated", 41 | "permission": "ALLOW" 42 | }, 43 | { 44 | "accessType": "EXECUTE", 45 | "principalType": "ROLE", 46 | "principalId": "$authenticated", 47 | "permission": "ALLOW", 48 | "property": [ 49 | "deleteTodos" 50 | ] 51 | } 52 | ], 53 | "methods": { 54 | "deleteTodos": { 55 | "accepts": { "arg": "where", "type": "Object", "required": false }, 56 | "http": { "path": "/deleteTodos", "verb": "post" }, 57 | "returns": { "arg": "count", "type": "number" } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /examples/server/common/models/todo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (Todo) { 4 | Todo.deleteTodos = function(where, cb) { 5 | Todo.destroyAll(where).then(result => cb(null, result.count)) 6 | } 7 | } 8 | 9 | 10 | // Todo.remoteMethod( 11 | // 'deleteAll', 12 | // { 13 | // //accepts: { arg: 'id', type: 'number', required: true }, 14 | // http: { path: '/deleteAll', verb: 'post' }, 15 | // returns: { arg: 'count', type: 'number' } 16 | // }); 17 | 18 | -------------------------------------------------------------------------------- /examples/server/common/models/todo.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Todo", 3 | "base": "PersistedModel", 4 | "idInjection": true, 5 | "options": { 6 | "validateUpsert": true 7 | }, 8 | "http": { 9 | "path":"/todos" 10 | }, 11 | "properties": { 12 | "text": { 13 | "type": "string", 14 | "required": true 15 | }, 16 | "completed": { 17 | "type": "boolean", 18 | "default": false 19 | } 20 | }, 21 | "validations": [], 22 | "relations": { 23 | "comments": { 24 | "type": "hasMany", 25 | "model": "Comment", 26 | "foreignKey": "todoId" 27 | } 28 | }, 29 | "acls": [ 30 | { 31 | "accessType": "*", 32 | "principalType": "ROLE", 33 | "principalId": "$everyone", 34 | "permission": "DENY" 35 | }, 36 | { 37 | "accessType": "*", 38 | "principalType": "ROLE", 39 | "principalId": "$authenticated", 40 | "permission": "ALLOW" 41 | }, 42 | { 43 | "accessType": "EXECUTE", 44 | "principalType": "ROLE", 45 | "principalId": "$authenticated", 46 | "permission": "ALLOW", 47 | "property": [ 48 | "deleteTodos" 49 | ] 50 | } 51 | ], 52 | "methods": { 53 | "deleteTodos": { 54 | "accepts": { 55 | "arg": "where", 56 | "type": "Object", 57 | "required": false 58 | }, 59 | "http": { 60 | "path": "/deleteTodos", 61 | "verb": "post" 62 | }, 63 | "returns": { 64 | "arg": "count", 65 | "type": "number" 66 | } 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /examples/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "main": "server/server.js", 5 | "engines": { 6 | "node": ">=4" 7 | }, 8 | "scripts": { 9 | "lint": "eslint .", 10 | "start": "node .", 11 | "posttest": "npm run lint && nsp check" 12 | }, 13 | "dependencies": { 14 | "compression": "^1.0.3", 15 | "cors": "^2.5.2", 16 | "helmet": "^1.3.0", 17 | "loopback-boot": "^2.6.5", 18 | "serve-favicon": "^2.0.1", 19 | "strong-error-handler": "^2.0.0", 20 | "loopback-component-explorer": "^4.0.0", 21 | "loopback": "^3.0.0" 22 | }, 23 | "devDependencies": { 24 | "eslint": "^3.17.1", 25 | "eslint-config-loopback": "^8.0.0", 26 | "nsp": "^2.1.0" 27 | }, 28 | "repository": { 29 | "type": "", 30 | "url": "" 31 | }, 32 | "license": "UNLICENSED", 33 | "description": "server" 34 | } 35 | -------------------------------------------------------------------------------- /examples/server/server/boot/authentication.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function enableAuthentication(server) { 4 | // enable authentication 5 | server.enableAuth(); 6 | }; 7 | -------------------------------------------------------------------------------- /examples/server/server/boot/dummy-data.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | module.exports = function (app) { 5 | //d `1ata sources 6 | var ds = app.dataSources['db']; 7 | 8 | const createRoles = (cb) => { 9 | cb(null, []) 10 | } 11 | 12 | const createUsers = (roles, cb) => { 13 | ds.automigrate('User', function (err) { 14 | if (err) return cb(err); 15 | var User = app.models.User; 16 | User.create([ 17 | { username: 'John', email: 'john@doe.com', password: 'gotthemilk' }, 18 | { username: 'Jane', email: 'jane@doe.com', password: 'gotthemilk' }, 19 | ], cb); 20 | }); 21 | } 22 | // {"email": "john@doe.com", "password": "gotthemilk" } 23 | const createTodos = (users, cb) => { 24 | ds.automigrate('Todo', function (err) { 25 | if (err) return cb(err); 26 | var Todo = app.models.Todo; 27 | Todo.create([ 28 | { text: 'Remember the milk', completed: false }, 29 | { text: 'Reminder to remember the milk', completed: false }, 30 | { text: 'Visualize milk as beer', completed: true }, 31 | { text: 'Don\'t forget the milk at the store', completed: false }, 32 | ], cb); 33 | }); 34 | } 35 | 36 | const createComments = (users, todos, cb) => { 37 | ds.automigrate('Comment', function (err) { 38 | if (err) return cb(err); 39 | var Comment = app.models.Comment; 40 | Comment.create([ 41 | { text: 'How are you gonna do this?', todoId: todos[0].id, addedBy: users[0].id }, 42 | { text: 'Fear is a great motivator :)', todoId: todos[0].id, addedBy: users[1].id }, 43 | { text: 'Oh, good strategy.', todoId: todos[0].id, addedBy: users[0].id } 44 | ], cb); 45 | }); 46 | } 47 | 48 | createRoles((err, roles) => 49 | createUsers(roles, (err,users) => 50 | createTodos(roles, (err,todos) => 51 | createComments(users, todos, (err,comments) => console.log('SEED DATA INIT DONE')) 52 | ) 53 | )) 54 | } 55 | -------------------------------------------------------------------------------- /examples/server/server/boot/root.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(server) { 4 | // Install a `/` route that returns server status 5 | var router = server.loopback.Router(); 6 | router.get('/', server.loopback.status()); 7 | server.use(router); 8 | }; 9 | -------------------------------------------------------------------------------- /examples/server/server/component-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "loopback-component-explorer": { 3 | "mountPath": "/explorer" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/server/server/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "restApiRoot": "/api", 3 | "host": "0.0.0.0", 4 | "port": 3000, 5 | "remoting": { 6 | "context": false, 7 | "rest": { 8 | "handleErrors": false, 9 | "normalizeHttpPath": false, 10 | "xml": false 11 | }, 12 | "json": { 13 | "strict": false, 14 | "limit": "100kb" 15 | }, 16 | "urlencoded": { 17 | "extended": true, 18 | "limit": "100kb" 19 | }, 20 | "cors": false 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/server/server/datasources.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": { 3 | "name": "db", 4 | "connector": "memory" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/server/server/middleware.development.json: -------------------------------------------------------------------------------- 1 | { 2 | "final:after": { 3 | "strong-error-handler": { 4 | "params": { 5 | "debug": true, 6 | "log": true 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/server/server/middleware.json: -------------------------------------------------------------------------------- 1 | { 2 | "initial:before": { 3 | "loopback#favicon": {} 4 | }, 5 | "initial": { 6 | "compression": {}, 7 | "cors": { 8 | "params": { 9 | "origin": true, 10 | "credentials": true, 11 | "maxAge": 86400 12 | } 13 | }, 14 | "helmet#xssFilter": {}, 15 | "helmet#frameguard": { 16 | "params": [ 17 | "deny" 18 | ] 19 | }, 20 | "helmet#hsts": { 21 | "params": { 22 | "maxAge": 0, 23 | "includeSubdomains": true 24 | } 25 | }, 26 | "helmet#hidePoweredBy": {}, 27 | "helmet#ieNoOpen": {}, 28 | "helmet#noSniff": {}, 29 | "helmet#noCache": { 30 | "enabled": false 31 | } 32 | }, 33 | "session": {}, 34 | "auth": {}, 35 | "parse": {}, 36 | "routes": { 37 | "loopback#rest": { 38 | "paths": [ 39 | "${restApiRoot}" 40 | ] 41 | } 42 | }, 43 | "files": {}, 44 | "final": { 45 | "loopback#urlNotFound": {} 46 | }, 47 | "final:after": { 48 | "strong-error-handler": {} 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/server/server/model-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "sources": [ 4 | "loopback/common/models", 5 | "loopback/server/models", 6 | "../common/models", 7 | "./models" 8 | ], 9 | "mixins": [ 10 | "loopback/common/mixins", 11 | "loopback/server/mixins", 12 | "../common/mixins", 13 | "./mixins" 14 | ] 15 | }, 16 | "User": { 17 | "dataSource": "db" 18 | }, 19 | "AccessToken": { 20 | "dataSource": "db", 21 | "public": false 22 | }, 23 | "ACL": { 24 | "dataSource": "db", 25 | "public": false 26 | }, 27 | "RoleMapping": { 28 | "dataSource": "db", 29 | "public": false, 30 | "options": { 31 | "strictObjectIDCoercion": true 32 | } 33 | }, 34 | "Role": { 35 | "dataSource": "db", 36 | "public": false 37 | }, 38 | "Todo": { 39 | "dataSource": "db", 40 | "public": true 41 | }, 42 | "Comment": { 43 | "dataSource": "db", 44 | "public": false 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/server/server/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var loopback = require('loopback'); 4 | var boot = require('loopback-boot'); 5 | 6 | var app = module.exports = loopback(); 7 | 8 | app.start = function() { 9 | // start the web server 10 | return app.listen(function() { 11 | app.emit('started'); 12 | var baseUrl = app.get('url').replace(/\/$/, ''); 13 | console.log('Web server listening at: %s', baseUrl); 14 | if (app.get('loopback-component-explorer')) { 15 | var explorerPath = app.get('loopback-component-explorer').mountPath; 16 | console.log('Browse your REST API at %s%s', baseUrl, explorerPath); 17 | } 18 | }); 19 | }; 20 | 21 | // Bootstrap the application, configure models, datasources and middleware. 22 | // Sub-apps like REST API are mounted via boot scripts. 23 | boot(app, __dirname, function(err) { 24 | if (err) throw err; 25 | 26 | // start the server if `$ node server.js` 27 | if (require.main === module) 28 | app.start(); 29 | }); 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rest-redux", 3 | "version": "0.4.1", 4 | "repository": "https://github.com/nachiket-p/rest-redux", 5 | "description": "Redux support for REST APIs", 6 | "main": "lib/index.js", 7 | "author": "Nachiket Patel", 8 | "license": "MIT", 9 | "scripts": { 10 | "clean": "rimraf lib", 11 | "build": "babel --presets es2015,stage-0 -d lib/ src/", 12 | "prepublish": "npm run clean && npm run build", 13 | "test": "jest", 14 | "test:watch": "npm test -- --watch", 15 | "generate-changelog": "./node_modules/github-changes/bin/index.js -o nachiket-p -r rest-redux -n ${npm_package_version} --use-commit-body", 16 | "preversion": "npm test", 17 | "version": "npm run generate-changelog && git add CHANGELOG.md" 18 | }, 19 | "dependencies": { 20 | "lodash": "^4.17.4", 21 | "normalizr": "^3.2.3", 22 | "prop-types": "^15.5.10", 23 | "query-string": "^4.3.4", 24 | "react": "^15.5.4", 25 | "react-redux": "^5.0.5", 26 | "redux": "^3.7.1", 27 | "whatwg-fetch": "^2.0.3" 28 | }, 29 | "devDependencies": { 30 | "@types/jest": "^19.2.4", 31 | "babel-cli": "^6.24.1", 32 | "babel-core": "^6.23.1", 33 | "babel-jest": "^20.0.3", 34 | "babel-loader": "^6.3.2", 35 | "babel-preset-es2015": "^6.22.0", 36 | "babel-preset-react": "^6.23.0", 37 | "babel-preset-stage-0": "^6.24.1", 38 | "babel-register": "^6.24.1", 39 | "github-changes": "^1.1.0", 40 | "jest": "^20.0.4", 41 | "nock": "^9.0.13", 42 | "redux-mock-store": "^1.2.3", 43 | "redux-testkit": "^1.0.6", 44 | "redux-thunk": "^2.2.0", 45 | "rimraf": "^2.6.1" 46 | }, 47 | "peerDependencies": { 48 | "redux": "^3.7.1", 49 | "redux-thunk": "^2.2.0" 50 | }, 51 | "jest": { 52 | "coverageDirectory": "./coverage/", 53 | "collectCoverage": true 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/adapter/ApiAdapter.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import queryString from 'query-string' 3 | 4 | function handleStatus(response) { 5 | if (response.status >= 200 && response.status < 300) { 6 | return response 7 | } else { 8 | var error = new Error(response.statusText) 9 | error.response = response 10 | throw error 11 | } 12 | } 13 | 14 | const rxOne = /^[\],:{}\s]*$/; 15 | const rxTwo = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g; 16 | const rxThree = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g; 17 | const rxFour = /(?:^|:|,)(?:\s*\[)+/g; 18 | const isJSON = (input) => ( 19 | input.length && rxOne.test( 20 | input.replace(rxTwo, '@') 21 | .replace(rxThree, ']') 22 | .replace(rxFour, '') 23 | ) 24 | ); 25 | 26 | function parse(response) { 27 | return response.text().then(function (text) { 28 | return isJSON(text) ? JSON.parse(text) : {} 29 | }) 30 | } 31 | 32 | //TODO: Add custom fetch support with subclassing 33 | export default class APIAdapter { 34 | constructor(config) { 35 | this.globalOptions = config.globalOptions 36 | } 37 | 38 | fetch(path, method, fetchOptions, handler, errorHandler) { 39 | const finalOptions = _.merge({}, this.globalOptions, fetchOptions) 40 | const { params } = finalOptions 41 | if (params && !_.isEmpty(params)) { 42 | path = `${path}?${queryString.stringify(params)}` 43 | delete fetchOptions.params 44 | } 45 | console.log('calling with options ', finalOptions, fetchOptions) 46 | finalOptions.method = method 47 | return fetch(path, finalOptions) 48 | .then(handleStatus) 49 | .then(parse) 50 | .then(handler) 51 | .catch(errorHandler) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/adapter/RequestAdapter.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | function template(str, data) { 3 | data = data || {}; 4 | 5 | const matcher = str.match(/{(.+?)}/g); 6 | if(!matcher) return str 7 | 8 | matcher.forEach(function(key) { 9 | str = str.replace(key, data[key.replace('{','').replace('}', '')]); 10 | }); 11 | return str; 12 | } 13 | 14 | export default class RequestResolver { 15 | constructor(config) { 16 | this.globalOptions = config.globalOptions 17 | } 18 | 19 | resolveRouteParams(model, routeParams) { 20 | console.log('Resolving Route: ', model, routeParams) 21 | return template(model.apiPath, routeParams) 22 | } 23 | 24 | create(data, requestOptions) { 25 | //{params, body, headers} 26 | return { 27 | url: requestOptions.apiPath, 28 | method: 'POST', 29 | options: { 30 | body: JSON.stringify(data) 31 | } 32 | } 33 | } 34 | 35 | update(id, data, requestOptions) { 36 | return { 37 | url: `${requestOptions.apiPath}/${id}`, 38 | method: 'PATCH', 39 | options: { 40 | body: JSON.stringify(data) 41 | } 42 | } 43 | } 44 | 45 | updateAll(where, data, requestOptions) { 46 | const params = { where: JSON.stringify(where) } 47 | const body = JSON.stringify(data) 48 | 49 | return { 50 | url: `${requestOptions.apiPath}/update`, 51 | method: 'POST', 52 | options: { params, body } 53 | } 54 | } 55 | 56 | find(filter, requestOptions) { 57 | const params = { filter: JSON.stringify(filter) } 58 | return { 59 | url: requestOptions.apiPath, 60 | method: 'GET', 61 | options: { params } 62 | } 63 | } 64 | 65 | findById(id, filter, requestOptions) { 66 | const params = filter ? { filter: JSON.stringify(filter) } : null 67 | return { 68 | url: `${requestOptions.apiPath}/${id}`, 69 | method: 'GET', 70 | options: { params } 71 | } 72 | } 73 | 74 | deleteById(id, requestOptions) { 75 | return { 76 | url: `${requestOptions.apiPath}/${id}`, 77 | method: 'DELETE', 78 | options: { } 79 | } 80 | } 81 | 82 | count(where, requestOptions) { 83 | const params = { where: JSON.stringify(where) } 84 | return { 85 | url: `${requestOptions.apiPath}/count`, 86 | method: 'GET', 87 | options: { params } 88 | } 89 | } 90 | 91 | custom(name, path, method, options = {}, requestOptions) { 92 | const _options = { headers: options.headers } 93 | if (options.params) _options.params = _.mapValues(options.params,(value)=>{ 94 | if(_.isObject(value)) 95 | { 96 | return JSON.stringify(value); 97 | } 98 | return value; 99 | }) 100 | if (options.body) _options.body = JSON.stringify(options.body) 101 | 102 | return { 103 | url: `${requestOptions.apiPath}/${path}`, 104 | method, 105 | options: _options 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | // const REPLACE_OR_CREATE = 'rest-redux/REPLACE_OR_CREATE' 2 | // const PATCH_OR_CREATE = 'rest-redux/PATCH_OR_CREAT' 3 | // const REPLACE_BY_ID = 'rest-redux/REPLACE_BY_ID' 4 | 5 | export const REQUEST = { 6 | CREATE: 'rest-redux/CREATE->request', 7 | FIND_BY_ID: 'rest-redux/FIND_BY_ID->request', 8 | FIND: 'rest-redux/FIND->request', 9 | DELETE_BY_ID: 'rest-redux/DELETE_BY_ID->request', 10 | DELETE: 'rest-redux/DELETE->request', 11 | UPDATE: 'rest-redux/UPDATE->request', 12 | UPDATE_ALL: 'rest-redux/UPDATE_ALL->request', 13 | COUNT: 'rest-redux/COUNT->request', 14 | CUSTOM: 'rest-redux/CUSTOM->request', 15 | } 16 | 17 | export const RESPONSE = { 18 | CREATE: 'rest-redux/CREATE->response', 19 | FIND_BY_ID: 'rest-redux/FIND_BY_ID->response', 20 | FIND: 'rest-redux/FIND->response', 21 | DELETE_BY_ID: 'rest-redux/DELETE_BY_ID->response', 22 | DELETE: 'rest-redux/DELETE->response', 23 | UPDATE: 'rest-redux/UPDATE->response', 24 | UPDATE_ALL: 'rest-redux/UPDATE_ALL->response', 25 | COUNT: 'rest-redux/COUNT->response', 26 | CUSTOM: 'rest-redux/CUSTOM->response', 27 | } 28 | 29 | export const ERROR = 'rest-redux/ERROR' 30 | export const RECEIVED = 'rest-redux/RECEIVED' 31 | export const CLEAR = 'rest-redux/CLEAR' 32 | 33 | export const LIST = { 34 | SET_OPTIONS: 'rest-redux/SET_OPTIONS->list', 35 | PAGE: 'rest-redux/PAGE->list', 36 | NEXT: 'rest-redux/NEXT->list', 37 | PREV: 'rest-redux/PREV->list', 38 | LAST: 'rest-redux/LAST->list', 39 | FIRST: 'rest-redux/FIRST->list', 40 | REFRESH: 'rest-redux/REFRESH->list', 41 | SET_PARAMS:'rest-redux/SET_PARAMS->list' 42 | } 43 | 44 | // export const ACTION = { 45 | // FIND: { 46 | // REQUEST: REQUEST.FIND, 47 | // RESPONSE: RESPONSE.FIND 48 | // }, 49 | // FIND_BY_ID: { 50 | // REQUEST: REQUEST.FIND_BY_ID, 51 | // RESPONSE: RESPONSE.FIND_BY_ID 52 | // }, 53 | // FIND_ONE: { 54 | // REQUEST: REQUEST.FIND_ONE, 55 | // RESPONSE: RESPONSE.FIND_ONE 56 | // }, 57 | // CREATE: { 58 | // REQUEST: REQUEST.CREATE, 59 | // RESPONSE: RESPONSE.CREATE 60 | // }, 61 | // UPDATE: { 62 | // REQUEST: REQUEST.UPDATE, 63 | // RESPONSE: RESPONSE.UPDATE 64 | // }, 65 | // UPDATE_ALL: { 66 | // REQUEST: REQUEST.UPDATE_ALL, 67 | // RESPONSE: RESPONSE.UPDATE_ALL 68 | // }, 69 | // DELETE_BY_ID: { 70 | // REQUEST: REQUEST.DELETE_BY_ID, 71 | // RESPONSE: RESPONSE.DELETE_BY_ID 72 | // }, 73 | // DELETE: { 74 | // REQUEST: REQUEST.DELETE, 75 | // RESPONSE: RESPONSE.DELETE 76 | // }, 77 | // EXIST: { 78 | // REQUEST: REQUEST.EXIST, 79 | // RESPONSE: RESPONSE.EXIST 80 | // }, 81 | // COUNT: { 82 | // REQUEST: REQUEST.COUNT, 83 | // RESPONSE: RESPONSE.COUNT 84 | // }, 85 | // CUSTOM: { 86 | // REQUEST: REQUEST.CUSTOM, 87 | // RESPONSE: RESPONSE.CUSTOM 88 | // } 89 | // } -------------------------------------------------------------------------------- /src/createReducer.js: -------------------------------------------------------------------------------- 1 | import createModelReducer from './model/modelReducer' 2 | import {combineReducers} from 'redux' 3 | 4 | export default (models) => { 5 | const reducers = {}; 6 | models.forEach(model => reducers[model.modelName] = createModelReducer(model) ) 7 | 8 | return combineReducers(reducers) 9 | } -------------------------------------------------------------------------------- /src/hoc/paging.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | const paging = (list, routeParams, props) => (Component) => { 5 | const actions = list.actions(routeParams) 6 | const selectors = list.selectors(routeParams) 7 | 8 | const mapStateToProps = (state) => { 9 | return { 10 | instances: selectors.getInstances(state), 11 | pages: selectors.getPages(state), 12 | currentPage: selectors.getCurrentPage(state), 13 | total: selectors.getTotal(state), 14 | hasNext: selectors.hasNext(state), 15 | hasPrev: selectors.hasPrev(state), 16 | pageSize: selectors.getPageSize(state) 17 | } 18 | } 19 | const mapDispatchToProps = (dispatch) => { 20 | return { 21 | gotoPage: (page) => dispatch(actions.page(page)), 22 | first: () => dispatch(actions.first()), 23 | last: () => dispatch(actions.last()), 24 | next: () => dispatch(actions.next()), 25 | prev: () => dispatch(actions.prev()), 26 | refresh: () => dispatch(actions.refresh()), 27 | setOptions: (options) => dispatch(actions.setOptions(options)), 28 | setParams: (params) => dispatch(actions.setParams(params)) 29 | } 30 | } 31 | 32 | class ListHoc extends React.Component { 33 | render() { 34 | return 35 | // restActions={actions} 36 | } 37 | } 38 | 39 | return connect(mapStateToProps, mapDispatchToProps)(ListHoc) 40 | } 41 | 42 | export default paging -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import _ from 'lodash' 3 | import 'whatwg-fetch' 4 | 5 | import ApiAdapter from './adapter/ApiAdapter' 6 | import RequestAdapter from './adapter/RequestAdapter' 7 | 8 | import _createReducer from './createReducer' 9 | import Model from './model' 10 | import pagingHoc from './hoc/paging' 11 | 12 | export const DEFAULT_CONFIG = { 13 | models: [], 14 | globalOptions: { 15 | params: null, 16 | headers: null, 17 | body: null 18 | }, 19 | rootSelector: (state) => state.rest 20 | } 21 | 22 | 23 | export default class Wrapper { 24 | config 25 | reducer 26 | middleware 27 | constructor(_config) { 28 | this.config = _.merge({}, DEFAULT_CONFIG, _config) 29 | const { models } = this.config 30 | console.log('config set: ', this.config) 31 | this.reducer = _createReducer(models) 32 | this.middleware = store => next => action => next(action) 33 | const apiAdapter = new ApiAdapter(this.config) 34 | const requestAdapter = new RequestAdapter(this.config) 35 | this._models = _.keyBy(_.map(models, model => new Model(model, this.config, apiAdapter, requestAdapter)), 'modelName') 36 | } 37 | 38 | get(modelName) { 39 | return this._models[modelName] 40 | } 41 | 42 | updateGlobal(options) { 43 | _.merge(this.config.globalOptions, options) 44 | } 45 | 46 | clear(dispatch) { 47 | _.each(this._models, model => dispatch(model.actions.clear())) 48 | } 49 | 50 | // createModelWrapper(model) { 51 | // const modelWrapper = 52 | // { 53 | // modelName: model.modelName, 54 | // actions: new ModelActions(model, this.config, this.apiAdapter), 55 | // selectors: modelSelectors(model, this.config.rootSelector), 56 | // createList: name => { 57 | // return { 58 | // name: name, 59 | // actions: new ListActions() 60 | // } 61 | // } 62 | // } 63 | // } 64 | } 65 | 66 | export const connectModel = (model, filter) => (Component) => { 67 | //const actions = createActions(model) 68 | // QUESTION:?? Get filtered action here const instances = actions.find() 69 | return class ModelComponent extends React.Component { 70 | render() { 71 | return 72 | // restActions={actions} 73 | } 74 | } 75 | } 76 | 77 | 78 | export const paging = pagingHoc 79 | -------------------------------------------------------------------------------- /src/list/ListActions.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { LIST } from '../constants' 3 | 4 | const { SET_OPTIONS, PAGE, FIRST, NEXT, LAST, PREV, SET_PARAMS } = LIST 5 | export default class ListActions { 6 | constructor(listName, listSelectors, model, modelActions) { 7 | this.modelActions = modelActions 8 | this.listSelectors = listSelectors 9 | this.listName = listName 10 | this.modelName = model.modelName 11 | } 12 | 13 | setOptions({ headers, params, pageSize, offset }) { 14 | const payload = _.omitBy({ headers, params, pageSize, offset, listName: this.listName, modelName: this.modelName }, _.isNil) 15 | return { type: SET_OPTIONS, payload } 16 | } 17 | 18 | setParams(params) { 19 | const payload = _.omitBy({ ...params, listName: this.listName, modelName: this.modelName }, _.isNil) 20 | return { type: SET_PARAMS, payload } 21 | } 22 | 23 | _page(page, dispatch, state) { 24 | dispatch({ type: PAGE, payload: { page, listName: this.listName, modelName: this.modelName } }) 25 | const listObj = this.listSelectors.getListObj(state) 26 | const { where, include } = listObj.params 27 | const filter = _.omitBy({ where, limit: listObj.pageSize, skip: page * listObj.pageSize, include }, _.isNil) 28 | 29 | dispatch(this.modelActions.find(filter, this.listName)) 30 | dispatch(this.modelActions.count(where, this.listName)) 31 | } 32 | 33 | page(page) { 34 | return (dispatch, getState) => this._page(page, dispatch, getState()) 35 | } 36 | 37 | next() { 38 | return (dispatch, getState) => { 39 | let hasNext = this.listSelectors.hasNext(getState()) 40 | if (hasNext) { 41 | const currentPage = this.listSelectors.getCurrentPage(getState()) 42 | return this._page(currentPage + 1, dispatch, getState()) 43 | } 44 | } 45 | } 46 | 47 | prev() { 48 | return (dispatch, getState) => { 49 | let hasPrev = this.listSelectors.hasPrev(getState()) 50 | if (hasPrev) { 51 | const currentPage = this.listSelectors.getCurrentPage(getState()) 52 | return this._page(currentPage - 1, dispatch, getState()) 53 | } 54 | } 55 | } 56 | 57 | refresh() { 58 | return (dispatch, getState) => { 59 | const currentPage = this.listSelectors.getCurrentPage(getState()) 60 | return this._page(currentPage, dispatch, getState()) 61 | } 62 | } 63 | 64 | first() { 65 | return (dispatch, getState) => { 66 | let hasPrev = this.listSelectors.hasPrev(getState()) 67 | if (hasPrev) { 68 | return this._page(0, dispatch, getState()) 69 | } 70 | } 71 | } 72 | 73 | last() { 74 | return (dispatch, getState) => { 75 | const state = getState() 76 | let hasNext = this.listSelectors.hasNext(state) 77 | if (hasNext) { 78 | const pageSize = this.listSelectors.getListObj(state).pageSize 79 | const total = this.listSelectors.getTotal(state) 80 | return this._page(Math.floor(total / pageSize), dispatch, state) 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/list/index.js: -------------------------------------------------------------------------------- 1 | import ListActions from './ListActions' 2 | import listSelectors from './listSelectors' 3 | 4 | export default class List { 5 | constructor(listName, model, options) { 6 | this.listName = listName 7 | this.selectors = (routeParams) => listSelectors(listName, model.selectors(routeParams)) 8 | this.actions = (routeParams) => new ListActions(listName, this.selectors(routeParams), model, model.actions(routeParams)) 9 | } 10 | } -------------------------------------------------------------------------------- /src/list/listReducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { LIST, RESPONSE, CLEAR } from '../constants' 3 | const { SET_OPTIONS, PAGE, NEXT, PREV, LAST, FIRST, SET_PARAMS } = LIST 4 | const DEFAULT = { 5 | offset: 0, 6 | pageSize: 10, 7 | total: null, 8 | result: [], 9 | headers: {}, 10 | params: {} 11 | } 12 | 13 | export function listReducer(model, list) { 14 | const defaultState = { ...DEFAULT, ...list.options } 15 | const reducer = (state = defaultState, { payload, type }) => { 16 | //REJECT actions without payloads & modelName 17 | if (!payload || !payload.modelName) { 18 | return state 19 | } 20 | 21 | if (payload.modelName !== model.modelName || payload.listName !== list.name) { 22 | if (payload.modelName == model.modelName && type == CLEAR) { 23 | return { ...defaultState } 24 | } 25 | return state 26 | } 27 | 28 | console.log('reducing for ', list.name, payload.listName, payload.modelName, model.modelName); 29 | switch (type) { 30 | case SET_OPTIONS: 31 | return { ...state, ...payload } 32 | case SET_PARAMS: 33 | return { ...state, params: { ...state.params, ...payload } } 34 | case PAGE: 35 | return { ...state, offset: state.pageSize * payload.page } 36 | case RESPONSE.FIND: 37 | return { ...state, result: payload.ids } 38 | case RESPONSE.COUNT: 39 | return { ...state, total: payload.count } 40 | default: 41 | return state 42 | } 43 | } 44 | 45 | return reducer 46 | } 47 | 48 | export function createListReducers(model) { 49 | if (!model.lists) { 50 | return {} 51 | } 52 | const listReducers = {}; 53 | model.lists.forEach(list => listReducers[list.name] = listReducer(model, list)) 54 | return combineReducers(listReducers) 55 | } -------------------------------------------------------------------------------- /src/list/listSelectors.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | export default (list, modelSelectors) => { 4 | 5 | const getListObj = (state) => modelSelectors.getModelObj(state).lists[list] 6 | 7 | const getTotal = (state) => getListObj(state).total 8 | 9 | const getInstances = (state) => { 10 | const result = getListObj(state).result 11 | //modelSelectors.getInstances() won't work, as it returns Array, instead of Object Map 12 | const instances = modelSelectors.getModelObj(state).instances 13 | return _.map(result, id => instances[id]) 14 | } 15 | 16 | const getCurrentPage = (state) => { 17 | const listObj = getListObj(state) 18 | return Math.ceil(listObj.offset/listObj.pageSize) 19 | } 20 | 21 | const getPages = (state) => { 22 | const listObj = getListObj(state) 23 | const pages = Math.ceil(listObj.total/listObj.pageSize) 24 | return [...Array(pages)].map((_, i) => i++) 25 | } 26 | const hasNext = (state) => { 27 | const listObj = getListObj(state) 28 | return (listObj.offset + listObj.pageSize < listObj.total) 29 | } 30 | 31 | const hasPrev = (state) => getListObj(state).offset > 0 32 | 33 | const getPageSize = (state) => { 34 | const listObj = getListObj(state) 35 | return listObj.pageSize 36 | } 37 | 38 | return { 39 | getListObj, getInstances, getTotal, getPages, getCurrentPage, hasNext, hasPrev, getPageSize 40 | } 41 | } -------------------------------------------------------------------------------- /src/model/ModelActions.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { schema, normalize } from 'normalizr'; 3 | import { REQUEST, RESPONSE, ACTION, ERROR, SELECTED, RECEIVED, CLEAR } from '../constants' 4 | 5 | //TODO: Move URL related logic to APIAdapter 6 | export default class ModelActions { 7 | constructor(model, config, routeParams = {}, api, requestAdapter) { 8 | this.model = model 9 | this.entitySchema = new schema.Entity(model.modelName) 10 | this.routeParams = routeParams 11 | this.api = api 12 | this.requestAdapter = requestAdapter 13 | } 14 | 15 | _successHandler(dispatch, creator) { 16 | return (response) => { 17 | console.log('request succeeded with JSON response', response) 18 | const actionResult = creator(response) 19 | const actions = _.isArray(actionResult) ? actionResult : [actionResult] 20 | console.log('firing actions:', actions) 21 | actions.forEach(action => dispatch(action)) 22 | return response 23 | } 24 | } 25 | 26 | _errorHandler(dispatch) { 27 | return (error) => { 28 | console.log('request failed', error) 29 | const dispatchError = (error, message) => dispatch({ type: ERROR, payload: { modelName: this.model.modelName, error, message } }) 30 | if (error.response) { 31 | error.response.json().then((response) => { 32 | dispatchError(response.error, error.message) 33 | }) 34 | } else { 35 | dispatchError(error, error.message) 36 | } 37 | throw error 38 | //return error 39 | } 40 | } 41 | 42 | _call(path, method, fetchOptions, requestCreator, successCreator) { 43 | return dispatch => { 44 | dispatch(requestCreator()) 45 | return this.api.fetch(path, method, fetchOptions, this._successHandler(dispatch, successCreator), this._errorHandler(dispatch)) 46 | } 47 | } 48 | 49 | _createAction(type, others) { 50 | return { type, payload: { ...others, modelName: this.model.modelName } } 51 | } 52 | 53 | _createNormalized(type, singleInstance = false, listName = undefined) { 54 | return (response) => { 55 | let normalized 56 | if (_.isArray(response)) { 57 | normalized = normalize(response, [this.entitySchema]) 58 | } else { 59 | normalized = normalize(response, this.entitySchema) 60 | } 61 | console.log('normalized: ', normalized) 62 | 63 | const actions = _.map(normalized.entities, (entities, modelName) => { 64 | return { type: RECEIVED, payload: { modelName, instances: entities } } 65 | }) 66 | actions.push(this._createAction(type, { [singleInstance ? 'id' : 'ids']: normalized.result, listName })) 67 | return actions; 68 | } 69 | } 70 | 71 | create(data) { 72 | const apiPath = this.requestAdapter.resolveRouteParams(this.model, this.routeParams) 73 | const {url, method, options} = this.requestAdapter.create(data, {apiPath}) 74 | return this._call(url, method, options, 75 | () => this._createAction(REQUEST.CREATE, { data }), 76 | this._createNormalized(RESPONSE.CREATE, true)) 77 | } 78 | 79 | update(id, data) { 80 | const apiPath = this.requestAdapter.resolveRouteParams(this.model, this.routeParams) 81 | const {url, method, options} = this.requestAdapter.update(id, data, {apiPath}) 82 | 83 | return this._call(url, method, options, 84 | () => this._createAction(REQUEST.UPDATE, { id, data }), 85 | this._createNormalized(RESPONSE.UPDATE, true)) 86 | } 87 | 88 | updateAll(where, data) { 89 | const apiPath = this.requestAdapter.resolveRouteParams(this.model, this.routeParams) 90 | const {url, method, options} = this.requestAdapter.updateAll(where, data, {apiPath}) 91 | 92 | return this._call(url, method, options, 93 | () => this._createAction(REQUEST.UPDATE_ALL, { where, data }), 94 | (response) => this._createAction(RESPONSE.UPDATE_ALL, { count: response.count })) 95 | } 96 | 97 | find(filter, listName = undefined) { 98 | const apiPath = this.requestAdapter.resolveRouteParams(this.model, this.routeParams) 99 | console.log('using apiPath', apiPath) 100 | const {url, method, options} = this.requestAdapter.find(filter, {apiPath}) 101 | return this._call(url, method, options, 102 | () => this._createAction(REQUEST.FIND, { filter, listName }), 103 | this._createNormalized(RESPONSE.FIND, false, listName)) 104 | } 105 | 106 | findById(id, filter) { 107 | const apiPath = this.requestAdapter.resolveRouteParams(this.model, this.routeParams) 108 | const {url, method, options} = this.requestAdapter.findById(id, filter, {apiPath}) 109 | return this._call(url, method, options, 110 | () => this._createAction(REQUEST.FIND_BY_ID, { id, filter }), 111 | this._createNormalized(RESPONSE.FIND_BY_ID, true)) 112 | } 113 | 114 | deleteById(id) { 115 | const apiPath = this.requestAdapter.resolveRouteParams(this.model, this.routeParams) 116 | const {url, method, options} = this.requestAdapter.deleteById(id, {apiPath}) 117 | return this._call(url, method, options, 118 | () => this._createAction(REQUEST.DELETE_BY_ID, { id }), 119 | (response) => this._createAction(RESPONSE.DELETE_BY_ID, { id: id }) 120 | ) 121 | } 122 | 123 | count(where, listName=null) { 124 | const apiPath = this.requestAdapter.resolveRouteParams(this.model, this.routeParams) 125 | const {url, method, options} = this.requestAdapter.count(where, {apiPath}) 126 | return this._call(url, method, options, 127 | () => this._createAction(REQUEST.COUNT, { where, listName }), 128 | (response) => this._createAction(RESPONSE.COUNT, { count: response.count, listName }) 129 | ) 130 | } 131 | 132 | custom(name, path, _method, _options = {}) { 133 | const apiPath = this.requestAdapter.resolveRouteParams(this.model, this.routeParams) 134 | const {url, method, options} = this.requestAdapter.custom(name, path, _method, _options, {apiPath}) 135 | return this._call(url, method, options, 136 | () => this._createAction(REQUEST.CUSTOM, { name, path, _method, _options }), 137 | (response) => this._createAction(RESPONSE.CUSTOM, { response: response, name }) 138 | ) 139 | } 140 | 141 | clear() { 142 | return this._createAction(CLEAR, { }) 143 | } 144 | 145 | delete(filter) { 146 | throw new Error('not implemented yet') 147 | // return this._delete(`${this.apiPath}`, {filter: JSON.stringify(filter)}, 148 | // () => this._createAction(REQUEST.DELETE, { filter }), 149 | // (response) => _createPayload(RESPONSE.DELETE, { ids: response.ids }) 150 | // ) 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /src/model/index.js: -------------------------------------------------------------------------------- 1 | import ModelActions from './ModelActions' 2 | import modelSelectors from './modelSelectors' 3 | import List from '../list' 4 | 5 | export default class Model { 6 | constructor(model, config, apiAdapter, requestAdapter) { 7 | model.apiPath = model.apiPath ? config.basePath + model.apiPath : config.basePath + '/' + model.modelName 8 | 9 | this.modelName = model.modelName 10 | this.config = config 11 | this.actions = (routeParams) => { 12 | return new ModelActions(model, config, routeParams, apiAdapter, requestAdapter) 13 | } 14 | this.selectors = (routeParams) => { 15 | return modelSelectors(model, routeParams, config.rootSelector) 16 | } 17 | this.lists = {} 18 | if(model.lists) { 19 | model.lists.forEach(list => this.lists[list.name] = this.createList(list)) 20 | } 21 | } 22 | 23 | createList({name, options}) { 24 | return new List(name, this, options) 25 | } 26 | } -------------------------------------------------------------------------------- /src/model/modelReducer.js: -------------------------------------------------------------------------------- 1 | import { schema, normalize } from 'normalizr' 2 | import { combineReducers } from 'redux' 3 | import _ from 'lodash' 4 | import { REQUEST, RESPONSE, ERROR, SELECTED, RECEIVED, CLEAR } from '../constants' 5 | import { createListReducers } from '../list/listReducer' 6 | 7 | export default (model) => { 8 | const instances = (state = {}, { type, payload }) => { 9 | if (payload && payload.modelName !== model.modelName) { 10 | return state 11 | } 12 | switch (type) { 13 | case RECEIVED: 14 | return { ...state, ...payload.instances } 15 | case RESPONSE.DELETE_BY_ID: 16 | case RESPONSE.DELETE: 17 | const newState = { ...state } 18 | return _.pick(newState, _.difference(_.keys(newState), [payload.id])) 19 | case CLEAR: 20 | return {} 21 | default: 22 | return state 23 | } 24 | } 25 | 26 | const error = (state = null, { type, payload }) => { 27 | if (payload && payload.modelName !== model.modelName) { 28 | return state 29 | } 30 | switch (type) { 31 | case ERROR: 32 | return payload 33 | case CLEAR: 34 | return null 35 | default: 36 | return state 37 | } 38 | } 39 | 40 | const DEFAULT_REQUEST = { loading: false } 41 | const request = (state = DEFAULT_REQUEST, { type, payload }) => { 42 | if (payload && payload.modelName !== model.modelName) { 43 | return state 44 | } 45 | switch (type) { 46 | case REQUEST.FIND: 47 | case REQUEST.FIND_BY_ID: 48 | case REQUEST.CREATE: 49 | case REQUEST.UPDATE: 50 | case REQUEST.DELETE_BY_ID: 51 | case REQUEST.DELETE: 52 | case REQUEST.CUSTOM: 53 | return { ...state, loading: true } 54 | case RESPONSE.FIND: 55 | case RESPONSE.FIND_BY_ID: 56 | case RESPONSE.CREATE: 57 | case RESPONSE.UPDATE: 58 | case RESPONSE.DELETE_BY_ID: 59 | case RESPONSE.DELETE: 60 | case RESPONSE.CUSTOM: 61 | case ERROR: 62 | return { ...state, loading: false } 63 | case CLEAR: 64 | return { ...DEFAULT_REQUEST } 65 | default: 66 | return state 67 | } 68 | } 69 | 70 | const DEFAULT_LAST_STATE = { find: [], delete: [], custom: {} } 71 | const last = (state = DEFAULT_LAST_STATE, { type, payload }) => { 72 | if (payload && payload.modelName !== model.modelName) { 73 | return state 74 | } 75 | 76 | if (payload && payload.listName) { 77 | return state 78 | } 79 | 80 | switch (type) { 81 | case RESPONSE.FIND: 82 | return { ...state, find: payload.ids } 83 | case RESPONSE.FIND_BY_ID: 84 | return { ...state, findById: payload.id } 85 | case RESPONSE.CREATE: 86 | return { ...state, create: payload.id } 87 | case RESPONSE.UPDATE: 88 | return { ...state, update: payload.id } 89 | case RESPONSE.UPDATE_ALL: 90 | return { ...state, updateAll: payload.count } 91 | case RESPONSE.COUNT: 92 | return { ...state, count: payload.count } 93 | case RESPONSE.DELETE_BY_ID: 94 | //case RESPONSE.DELETE: 95 | const newState = { ...state, deleteById: payload.id } 96 | if (newState.findById == payload.id) newState.findById = null 97 | if (newState.create == payload.id) newState.create = null 98 | if (newState.update == payload.id) newState.update = null 99 | if (newState.find.indexOf(payload.id) > -1) { 100 | newState.find = newState.find.filter(id => id !== payload.id) 101 | } 102 | return newState 103 | case RESPONSE.CUSTOM: 104 | return { ...state, custom: { ...state.custom, [payload.name]: payload.response } } 105 | case CLEAR: 106 | return { ...DEFAULT_LAST_STATE } 107 | default: 108 | return state 109 | } 110 | } 111 | //console.log('listREducer', listReducer) 112 | return combineReducers({ 113 | instances, 114 | last, 115 | request, 116 | error, 117 | lists: createListReducers(model) 118 | }) 119 | } -------------------------------------------------------------------------------- /src/model/modelSelectors.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | export default (model, routeParams, rootSelector) => { 4 | const getModelObj = state => rootSelector(state)[model.modelName] 5 | const getInstances = state => _.values(rootSelector(state)[model.modelName].instances) 6 | const isLoading = state => rootSelector(state)[model.modelName].request.loading 7 | const getCount = state => rootSelector(state)[model.modelName].last.count 8 | const getFound = state => { 9 | const found = rootSelector(state)[model.modelName].last.find 10 | const instances = rootSelector(state)[model.modelName].instances 11 | return _.map(found, id => instances[id]) 12 | } 13 | 14 | return { 15 | getModelObj, getInstances, isLoading, getCount, getFound 16 | } 17 | } -------------------------------------------------------------------------------- /tests/integration/smoke.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nachiket-p/rest-redux/c354d0367bda6e3e59e8cd883fcbafe88eb18732/tests/integration/smoke.js -------------------------------------------------------------------------------- /tests/unit/list/reducers/list.test.js: -------------------------------------------------------------------------------- 1 | import { Reducer } from 'redux-testkit' 2 | import { listReducer } from '../../../../src/list/listReducer' 3 | import Model from '../../../../src/list' 4 | import List from '../../../../src/list' 5 | import { LIST, RESPONSE, CLEAR } from '../../../../src/constants' 6 | const { SET_OPTIONS, PAGE, NEXT, PREV, LAST, FIRST, SET_PARAMS } = LIST 7 | 8 | const completeListReducer = listReducer({ modelName: 'todos' }, { name: 'completed' }) 9 | const errorListReducer = listReducer({ modelName: 'todos' }, { name: 'incomplete' }) 10 | const errorModelReducer = listReducer({ modelName: 'users' }, { name: 'completed' }) 11 | const ID = 4 12 | describe(' complete list reducer', () => { 13 | let model, list 14 | beforeEach(() => { 15 | list = new List({ name: 'completed' }, { modelName: 'todos' }, { pageSize: ID, params: { completed: true } }) 16 | }) 17 | it('should test SET_OPTIONS action ', () => { 18 | const ACTION_CUSTOM_SET_OPTIONS = { 19 | type: SET_OPTIONS, 20 | 'payload': { 21 | offset: 1, 22 | pageSize: 5, 23 | total: null, 24 | result: [], 25 | headers: {}, 26 | params: { 27 | where: { 28 | 'completed': false 29 | } 30 | }, 31 | listName: 'completed', 32 | modelName: 'todos' 33 | } 34 | } 35 | const state = completeListReducer({ pageSize: ID, params: {} }, ACTION_CUSTOM_SET_OPTIONS) 36 | expect(state.pageSize).toEqual(5) 37 | expect(state.params).toEqual(ACTION_CUSTOM_SET_OPTIONS.payload.params) 38 | }) 39 | it('should test PAGE action', () => { 40 | const ACTION_CUSTOM_PAGE = { 41 | type: PAGE, 42 | 'payload': { 43 | page: 10, 44 | listName: 'completed', 45 | modelName: 'todos' 46 | } 47 | } 48 | const state = completeListReducer({ pageSize: ID }, ACTION_CUSTOM_PAGE) 49 | expect(state.offset).toEqual(ID * ACTION_CUSTOM_PAGE.payload.page) 50 | }) 51 | it('should test response find ', () => { 52 | const ACTION_CUSTOM_RESPONSE_FIND = { 53 | type: RESPONSE.FIND, 54 | 'payload': { 55 | ids: [ 56 | 3 57 | ], 58 | listName: 'completed', 59 | modelName: 'todos' 60 | } 61 | } 62 | const state = completeListReducer({ pageSize: ID }, ACTION_CUSTOM_RESPONSE_FIND) 63 | expect(state.result).toEqual(ACTION_CUSTOM_RESPONSE_FIND.payload.ids) 64 | }) 65 | it('should test response count', () => { 66 | const ACTION_CUSTOM_RESPONSE_COUNT = { 67 | type: RESPONSE.COUNT, 68 | 'payload': { 69 | count: ID, 70 | listName: 'completed', 71 | modelName: 'todos' 72 | } 73 | } 74 | const state = completeListReducer({ pageSize: ID }, ACTION_CUSTOM_RESPONSE_COUNT) 75 | expect(state.total).toEqual(ID) 76 | }) 77 | it('should test SET_PARAMS action ', () => { 78 | const ACTION_CUSTOM_SET_PARAMS = { 79 | type: SET_PARAMS, 80 | payload: { 81 | include: ['items'], 82 | listName: 'completed', 83 | modelName: 'todos' 84 | } 85 | } 86 | const state = completeListReducer({params:{where:{completed:true}}}, ACTION_CUSTOM_SET_PARAMS) 87 | expect(state.params.include).toEqual(ACTION_CUSTOM_SET_PARAMS.payload.include) 88 | expect(state.params.where).toEqual({completed:true}) 89 | }) 90 | it('should test error list test cases', () => { 91 | const ACTION_CUSTOM_ERROR_PAYLOAD = { 92 | type: RESPONSE.FIND, 93 | 'payload': { 94 | ids: [ 95 | 3 96 | ], 97 | listName: 'completed', 98 | modelName: 'todos' 99 | } 100 | } 101 | 102 | // Error case as model name does not matches 103 | const errModelState = errorModelReducer({ pageSize: ID }, ACTION_CUSTOM_ERROR_PAYLOAD) 104 | expect(errModelState.result).toBeUndefined() 105 | // type does not match 106 | expect(errModelState.total).toBeUndefined() 107 | expect(errModelState.offset).toBeUndefined() 108 | // Error case as list name does not matches 109 | const errListState = errorListReducer({ pageSize: ID }, ACTION_CUSTOM_ERROR_PAYLOAD) 110 | expect(errListState.result).toBeUndefined() 111 | // type does not match 112 | expect(errListState.offset).toBeUndefined() 113 | expect(errListState.total).toBeUndefined() 114 | 115 | 116 | }) 117 | }) 118 | 119 | -------------------------------------------------------------------------------- /tests/unit/list/selectors/list.test.js: -------------------------------------------------------------------------------- 1 | import listSelectors from '../../../../src/list/listSelectors' 2 | import modelSelectors from '../../../../src/model/modelSelectors' 3 | import { DEFAULT_CONFIG } from '../../../../src' 4 | import _ from 'lodash' 5 | const data = { 6 | '1': { 7 | text: 'Remember the milk', 8 | completed: false, 9 | id: 1 10 | }, 11 | '2': { 12 | text: 'Reminder to remember the milk', 13 | completed: false, 14 | id: 2 15 | }, 16 | '3': { 17 | text: 'Visualize milk as beer', 18 | completed: false, 19 | id: 3 20 | }, 21 | '4': { 22 | text: 'Don\'t forget the milk at the store', 23 | completed: false, 24 | id: 4 25 | }, 26 | '5': { 27 | text: 'fifth', 28 | completed: false, 29 | id: 5 30 | }, 31 | '6': { 32 | text: 'sixth', 33 | completed: false, 34 | id: 6 35 | }, 36 | '7': { 37 | text: 'seventh', 38 | completed: false, 39 | id: 7 40 | } 41 | } 42 | const STATE_FIRST_PAGE = { 43 | rest: { 44 | todos: { 45 | instances: { 46 | data 47 | }, 48 | last: { 49 | find: [ 50 | 1, 51 | 2, 52 | 3, 53 | 4, 54 | 5, 55 | 6, 56 | 7 57 | ], 58 | 'delete': [], 59 | custom: {} 60 | }, 61 | request: { 62 | loading: true 63 | }, 64 | error: null, 65 | lists: { 66 | completed: { 67 | offset: 0, 68 | pageSize: 3, 69 | total: 7, 70 | result: [1, 2, 3], 71 | headers: {}, 72 | params: { 73 | where: { completed: false } 74 | } 75 | } 76 | } 77 | } 78 | } 79 | } 80 | 81 | const STATE_MIDDLE_PAGE = { 82 | rest: { 83 | todos: { 84 | instances: { 85 | data 86 | }, 87 | last: { 88 | find: [ 89 | 1, 90 | 2, 91 | 3, 92 | 4, 93 | 5, 94 | 6, 95 | 7 96 | ], 97 | 'delete': [], 98 | custom: {} 99 | }, 100 | request: { 101 | loading: true 102 | }, 103 | error: null, 104 | lists: { 105 | completed: { 106 | offset: 3, 107 | pageSize: 3, 108 | total: 7, 109 | result: [4, 5, 6], 110 | headers: {}, 111 | params: { 112 | where: { completed: false } 113 | } 114 | } 115 | } 116 | } 117 | } 118 | } 119 | 120 | const STATE_LAST_PAGE = { 121 | rest: { 122 | todos: { 123 | instances: { 124 | data 125 | }, 126 | last: { 127 | find: [ 128 | 1, 129 | 2, 130 | 3, 131 | 4, 132 | 5, 133 | 6, 134 | 7 135 | ], 136 | 'delete': [], 137 | custom: {} 138 | }, 139 | request: { 140 | loading: true 141 | }, 142 | error: null, 143 | lists: { 144 | completed: { 145 | offset: 6, 146 | pageSize: 3, 147 | total: 7, 148 | result: [7], 149 | headers: {}, 150 | params: { 151 | where: { completed: false } 152 | } 153 | } 154 | } 155 | } 156 | } 157 | } 158 | 159 | 160 | 161 | const modelSelector = modelSelectors({ modelName: 'todos' }, {}, DEFAULT_CONFIG.rootSelector) 162 | describe('List Selector', () => { 163 | let selectors 164 | beforeEach(() => { 165 | selectors = listSelectors('completed', modelSelector) 166 | }) 167 | it('should return list Obj', () => { 168 | expect(selectors.getListObj(STATE_FIRST_PAGE)).toEqual(modelSelector.getModelObj(STATE_FIRST_PAGE).lists['completed']) 169 | }) 170 | it('should return total', () => { 171 | expect(selectors.getTotal(STATE_FIRST_PAGE)).toEqual(selectors.getListObj(STATE_FIRST_PAGE).total) 172 | }) 173 | it('should return instaces', () => { 174 | const instances = STATE_FIRST_PAGE.rest.todos.instances 175 | expect(selectors.getInstances(STATE_FIRST_PAGE)).toEqual([instances['1'], instances['2'], instances['3']]) 176 | }) 177 | it('should return get curent page', () => { 178 | const listObj = selectors.getListObj(STATE_FIRST_PAGE) 179 | expect(selectors.getCurrentPage(STATE_FIRST_PAGE)).toEqual(listObj.offset / listObj.pageSize) 180 | }) 181 | it('should return get pages ', () => { 182 | const listObj = selectors.getListObj(STATE_FIRST_PAGE) 183 | const pages = Math.ceil(listObj.total / listObj.pageSize) 184 | expect(selectors.getPages(STATE_FIRST_PAGE)).toEqual([...Array(pages)].map((_, i) => i++)) 185 | }) 186 | it('should return has Next', () => { 187 | expect(selectors.hasNext(STATE_FIRST_PAGE)).toEqual(true) 188 | expect(selectors.hasNext(STATE_MIDDLE_PAGE)).toEqual(true) 189 | expect(selectors.hasNext(STATE_LAST_PAGE)).toEqual(false) 190 | }) 191 | it('should return has prev', () => { 192 | expect(selectors.hasPrev(STATE_FIRST_PAGE)).toEqual(false) 193 | expect(selectors.hasPrev(STATE_MIDDLE_PAGE)).toEqual(true) 194 | expect(selectors.hasPrev(STATE_LAST_PAGE)).toEqual(true) 195 | }) 196 | }) 197 | 198 | -------------------------------------------------------------------------------- /tests/unit/model/actions.test.js: -------------------------------------------------------------------------------- 1 | import thunk from 'redux-thunk' 2 | import _ from 'lodash' 3 | import configureStore from 'redux-mock-store' 4 | //import nock from 'nock' 5 | 6 | import RestRedux from '../../../src' 7 | import Model from '../../../src/model' 8 | import { REQUEST, RESPONSE, CLEAR, RECEIVED } from '../../../src/constants' 9 | 10 | const middlewares = [thunk] 11 | const mockStore = configureStore(middlewares) 12 | 13 | const DEFAULT_STATE = { rest: { todos: { "error": null, "instances": {}, "request": { "loading": false }, last: { find: [], delete: [], custom: {} } } } } 14 | const BASE_PATH = 'http://localhost:3000/api' 15 | 16 | const mockResponse = (status, statusText, response) => { 17 | return new window.Response(JSON.stringify(response), { 18 | status: status, 19 | statusText: statusText, 20 | headers: { 21 | 'Content-type': 'application/json' 22 | } 23 | }); 24 | }; 25 | 26 | describe('ModelActions', () => { 27 | let model 28 | beforeEach(() => { 29 | const restRedux = new RestRedux({ 30 | basePath: BASE_PATH, 31 | models: [{ modelName: 'todos', apiPath: '/todos' }] 32 | }) 33 | model = restRedux.get('todos') 34 | }) 35 | 36 | afterEach(() => { 37 | //nock.cleanAll() 38 | }) 39 | 40 | it('should create a ModelActions', () => { 41 | const todoActions = model.actions() 42 | expect(todoActions).toBeDefined() 43 | expect(todoActions.create).toBeDefined() 44 | expect(todoActions.find).toBeDefined() 45 | expect(todoActions.findById).toBeDefined() 46 | expect(todoActions.update).toBeDefined() 47 | expect(todoActions.updateAll).toBeDefined() 48 | expect(todoActions.deleteById).toBeDefined() 49 | expect(todoActions.delete).toBeDefined() 50 | expect(todoActions.count).toBeDefined() 51 | expect(todoActions.custom).toBeDefined() 52 | }) 53 | 54 | it('should return instances of Model (find)', () => { 55 | const response = [{ "text": "Remember the milk", "completed": false, "id": 1 }, { "text": "Reminder to remember the milk", "completed": false, "id": 2 }] 56 | // nock(BASE_PATH) 57 | // .log((msg) => console.log("##NOCK@@ " + msg)) 58 | // .get('/todos').query(true) 59 | // .reply(200, response) 60 | window.fetch = jest.fn().mockImplementation(() => Promise.resolve(mockResponse(200, null, response))); 61 | 62 | const todoActions = model.actions() 63 | const store = mockStore(DEFAULT_STATE) 64 | 65 | const expectedActions = [ 66 | { type: REQUEST.FIND, payload: { modelName: "todos", filter: {} } }, 67 | { type: RECEIVED, payload: { modelName: "todos", instances: _.keyBy(response, 'id') } }, 68 | { type: RESPONSE.FIND, payload: { modelName: "todos", ids: response.map(todo => todo.id) } }, 69 | ] 70 | 71 | return store.dispatch(todoActions.find({})).then(() => { 72 | // return of async actions 73 | expect(store.getActions()).toEqual(expectedActions) 74 | }) 75 | }) 76 | 77 | it('should create instances of Model', () => { 78 | const NEW_TODO = { text: 'new todo', 'completed': false } 79 | const NEW_ID = 2 80 | const response = { ...NEW_TODO, "id": NEW_ID } 81 | // nock(BASE_PATH) 82 | // .log((msg) => console.log("##NOCK@@ " + msg)) 83 | // .post('/todos').query(true) 84 | // .reply(200, response) 85 | window.fetch = jest.fn().mockImplementation(() => Promise.resolve(mockResponse(200, null, response))); 86 | 87 | 88 | const todoActions = model.actions() 89 | const store = mockStore(DEFAULT_STATE) 90 | 91 | const expectedActions = [ 92 | { type: REQUEST.CREATE, payload: { modelName: "todos", data: NEW_TODO } }, 93 | { type: RECEIVED, payload: { modelName: "todos", instances: { [NEW_ID]: response } } }, 94 | { type: RESPONSE.CREATE, payload: { modelName: "todos", id: NEW_ID } }, 95 | ] 96 | 97 | return store.dispatch(todoActions.create(NEW_TODO)).then(() => { 98 | // return of async actions 99 | expect(store.getActions()).toEqual(expectedActions) 100 | }) 101 | }) 102 | 103 | 104 | it('should update an instance of model ', () => { 105 | const UPDATE_TODO = [{ text: "ok", "completed": true, id: 1 }] 106 | const ID = 1 107 | const response = { ...UPDATE_TODO, "id": ID } 108 | 109 | window.fetch = jest.fn().mockImplementation(() => Promise.resolve(mockResponse(200, null, response))); 110 | 111 | const updateActions = model.actions() 112 | const store = mockStore(DEFAULT_STATE) 113 | const expectedActions = [ 114 | { type: REQUEST.UPDATE, payload: { data: UPDATE_TODO, modelName: 'todos', id: ID } }, 115 | { type: RECEIVED, payload: { modelName: "todos", instances: { [ID]: response } } }, 116 | { type: RESPONSE.UPDATE, payload: { modelName: "todos", id: ID } } 117 | ] 118 | return store.dispatch(updateActions.update(ID, UPDATE_TODO)).then(() => { 119 | expect(store.getActions()).toEqual(expectedActions) 120 | }) 121 | }) 122 | 123 | it('should update all instances of model ', () => { 124 | const UPDATE_WHERE = { text: "ok", "completed": false, id: 1 } 125 | const UPDATE_DATA = { "completed": true } 126 | const UPDATE_VALUE = true 127 | const response = { ...UPDATE_WHERE, "completed": UPDATE_VALUE } 128 | window.fetch = jest.fn().mockImplementation(() => Promise.resolve(mockResponse(200, null, response))); 129 | 130 | const updateActions = model.actions() 131 | const store = mockStore(DEFAULT_STATE) 132 | 133 | const expectedActions = [ 134 | { type: REQUEST.UPDATE_ALL, payload: { where: UPDATE_WHERE, data: UPDATE_DATA, modelName: 'todos' } }, 135 | { type: RESPONSE.UPDATE_ALL, payload: { count: undefined, modelName: 'todos' } } 136 | ] 137 | return store.dispatch(updateActions.updateAll(UPDATE_WHERE, UPDATE_DATA)).then(() => { 138 | expect(store.getActions()).toEqual(expectedActions) 139 | }) 140 | }) 141 | 142 | it('should delete an instance by ID', () => { 143 | const COUNT = 3 144 | const ID = { 'count': COUNT } 145 | const response = { ...ID } 146 | window.fetch = jest.fn().mockImplementation(() => Promise.resolve(mockResponse(200, null, response))); 147 | 148 | const deleteActions = model.actions() 149 | const store = mockStore(DEFAULT_STATE) 150 | 151 | const expectedActions = [ 152 | { type: REQUEST.DELETE_BY_ID, payload: { 'id': COUNT, modelName: 'todos' } }, 153 | { type: RESPONSE.DELETE_BY_ID, payload: { 'id': COUNT, modelName: 'todos' } } 154 | ] 155 | return store.dispatch(deleteActions.deleteById(COUNT)).then(() => { 156 | expect(store.getActions()).toEqual(expectedActions) 157 | }) 158 | }) 159 | 160 | it('should return count of instance', () => { 161 | const WHERE = { "completed": false } 162 | const LISTNAME = null 163 | const response = { count: 3 } 164 | 165 | window.fetch = jest.fn().mockImplementation(() => Promise.resolve(mockResponse(200, null, response))); 166 | 167 | const countActions = model.actions() 168 | const store = mockStore(DEFAULT_STATE) 169 | 170 | const expectedActions = [ 171 | { type: REQUEST.COUNT, payload: { 'where': WHERE, 'listName': LISTNAME, modelName: 'todos' } }, 172 | { type: RESPONSE.COUNT, payload: { 'count': response.count, 'listName': LISTNAME, modelName: 'todos' } } 173 | ] 174 | return store.dispatch(countActions.count(WHERE, LISTNAME)).then(() => { 175 | expect(store.getActions()).toEqual(expectedActions) 176 | }) 177 | }) 178 | it('should return coustom action for instance', () => { 179 | const NAME = { name: 'LOGIN' } 180 | const PATH = { path: "login" } 181 | const METHOD = { method: 'POST' } 182 | const OPTION = { body: { email: 'john@doe.com', password: 'gotthemilk' } } 183 | const response = { ...NAME, ...PATH, ...METHOD, ...OPTION } 184 | 185 | window.fetch = jest.fn().mockImplementation(() => Promise.resolve(mockResponse(200, null, response))); 186 | 187 | const customAction = model.actions() 188 | const store = mockStore(DEFAULT_STATE) 189 | 190 | const expectedActions = [ 191 | { type: REQUEST.CUSTOM, payload: { name: NAME, path: PATH, _method: METHOD, _options: OPTION, modelName: 'todos' } }, 192 | { type: RESPONSE.CUSTOM, payload: { 'response': response, name: NAME, modelName: 'todos' } } 193 | ] 194 | return store.dispatch(customAction.custom(NAME, PATH, METHOD, OPTION)).then(() => { 195 | expect(store.getActions()).toEqual(expectedActions) 196 | }) 197 | }) 198 | it('should return instance of model (findById)', () => { 199 | const ID = 1 200 | const response = [{ "text": "Remember the milk", "completed": false, "id": 1 }] 201 | 202 | window.fetch = jest.fn().mockImplementation(() => Promise.resolve(mockResponse(200, null, response))); 203 | 204 | const FindByIdActions = model.actions() 205 | const store = mockStore(DEFAULT_STATE) 206 | const expectedActions = [ 207 | { type: REQUEST.FIND_BY_ID, payload: { id: ID , modelName:'todos'} }, 208 | { type: RECEIVED, payload: { modelName: 'todos', instances: _.keyBy(response, 'id') } }, 209 | { type: RESPONSE.FIND_BY_ID, payload: { id: response.map(todo => ID) , modelName:'todos'} } 210 | ] 211 | return store.dispatch(FindByIdActions.findById(ID)).then(() => { 212 | expect(store.getActions()).toEqual(expectedActions) 213 | }) 214 | }) 215 | }) 216 | -------------------------------------------------------------------------------- /tests/unit/model/model.test.js: -------------------------------------------------------------------------------- 1 | import Model from '../../../src/model' 2 | 3 | describe('Model', () => { 4 | it('should create a Model', () => { 5 | const model = new Model({modelName: 'todos'}, { basePath: 'http://localhost:3000/api' }) 6 | 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /tests/unit/model/reducers/error.test.js: -------------------------------------------------------------------------------- 1 | import { Reducer } from 'redux-testkit'; 2 | import modelReducer from '../../../../src/model/modelReducer'; 3 | import Model from '../../../../src/model' 4 | import instances from '../../../../src/model/modelReducer' 5 | import { REQUEST, RESPONSE, ERROR, SELECTED, RECEIVED, CLEAR } from '../../../../src/constants' 6 | import { error } from '../../../../src/model/modelReducer' 7 | const todoReducer = modelReducer({ modelName: 'todo' }) 8 | 9 | const DEFAULT_STATE_NO_LIST = { "error": null, "instances": {}, "request": { "loading": false }, last: { find: [], delete: [], custom: {} } } 10 | describe('error reducer', () => { 11 | let model 12 | beforeEach(() => { 13 | model = new Model({ modelName: 'todos' }, { basePath: 'http://localhost:3000/api' }) 14 | }) 15 | 16 | it('should have initial state', () => { 17 | expect(todoReducer(undefined, {})).toEqual(DEFAULT_STATE_NO_LIST); 18 | }) 19 | 20 | it('should return the payload as it is ', () => { 21 | expect(todoReducer({}, { 22 | type: ERROR, 23 | payload: { 24 | "error": 'something is a problem' 25 | } 26 | }) 27 | ).toEqual({ 28 | "error": null, "instances": {}, "last": { "custom": {}, "delete": [], "find": [] }, "request": { 29 | "loading": 30 | false 31 | } 32 | }) 33 | 34 | }) 35 | it('we are testing the clear function', () => { 36 | expect(todoReducer({}, { 37 | type: CLEAR, 38 | payload: { 39 | } 40 | }) 41 | ).toEqual({ 42 | "error": null, "instances": {}, "last": { "custom": {}, "delete": [], "find": [] }, "request": { 43 | "loading": 44 | false 45 | } 46 | }) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /tests/unit/model/reducers/instance.test.js: -------------------------------------------------------------------------------- 1 | import { Reducer } from 'redux-testkit'; 2 | import modelReducer from '../../../../src/model/modelReducer'; 3 | import Model from '../../../../src/model' 4 | import instances from '../../../../src/model/modelReducer' 5 | import { REQUEST, RESPONSE, ERROR, SELECTED, RECEIVED, CLEAR } from '../../../../src/constants' 6 | const todoReducer = modelReducer({ modelName: 'todos' }) 7 | 8 | 9 | const DEFAULT_STATE_NO_LIST = { "error": null, "instances": {}, "request": { "loading": false }, last: { find: [], delete: [], custom: {} } } 10 | describe('counter reducer', () => { 11 | let model 12 | beforeEach(() => { 13 | model = new Model({ modelName: 'todos' }, { basePath: 'http://localhost:3000/api' }) 14 | }) 15 | 16 | it('should have initial state', () => { 17 | expect(todoReducer(undefined, {})).toEqual(DEFAULT_STATE_NO_LIST); 18 | }) 19 | it('should test instance received ', () => { 20 | const store = todoReducer({}, { 21 | type: RECEIVED, 22 | payload: { 23 | modelName: 'todos', 24 | instances: { 25 | '1': { 26 | name: 'todos' 27 | }, 28 | 29 | '2': { 30 | name: 'example' 31 | } 32 | } 33 | } 34 | 35 | 36 | }) 37 | expect(store.instances).toEqual({ 1: { "name": 'todos' }, 2: { "name": 'example' } }) 38 | 39 | }) 40 | }) -------------------------------------------------------------------------------- /tests/unit/model/reducers/request.test.js: -------------------------------------------------------------------------------- 1 | import { Reducer } from 'redux-testkit'; 2 | import modelReducer from '../../../../src/model/modelReducer'; 3 | import Model from '../../../../src/model' 4 | import { REQUEST, RESPONSE, ERROR, SELECTED, RECEIVED, CLEAR } from '../../../../src/constants' 5 | const todoReducer = modelReducer({ modelName: 'todos' }) 6 | 7 | const DEFAULT_STATE_NO_LIST = { "error": null, "instances": {}, "request": { "loading": false }, last: { find: [], delete: [], custom: {} } } 8 | describe('request reducer', () => { 9 | let model 10 | beforeEach(() => { 11 | model = new Model({ modelName: 'todos' }, { basePath: 'http://localhost:3000/api' }) 12 | }) 13 | 14 | it('should have initial state', () => { 15 | expect(todoReducer(undefined, {})).toEqual(DEFAULT_STATE_NO_LIST); 16 | }) 17 | it('should test request create ', () => { 18 | const state = todoReducer({}, { 19 | type: REQUEST.CREATE, 20 | data: { 21 | text: 'checking working or not ' 22 | }, 23 | modelName: 'todos' 24 | 25 | }) 26 | expect(state.request).toEqual({ "loading": true }) 27 | }) 28 | it('should test request custom ', () => { 29 | const ACTION_CUSTOM = { 30 | type: REQUEST.CUSTOM, 31 | name: 'LOGIN', 32 | path: 'login', 33 | method: 'POST', 34 | options: { 35 | body: { 36 | email: 'john@doe.com', 37 | password: 'gotthemilk' 38 | } 39 | }, 40 | modelName: 'users' 41 | } 42 | const state = todoReducer({}, ACTION_CUSTOM) 43 | expect(state.request).toEqual({ "loading": true }) 44 | }) 45 | it('should test request find', () => { 46 | const state = todoReducer({}, { 47 | type: REQUEST.FIND, 48 | params: { 49 | filter: '{}' 50 | }, 51 | modelName: 'todos' 52 | }) 53 | expect(state.request).toEqual({ "loading": true }) 54 | }) 55 | it('should test request update_all', () => { 56 | const state = todoReducer({}, { 57 | type: REQUEST.UPDATE, 58 | where: { 59 | completed: false 60 | }, 61 | data: { 62 | completed: true 63 | }, 64 | modelName: 'todos' 65 | }) 66 | expect(state.request).toEqual({ "loading": true }) 67 | }) 68 | it('should test request delete_by_id', () => { 69 | const state = todoReducer({}, { 70 | type: REQUEST.DELETE_BY_ID, 71 | id: 3, 72 | modelName: 'todos' 73 | }) 74 | expect(state.request).toEqual({ "loading": true }) 75 | 76 | }) 77 | it('should test request delete', () => { 78 | const state = todoReducer({}, { 79 | type: REQUEST.DELETE, 80 | modelName: 'todos' 81 | }) 82 | expect(state.request).toEqual({ "loading": true }) 83 | }) 84 | it('should test response find', () => { 85 | const state = todoReducer({}, { 86 | type: RESPONSE.FIND, 87 | "payload": { 88 | ids: [ 89 | 1, 90 | 2, 91 | 3, 92 | 4 93 | ], 94 | modelName: 'todos' 95 | } 96 | }) 97 | expect(state.request).toEqual({ "loading": false }) 98 | expect(state.last.find).toEqual([1, 2, 3, 4]) 99 | }) 100 | it('should test response find_by_id', () => { 101 | const state = todoReducer({}, { 102 | type: RESPONSE.FIND_BY_ID, 103 | "payload": { 104 | id: [ 105 | 1 106 | ], 107 | modelName: 'todos' 108 | } 109 | }) 110 | expect(state.request).toEqual({ "loading": false }) 111 | expect(state.last.findById).toEqual([1]) 112 | }) 113 | it('should test response create', () => { 114 | const store = todoReducer({}, { 115 | type: RESPONSE.CREATE, 116 | "payload": { 117 | id: 6, 118 | modelName: 'todos' 119 | } 120 | }) 121 | expect(store.request).toEqual({ "loading": false }) 122 | expect(store.last.create).toEqual(6) 123 | }) 124 | it('should test response count', () => { 125 | const store = todoReducer({}, { 126 | type: RESPONSE.COUNT, 127 | "payload": { 128 | count: 2, 129 | listName: null, 130 | modelName: 'todos' 131 | } 132 | }) 133 | expect(store.request).toEqual({ "loading": false }) 134 | expect(store.last.count).toEqual(2) 135 | }) 136 | it('should test response delete_by_id', () => { 137 | const ID = 7 138 | const store = todoReducer({}, { 139 | type: RESPONSE.DELETE_BY_ID, 140 | "payload": { 141 | id: ID, 142 | modelName: 'todos' 143 | } 144 | }) 145 | expect(store.request).toEqual({ "loading": false }) 146 | expect(store.last.deleteById).toEqual(ID) 147 | expect(store.instances[ID]).toBeUndefined() 148 | }) 149 | it('should test response delete', () => { 150 | const ID = 3 151 | const store = todoReducer({}, { 152 | type: RESPONSE.DELETE, 153 | "payload": { 154 | id: ID, 155 | modelName: 'todos' 156 | } 157 | }) 158 | expect(store.instances[ID]).toBeUndefined() 159 | }) 160 | it('should test response update ', () => { 161 | const store = todoReducer({}, { 162 | type: RESPONSE.UPDATE, 163 | "payload": { 164 | id: 3, 165 | data: { 166 | text: 'checking working or not ' 167 | }, 168 | modelName: 'todos' 169 | } 170 | }) 171 | expect(store.request).toEqual({ "loading": false }) 172 | expect(store.last.update).toEqual(3) 173 | }) 174 | it('should test response update all', () => { 175 | const store = todoReducer({}, { 176 | type: RESPONSE.UPDATE_ALL, 177 | "payload": { 178 | count: 7, 179 | modelName: 'todos' 180 | } 181 | }) 182 | expect(store.last.updateAll).toEqual(7) 183 | }) 184 | it('should test response custom ', () => { 185 | const store = todoReducer({}, { 186 | type: RESPONSE.CUSTOM, 187 | "payload": { 188 | error: 'null pointer exception', 189 | name: 'LOGIN', 190 | path: 'login', 191 | method: 'POST', 192 | options: { 193 | body: { 194 | email: 'john@doe.com', 195 | password: 'gotthemilk' 196 | } 197 | } 198 | }, 199 | modelName: 'users' 200 | }) 201 | expect(store.last.custom).toEqual({}) 202 | }) 203 | }) 204 | -------------------------------------------------------------------------------- /tests/unit/model/selectors/model.test.js: -------------------------------------------------------------------------------- 1 | import modelSelectors from '../../../../src/model/modelSelectors' 2 | import { DEFAULT_CONFIG } from '../../../../src' 3 | import _ from 'lodash' 4 | const STATE ={ 5 | rest: { 6 | todos: { 7 | instances: { 8 | '1': { 9 | text: 'Remember the milk', 10 | completed: true, 11 | id: 1 12 | }, 13 | '2': { 14 | text: 'Reminder to remember the milk', 15 | completed: true, 16 | id: 2 17 | }, 18 | '3': { 19 | text: 'Visualize milk as beer', 20 | completed: true, 21 | id: 3 22 | }, 23 | '4': { 24 | text: 'Don\'t forget the milk at the store', 25 | completed: true, 26 | id: 4 27 | }, 28 | '5': { 29 | text: 'adsf', 30 | completed: false, 31 | id: 5 32 | } 33 | }, 34 | last: { 35 | find: [ 36 | 1, 37 | 2, 38 | 3 39 | ], 40 | 'delete': [], 41 | count : 3 , 42 | custom: {} 43 | }, 44 | request: { 45 | loading: false 46 | }, 47 | error: null 48 | }, 49 | users: { 50 | instances: {}, 51 | last: { 52 | find: [], 53 | 'delete': [], 54 | custom: { 55 | LOGIN: { 56 | id: 'NzeiN0JOtXKmyHN2Ut2cx8CaT5J1KEREusthSu9sbfkKdsASUsnSA8RDevSoGJZo', 57 | ttl: 1209600, 58 | created: '2017-07-22T09:24:58.073Z', 59 | userId: 1 60 | } 61 | } 62 | }, 63 | request: { 64 | loading: false 65 | }, 66 | error: null 67 | } 68 | } 69 | } 70 | const instances = STATE.rest.todos.instances 71 | 72 | describe('Model Selectors ', () => { 73 | let selectors 74 | beforeEach(() => { 75 | selectors = modelSelectors({ modelName: 'todos' }, {}, DEFAULT_CONFIG.rootSelector) 76 | }) 77 | 78 | it('should be defined', () => { 79 | expect(selectors).toBeDefined() 80 | }) 81 | 82 | it('should have following methods', () => { 83 | expect(selectors.getInstances).toBeDefined() 84 | expect(selectors.isLoading).toBeDefined() 85 | expect(selectors.getModelObj).toBeDefined() 86 | expect(selectors.getCount).toBeDefined() 87 | expect(selectors.getFound).toBeDefined() 88 | 89 | }) 90 | 91 | it('should return ModelObject', () => { 92 | expect(selectors.getModelObj(STATE)).toEqual(STATE.rest.todos) 93 | }) 94 | 95 | it('should return found Instances', () => { 96 | expect(selectors.getFound(STATE)).toEqual([ instances['1'], instances['2'], instances['3']]) 97 | }) 98 | it('should return instances' ,() => { 99 | expect(selectors.getInstances(STATE)).toEqual(_.values(STATE.rest.todos.instances)) 100 | }) 101 | it('should return loading',() => { 102 | expect(selectors.isLoading(STATE)).toEqual(STATE.rest.todos.request.loading) 103 | }) 104 | it('should return count ',() => { 105 | expect(selectors.getCount(STATE)).toEqual(STATE.rest.todos.last.count) 106 | }) 107 | }) 108 | --------------------------------------------------------------------------------