├── .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 | [](https://travis-ci.org/nachiket-p/rest-redux)
5 | [](https://codecov.io/gh/nachiket-p/rest-redux)
6 | [](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 |
20 |
21 |
22 |
23 |
24 |
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 |
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 |
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 |
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 |
46 | {todos.map(todo => - {todo.text}
)}
49 |
50 |
51 |
Comments of a First Todo
52 |
53 | {todoComments.map(comment => - {comment.text}
)}
54 |
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 |
--------------------------------------------------------------------------------