├── .babelrc
├── .editorconfig
├── .eslintrc
├── .gitignore
├── .npmignore
├── .travis.yml
├── .yaspellerrc
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── bower.json
├── dist
├── redux-api.js
├── redux-api.js.map
├── redux-api.min.js
└── redux-api.min.js.map
├── docs
├── AuthorizationJWT.md
├── DOCS.md
└── Scoping.md
├── examples
└── isomorphic
│ ├── .babelrc
│ ├── .gitignore
│ ├── README.md
│ ├── app
│ ├── client.jsx
│ ├── pages
│ │ ├── Application.jsx
│ │ ├── Repo.jsx
│ │ └── User.jsx
│ ├── routes
│ │ └── routes.js
│ ├── server.jsx
│ └── utils
│ │ └── rest.js
│ ├── dist
│ ├── 404.jpg
│ ├── favicon.ico
│ └── styles.css
│ ├── package.json
│ ├── server.js
│ ├── views
│ ├── 404.ejs
│ └── index.ejs
│ ├── webpack.config.js
│ └── yarn.lock
├── package.json
├── src
├── PubSub.js
├── actionFn.js
├── adapters
│ └── fetch.js
├── async.js
├── createHolder.js
├── fetchResolver.js
├── helpers.js
├── index.js
├── reducerFn.js
├── transformers.js
├── urlTransform.js
└── utils
│ ├── cache.js
│ ├── get.js
│ ├── merge.js
│ └── omit.js
├── test
├── PubSub_spec.js
├── actionFn_spec.js
├── adapters_fetch_spec.js
├── cache_spec.js
├── createHolder_spec.js
├── fetchResolver_spec.js
├── get_spec.js
├── index_spec.js
├── merge_spec.js
├── omit_spec.js
├── reducerFn_spec.js
├── redux_spec.js
└── urlTransform_spec.js
├── webpack.config.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env"],
3 | "plugins": ["add-module-exports"]
4 | }
5 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 | [*]
7 | # Change these settings to your own preference
8 | indent_style = space
9 | indent_size = 2
10 |
11 | # We recommend you to keep these unchanged
12 | end_of_line = lf
13 | charset = utf-8
14 | trim_trailing_whitespace = true
15 | insert_final_newline = true
16 |
17 | [*.md]
18 | trim_trailing_whitespace = false
19 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "airbnb",
4 | "prettier"
5 | ],
6 | "parser": "babel-eslint",
7 | "env": {
8 | "browser": true,
9 | "node": true
10 | },
11 | "rules": {
12 | "prefer-promise-reject-errors": "off",
13 | "prefer-destructuring": "off",
14 | "no-multi-spaces": 0,
15 | "space-infix-ops": 0,
16 | "quotes": [
17 | 2, "double", "avoid-escape" // http://eslint.org/docs/rules/quotes
18 | ],
19 | "func-names": 0,
20 | "vars-on-top": 0,
21 | "strict": 0,
22 | "no-unused-expressions": 0,
23 | "consistent-return": 0,
24 | "one-var": 0,
25 | "new-cap": 0,
26 | "no-else-return": 0,
27 | "semi-spacing": 0,
28 | "no-nested-ternary": 0,
29 | "no-shadow": 0,
30 | "no-param-reassign": 0,
31 | "no-extend-native": 0,
32 | "no-empty": 0,
33 | "guard-for-in": 0,
34 | "comma-dangle": 0,
35 | "space-before-function-paren": 0,
36 | "arrow-spacing": [2, { "before": true, "after": true }],
37 | "arrow-parens": [2, "as-needed"],
38 | "prefer-template": 0,
39 | "prefer-arrow-callback": 0,
40 | "arrow-body-style": 0,
41 | "no-confusing-arrow": 0,
42 | "react/require-extension": 0,
43 | "import/no-unresolved": 0,
44 | "import/extentions": 0,
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | coverage/
4 | bower_components
5 | /lib
6 | .DS_Store
7 |
8 | #Webstorm metadata
9 | .idea
10 |
11 | #VSCode metadata
12 | .vscode
13 |
14 | #IntelliJ metadata
15 | *.iml
16 | /.nyc_output
17 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /coverage
2 | /dist
3 | /examples
4 | /node_modules
5 | /src
6 | /test
7 | /.babelrc
8 | /.editorconfig
9 | /.eslintrc
10 | /.gitignore
11 | /.travis.yml
12 | /webpack.config.js
13 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '10'
4 | - '12'
5 | - '13'
6 | after_script:
7 | - npm run coveralls
8 |
--------------------------------------------------------------------------------
/.yaspellerrc:
--------------------------------------------------------------------------------
1 |
2 | {
3 | "checkYo": true,
4 | "lang": "en,ru",
5 | "format": "auto",
6 | "fileExtensions": [".md"],
7 | "excludeFiles":[
8 | "coverage",
9 | "dist",
10 | "examples",
11 | "lib",
12 | "node_modules",
13 | "src",
14 | "test",
15 | "yaspeller"
16 | ],
17 | "dictionary": [
18 | "JWT",
19 | "api",
20 | "AuthorizationJWT",
21 | "Changelog",
22 | "Redux",
23 | "browserify",
24 | "endpoint",
25 | "endpoints",
26 | "endpoint's",
27 | "actionFail - emits",
28 | "reduxApi",
29 | "github",
30 | "isomorphic",
31 | "js",
32 | "jsx",
33 | "md",
34 | "npm",
35 | "redux",
36 | "webpack",
37 | "actionFail",
38 | "actionFetch",
39 | "actionReset",
40 | "actionSuccess",
41 | "async",
42 | "autogenerate",
43 | "backend",
44 | "backends",
45 | "crud",
46 | "es7",
47 | "getUser",
48 | "isServer",
49 | "middlewareParser",
50 | "param",
51 | "params",
52 | "postfetch",
53 | "qs",
54 | "reducerName",
55 | "rootUrl",
56 | "stringify",
57 | "thunk",
58 | "updateUser",
59 | "urlOptions",
60 | "urlparams",
61 | "urls",
62 | "usefull",
63 | "v1",
64 | "xhr",
65 | "javascript",
66 | "responseHandler",
67 | "restApi",
68 | "Efremov",
69 | "NONINFRINGEMENT",
70 | "sublicense",
71 | "baseConfig",
72 | "It generates",
73 | "scoping",
74 | "uses",
75 | "reducers",
76 | "from success response",
77 | "initial store",
78 | "init",
79 | "size mode",
80 | "use",
81 | "sending events"
82 | ]
83 | }
84 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 | ## [0.12.0](https://github.com/lexich/redux-api/compare/v0.11.2...v0.12.0) (2020-02-25)
6 |
7 |
8 | ### Bug Fixes
9 |
10 | * potential security vulnerabilities in dependencies ([5a37929](https://github.com/lexich/redux-api/commit/5a379290d78798ca39b4eb7792f9a0cd0d6ab0b8))
11 |
12 |
13 | ## [0.11.2](https://github.com/lexich/redux-api/compare/v0.11.1...v0.11.2) (2017-12-29)
14 |
15 |
16 |
17 |
18 | ## [0.11.1](https://github.com/lexich/redux-api/compare/v0.11.0...v0.11.1) (2017-10-02)
19 |
20 |
21 |
22 |
23 | # [0.11.0](https://github.com/lexich/redux-api/compare/v0.10.8...v0.11.0) (2017-07-31)
24 |
25 |
26 |
27 |
28 | ## [0.10.8](https://github.com/lexich/redux-api/compare/v0.10.7...v0.10.8) (2017-07-14)
29 |
30 |
31 |
32 |
33 | ## [0.10.7](https://github.com/lexich/redux-api/compare/v0.10.6...v0.10.7) (2017-07-10)
34 |
35 |
36 |
37 |
38 | ## [0.10.6](https://github.com/lexich/redux-api/compare/v0.10.5...v0.10.6) (2017-06-12)
39 |
40 |
41 |
42 |
43 | ## [0.10.5](https://github.com/lexich/redux-api/compare/v0.10.4...v0.10.5) (2017-05-18)
44 |
45 |
46 |
47 |
48 | ## [0.10.4](https://github.com/lexich/redux-api/compare/v0.10.3...v0.10.4) (2017-04-28)
49 |
50 |
51 |
52 |
53 | ## [0.10.3](https://github.com/lexich/redux-api/compare/v0.10.2...v0.10.3) (2017-03-30)
54 |
55 |
56 |
57 |
58 | ## [0.10.2](https://github.com/lexich/redux-api/compare/v0.9.18...v0.10.2) (2017-03-29)
59 | Add support cache [option](https://github.com/lexich/redux-api/blob/master/docs/DOCS.md#cache)
60 |
61 |
62 | ## [0.9.18](https://github.com/lexich/redux-api/compare/v0.9.17...v0.9.18) (2017-03-06)
63 |
64 |
65 |
66 |
67 | ## [0.9.17](https://github.com/lexich/redux-api/compare/0.9.16...v0.9.17) (2017-01-19)
68 |
69 |
70 |
71 |
72 | ## [0.9.15](https://github.com/lexich/redux-api/compare/v0.9.13...v0.9.15) (2016-11-16)
73 |
74 |
75 |
76 |
77 | ## [0.9.13](https://github.com/lexich/redux-api/compare/v0.9.12...v0.9.13) (2016-10-24)
78 |
79 |
80 |
81 |
82 | ## [0.9.12](https://github.com/lexich/redux-api/compare/0.9.11...v0.9.12) (2016-10-24)
83 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016 Efremov Alex
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### Redux-api
2 | Flux REST API for redux infrastructure
3 |
4 | [](https://travis-ci.org/lexich/redux-api)
5 | [](http://badge.fury.io/js/redux-api)
6 | [](https://coveralls.io/r/lexich/redux-api?branch=master)
7 |
8 | ## Introduction
9 | `redux-api` solves the problem of writing clients to communicate with backends. It generates [actions](http://redux.js.org/docs/basics/Actions.html) and [reducers](http://redux.js.org/docs/basics/Reducers.html) for making AJAX calls to API endpoints. You don't need to write a lot of [boilerplate code](http://redux.js.org/docs/advanced/ExampleRedditAPI.html) if you use `redux` and want to exchange data with server.
10 |
11 | Inspired by [Redux-rest](https://github.com/Kvoti/redux-rest) and is intended to be used with [Redux](https://github.com/gaearon/redux).
12 |
13 |
14 | ## Documentation
15 | See [DOCS.md](docs/DOCS.md) for API documentation.
16 | ## Use cases
17 | * [AuthorizationJWT.md](docs/AuthorizationJWT.md) - example of JWT Authorization
18 | * [Scoping.md](docs/Scoping.md) - use scoping or using multiple redux-api instance without naming intersections.
19 |
20 | ## Install
21 | With npm:
22 | ```sh
23 | npm install redux-api --save
24 | ```
25 | With bower:
26 | ```sh
27 | bower install redux-api --save
28 | ```
29 |
30 | If you don't use tools like webpack, browserify, etc and you want to load redux-api manually, the best way to add redux-api to your project is:
31 | ```js
32 |
33 |
39 | ```
40 |
41 | =======
42 | ## Remote calls
43 |
44 | `redux-api` doesn't bind you to a technology to make AJAX calls. It uses configurable `adapters` - a pretty simple function which receives 2 arguments: `endpoint` and `options`, and returns a Promise as result. The default adapter uses [isomorphic-fetch](https://github.com/matthew-andrews/isomorphic-fetch), and has an implementation like this:
45 | ```js
46 | function adapterFetch(url, options) {
47 | return fetch(url, options);
48 | }
49 | ```
50 |
51 | However, you are not tied to using isomorphic-fetch. For instance, if you prefer to use jQuery, you can use the following adapter:
52 | ```js
53 | function adapterJquery(url, options) {
54 | return new Promise((success, error)=> {
55 | $.ajax({ ...options, url, success, error });
56 | });
57 | }
58 | ```
59 | This implementation allows you to make any request and process any response.
60 |
61 | And of course you have to set up adapter to your `redux-api` instance before using.
62 | ```
63 | reduxApi(....).use("fetch", adapterFetch)
64 | ```
65 |
66 | =======
67 | ## Examples
68 | [examples/isomorphic](https://github.com/lexich/redux-api/tree/master/examples/isomorphic) - React + Redux + React-Router + Redux-api with webpack and express + github API
69 |
70 | ### Example
71 | rest.js
72 | ```js
73 | import "isomorphic-fetch";
74 | import reduxApi, {transformers} from "redux-api";
75 | import adapterFetch from "redux-api/lib/adapters/fetch";
76 | export default reduxApi({
77 | // simple endpoint description
78 | entry: `/api/v1/entry/:id`,
79 | // complex endpoint description
80 | regions: {
81 | url: `/api/v1/regions`,
82 | // reimplement default `transformers.object`
83 | transformer: transformers.array,
84 | // base endpoint options `fetch(url, options)`
85 | options: {
86 | headers: {
87 | "Accept": "application/json"
88 | }
89 | }
90 | }
91 | }).use("fetch", adapterFetch(fetch));
92 | ```
93 |
94 | index.jsx
95 | ```js
96 | import React, {PropTypes} from "react";
97 | import { createStore, applyMiddleware, combineReducers } from "redux";
98 | import thunk from "redux-thunk";
99 | import { Provider, connect } from "react-redux";
100 | import rest from "./rest"; //our redux-rest object
101 |
102 | const createStoreWithMiddleware = applyMiddleware(thunk)(createStore);
103 | const reducer = combineReducers(rest.reducers);
104 | const store = createStoreWithMiddleware(reducer);
105 |
106 | function select(state) {
107 | return { entry: state.entry, regions: state.regions };
108 | }
109 |
110 | class Application {
111 | static propTypes = {
112 | entry: PropTypes.shape({
113 | loading: PropTypes.bool.isRequired,
114 | data: PropTypes.shape({
115 | text: PropTypes.string
116 | }).isRequired
117 | }).isRequired,
118 | regions: PropTypes.shape({
119 | loading: PropTypes.bool.isRequired,
120 | data: PropTypes.array.isRequired
121 | }).isRequired,
122 | dispatch: PropTypes.func.isRequired
123 | };
124 | componentDidMount() {
125 | const {dispatch} = this.props;
126 | // fetch `/api/v1/regions
127 | dispatch(rest.actions.regions.sync());
128 | //specify id for GET: /api/v1/entry/1
129 | dispatch(rest.actions.entry({id: 1}));
130 | }
131 | render() {
132 | const {entry, regions} = this.props;
133 | const Regions = regions.data.map((item)=>
{ item.name }
)
134 | return (
135 |
136 | Loading regions: { regions.loading }
137 |
138 | Loading entry: {entry.loading}
139 |
{{ entry.data.text }}
140 |
141 | );
142 | }
143 | }
144 |
145 | const SmartComponent = connect(select)(Application);
146 |
147 | React.render(
148 |
149 |
150 | ,
151 | document.getElementById("content")
152 | );
153 | ```
154 |
155 | ### [Releases Changelog](https://github.com/lexich/redux-api/releases)
156 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-api",
3 | "version": "0.12.0",
4 | "main": "dist/redux-api.min.js",
5 | "dependencies": {}
6 | }
7 |
--------------------------------------------------------------------------------
/dist/redux-api.min.js:
--------------------------------------------------------------------------------
1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define("redux-api",[],e):"object"==typeof exports?exports["redux-api"]=e():t["redux-api"]=e()}(window,(function(){return function(t){var e={};function r(n){if(e[n])return e[n].exports;var o=e[n]={i:n,l:!1,exports:{}};return t[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}return r.m=t,r.c=e,r.d=function(t,e,n){r.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:n})},r.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},r.t=function(t,e){if(1&e&&(t=r(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)r.d(n,o,function(e){return t[e]}.bind(null,o));return n},r.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return r.d(e,"a",e),e},r.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},r.p="",r(r.s=13)}([function(t,e,r){"use strict";var n=r(4),o=r(7);function a(){this.protocol=null,this.slashes=null,this.auth=null,this.host=null,this.port=null,this.hostname=null,this.hash=null,this.search=null,this.query=null,this.pathname=null,this.path=null,this.href=null}e.parse=m,e.resolve=function(t,e){return m(t,!1,!0).resolve(e)},e.resolveObject=function(t,e){return t?m(t,!1,!0).resolveObject(e):e},e.format=function(t){o.isString(t)&&(t=m(t));return t instanceof a?t.format():a.prototype.format.call(t)},e.Url=a;var i=/^([a-z0-9.+-]+:)/i,c=/:[0-9]*$/,s=/^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/,u=["{","}","|","\\","^","`"].concat(["<",">",'"',"`"," ","\r","\n","\t"]),l=["'"].concat(u),f=["%","/","?",";","#"].concat(l),h=["/","?","#"],p=/^[+a-z0-9A-Z_-]{0,63}$/,y=/^([+a-z0-9A-Z_-]{0,63})(.*)$/,d={javascript:!0,"javascript:":!0},b={javascript:!0,"javascript:":!0},v={http:!0,https:!0,ftp:!0,gopher:!0,file:!0,"http:":!0,"https:":!0,"ftp:":!0,"gopher:":!0,"file:":!0},g=r(8);function m(t,e,r){if(t&&o.isObject(t)&&t instanceof a)return t;var n=new a;return n.parse(t,e,r),n}a.prototype.parse=function(t,e,r){if(!o.isString(t))throw new TypeError("Parameter 'url' must be a string, not "+typeof t);var a=t.indexOf("?"),c=-1!==a&&a127?F+="x":F+=N[I];if(!F.match(p)){var U=C.slice(0,D),R=C.slice(D+1),L=N.match(y);L&&(U.push(L[1]),R.unshift(L[2])),R.length&&(m="/"+R.join(".")+m),this.hostname=U.join(".");break}}}this.hostname.length>255?this.hostname="":this.hostname=this.hostname.toLowerCase(),k||(this.hostname=n.toASCII(this.hostname));var H=this.port?":"+this.port:"",_=this.hostname||"";this.host=_+H,this.href+=this.host,k&&(this.hostname=this.hostname.substr(1,this.hostname.length-2),"/"!==m[0]&&(m="/"+m))}if(!d[w])for(D=0,E=l.length;D0)&&r.host.split("@"))&&(r.auth=k.shift(),r.host=r.hostname=k.shift());return r.search=t.search,r.query=t.query,o.isNull(r.pathname)&&o.isNull(r.search)||(r.path=(r.pathname?r.pathname:"")+(r.search?r.search:"")),r.href=r.format(),r}if(!P.length)return r.pathname=null,r.search?r.path="/"+r.search:r.path=null,r.href=r.format(),r;for(var A=P.slice(-1)[0],S=(r.host||t.host||P.length>1)&&("."===A||".."===A)||""===A,D=0,q=P.length;q>=0;q--)"."===(A=P[q])?P.splice(q,1):".."===A?(P.splice(q,1),D++):D&&(P.splice(q,1),D--);if(!j&&!w)for(;D--;D)P.unshift("..");!j||""===P[0]||P[0]&&"/"===P[0].charAt(0)||P.unshift(""),S&&"/"!==P.join("/").substr(-1)&&P.push("");var k,C=""===P[0]||P[0]&&"/"===P[0].charAt(0);x&&(r.hostname=r.host=C?"":P.length?P.shift():"",(k=!!(r.host&&r.host.indexOf("@")>0)&&r.host.split("@"))&&(r.auth=k.shift(),r.host=r.hostname=k.shift()));return(j=j||r.host&&P.length)&&!C&&P.unshift(""),P.length?r.pathname=P.join("/"):(r.pathname=null,r.path=null),o.isNull(r.pathname)&&o.isNull(r.search)||(r.path=(r.pathname?r.pathname:"")+(r.search?r.search:"")),r.auth=t.auth||r.auth,r.slashes=r.slashes||t.slashes,r.href=r.format(),r},a.prototype.parseHost=function(){var t=this.host,e=c.exec(t);e&&(":"!==(e=e[0])&&(this.port=e.substr(1)),t=t.substr(0,t.length-e.length)),t&&(this.hostname=t)}},function(t,e){t.exports=function(t,e,r){switch(r?r.length:0){case 0:return e?t.call(e):t();case 1:return e?t.call(e,r[0]):t(r[0]);case 2:return e?t.call(e,r[0],r[1]):t(r[0],r[1]);case 3:return e?t.call(e,r[0],r[1],r[2]):t(r[0],r[1],r[2]);case 4:return e?t.call(e,r[0],r[1],r[2],r[3]):t(r[0],r[1],r[2],r[3]);case 5:return e?t.call(e,r[0],r[1],r[2],r[3],r[4]):t(r[0],r[1],r[2],r[3],r[4]);default:return t.apply(e,r)}}},function(t,e,r){var n=r(11),o=r(12);t.exports={stringify:n,parse:o}},function(t,e){var r={};r.hexTable=new Array(256);for(var n=0;n<256;++n)r.hexTable[n]="%"+((n<16?"0":"")+n.toString(16)).toUpperCase();e.arrayToObject=function(t,e){for(var r=e.plainObjects?Object.create(null):{},n=0,o=t.length;n=48&&a<=57||a>=65&&a<=90||a>=97&&a<=122?e+=t[n]:a<128?e+=r.hexTable[a]:a<2048?e+=r.hexTable[192|a>>6]+r.hexTable[128|63&a]:a<55296||a>=57344?e+=r.hexTable[224|a>>12]+r.hexTable[128|a>>6&63]+r.hexTable[128|63&a]:(++n,a=65536+((1023&a)<<10|1023&t.charCodeAt(n)),e+=r.hexTable[240|a>>18]+r.hexTable[128|a>>12&63]+r.hexTable[128|a>>6&63]+r.hexTable[128|63&a])}return e},e.compact=function(t,r){if("object"!=typeof t||null===t)return t;var n=(r=r||[]).indexOf(t);if(-1!==n)return r[n];if(r.push(t),Array.isArray(t)){for(var o=[],a=0,i=t.length;a= 0x80 (not a basic code point)","invalid-input":"Invalid input"},p=Math.floor,y=String.fromCharCode;function d(t){throw new RangeError(h[t])}function b(t,e){for(var r=t.length,n=[];r--;)n[r]=e(t[r]);return n}function v(t,e){var r=t.split("@"),n="";return r.length>1&&(n=r[0]+"@",t=r[1]),n+b((t=t.replace(f,".")).split("."),e).join(".")}function g(t){for(var e,r,n=[],o=0,a=t.length;o=55296&&e<=56319&&o65535&&(e+=y((t-=65536)>>>10&1023|55296),t=56320|1023&t),e+=y(t)})).join("")}function O(t,e){return t+22+75*(t<26)-((0!=e)<<5)}function j(t,e,r){var n=0;for(t=r?p(t/700):t>>1,t+=p(t/e);t>455;n+=36)t=p(t/35);return p(n+36*t/(t+38))}function w(t){var e,r,n,o,a,i,c,u,l,f,h,y=[],b=t.length,v=0,g=128,O=72;for((r=t.lastIndexOf("-"))<0&&(r=0),n=0;n=128&&d("not-basic"),y.push(t.charCodeAt(n));for(o=r>0?r+1:0;o=b&&d("invalid-input"),((u=(h=t.charCodeAt(o++))-48<10?h-22:h-65<26?h-65:h-97<26?h-97:36)>=36||u>p((s-v)/i))&&d("overflow"),v+=u*i,!(u<(l=c<=O?1:c>=O+26?26:c-O));c+=36)i>p(s/(f=36-l))&&d("overflow"),i*=f;O=j(v-a,e=y.length+1,0==a),p(v/e)>s-g&&d("overflow"),g+=p(v/e),v%=e,y.splice(v++,0,g)}return m(y)}function P(t){var e,r,n,o,a,i,c,u,l,f,h,b,v,m,w,P=[];for(b=(t=g(t)).length,e=128,r=0,a=72,i=0;i=e&&hp((s-r)/(v=n+1))&&d("overflow"),r+=(c-e)*v,e=c,i=0;is&&d("overflow"),h==e){for(u=r,l=36;!(u<(f=l<=a?1:l>=a+26?26:l-a));l+=36)w=u-f,m=36-f,P.push(y(O(f+w%m,0))),u=p(w/m);P.push(y(O(u,0))),a=j(r,v,n==o),r=0,++n}++r,++e}return P.join("")}c={version:"1.4.1",ucs2:{decode:g,encode:m},decode:w,encode:P,toASCII:function(t){return v(t,(function(t){return l.test(t)?"xn--"+P(t):t}))},toUnicode:function(t){return v(t,(function(t){return u.test(t)?w(t.slice(4).toLowerCase()):t}))}},void 0===(o=function(){return c}.call(e,r,e,t))||(t.exports=o)}()}).call(this,r(5)(t),r(6))},function(t,e){t.exports=function(t){return t.webpackPolyfill||(t.deprecate=function(){},t.paths=[],t.children||(t.children=[]),Object.defineProperty(t,"loaded",{enumerable:!0,get:function(){return t.l}}),Object.defineProperty(t,"id",{enumerable:!0,get:function(){return t.i}}),t.webpackPolyfill=1),t}},function(t,e){var r;r=function(){return this}();try{r=r||new Function("return this")()}catch(t){"object"==typeof window&&(r=window)}t.exports=r},function(t,e,r){"use strict";t.exports={isString:function(t){return"string"==typeof t},isObject:function(t){return"object"==typeof t&&null!==t},isNull:function(t){return null===t},isNullOrUndefined:function(t){return null==t}}},function(t,e,r){"use strict";e.decode=e.parse=r(9),e.encode=e.stringify=r(10)},function(t,e,r){"use strict";function n(t,e){return Object.prototype.hasOwnProperty.call(t,e)}t.exports=function(t,e,r,a){e=e||"&",r=r||"=";var i={};if("string"!=typeof t||0===t.length)return i;var c=/\+/g;t=t.split(e);var s=1e3;a&&"number"==typeof a.maxKeys&&(s=a.maxKeys);var u=t.length;s>0&&u>s&&(u=s);for(var l=0;l=0?(f=d.substr(0,b),h=d.substr(b+1)):(f=d,h=""),p=decodeURIComponent(f),y=decodeURIComponent(h),n(i,p)?o(i[p])?i[p].push(y):i[p]=[i[p],y]:i[p]=y}return i};var o=Array.isArray||function(t){return"[object Array]"===Object.prototype.toString.call(t)}},function(t,e,r){"use strict";var n=function(t){switch(typeof t){case"string":return t;case"boolean":return t?"true":"false";case"number":return isFinite(t)?t:"";default:return""}};t.exports=function(t,e,r,c){return e=e||"&",r=r||"=",null===t&&(t=void 0),"object"==typeof t?a(i(t),(function(i){var c=encodeURIComponent(n(i))+r;return o(t[i])?a(t[i],(function(t){return c+encodeURIComponent(n(t))})).join(e):c+encodeURIComponent(n(t[i]))})).join(e):c?encodeURIComponent(n(c))+r+encodeURIComponent(n(t)):""};var o=Array.isArray||function(t){return"[object Array]"===Object.prototype.toString.call(t)};function a(t,e){if(t.map)return t.map(e);for(var r=[],n=0;n=0&&r.parseArrays&&c<=r.arrayLimit?(n=[])[c]=o.parseObject(t,e,r):n[i]=o.parseObject(t,e,r)}return n},parseKeys:function(t,e,r){if(t){r.allowDots&&(t=t.replace(/\.([^\.\[]+)/g,"[$1]"));var n=/(\[[^\[\]]*\])/g,a=/^([^\[\]]*)/.exec(t),i=[];if(a[1]){if(!r.plainObjects&&Object.prototype.hasOwnProperty(a[1])&&!r.allowPrototypes)return;i.push(a[1])}for(var c=0;null!==(a=n.exec(t))&&ci.pop().valueOf()?r:void 0}},id:function(t){return t?Object.keys(t).reduce((function(e,r){return e+"".concat(r,"=").concat(t[r],";")}),""):""}};function s(t,e){var r=t;if("number"==typeof r||r instanceof Number){var n=i.pop();n.setSeconds(n.getSeconds()+r),r=n}return e instanceof Date&&r instanceof Date&&r.valueOf()1?r-1:0),o=1;o0&&void 0!==arguments[0]?arguments[0]:0,e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:F;!e.prefetch||t>=e.prefetch.length?r():e.prefetch[t](e,(function(n){return n?r(n):I(t+1,e,r)}))}function T(t,e){for(var r=0;r1&&void 0!==arguments[1]&&arguments[1],r=arguments.length>2?arguments[2]:void 0;return console.warn("Deprecated method, use `use` method"),this.use("fetch",t),this.use("server",e),this.use("rootUrl",r),this},actions:{},reducers:{},events:{}};function a(t,n,a){var i="object"===et(n)?Y({},rt,{reducerName:a},n):Y({},rt,{reducerName:a,url:n});void 0!==i.broadcast&&console.warn("Deprecated `broadcast` option. you shoud use `events`to catch redux-api events (see https://github.com/lexich/redux-api/blob/master/DOCS.md#Events)");var l,p=i.url,d=i.urlOptions,v=i.options,g=i.transformer,m=i.broadcast,O=i.crud,j=i.reducerName,w=i.prefetch,P=i.postfetch,A=i.validation,S=i.helpers,D=e&&e.prefix||"",q={actionFetch:"".concat(nt,"@").concat(D).concat(j),actionSuccess:"".concat(nt,"@").concat(D).concat(j,"_success"),actionFail:"".concat(nt,"@").concat(D).concat(j,"_fail"),actionReset:"".concat(nt,"@").concat(D).concat(j,"_delete"),actionCache:"".concat(nt,"@").concat(D).concat(j,"_cache"),actionAbort:"".concat(nt,"@").concat(D).concat(j,"_abort")},C=i.fetch?i.fetch:function(){for(var t=arguments.length,e=new Array(t),n=0;n3&&void 0!==arguments[3]?arguments[3]:{},o=arguments.length>4&&void 0!==arguments[4]?arguments[4]:{},a=n.actionFetch,i=n.actionSuccess,c=n.actionFail,s=n.actionReset,l=n.actionCache,f=n.actionAbort,h=new U,p=R();function d(t,e,n){var a=o.holder?o.holder.options instanceof Function?o.holder.options(t,e,n):o.holder.options:{},i=r instanceof Function?r(t,e,n):r;return x({},a,i,e)}function v(e,r,n){var a=k(t,e,o.urlOptions),i=N(o,"holder","rootUrl");if(i=i instanceof Function?i(a,r,n):i){var c=b.a.parse(i),s=b.a.parse(a);if(!s.host){var u=(c.path?c.path.replace(/\/$/,""):"")+"/"+(s.path?s.path.replace(/^\//,""):"");a="".concat(c.protocol,"//").concat(c.host).concat(u)}}return a}function g(t,e){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},n=arguments.length>3&&void 0!==arguments[3]?arguments[3]:M,a=arguments.length>4&&void 0!==arguments[4]?arguments[4]:M,i=v(t,e,n),c=d(i,e,n),s=o.reducerName||"",f=u(r.expire,o.cache);if(f&&n!==M){var h=n(),p=N(h,o.prefix,o.reducerName,"cache");s+="_"+f.id(t,e);var y=f.getData(p&&s&&void 0!==p[s]&&p[s]);if(void 0!==y)return Promise.resolve(y)}var b=o.fetch(i,c);return f&&a!==M&&s&&b.then((function(t){a({type:l,id:s,data:t,expire:f.expire})})),b}function m(){var t=p.pop(),e=new Error("Application abort request");return t&&t.reject(e),e}function O(t,e,r){var n=arguments.length>3&&void 0!==arguments[3]?arguments[3]:M,a=arguments.length>4&&void 0!==arguments[4]?arguments[4]:M,i=g(t,e,r,n,a),c=o.validation?i.then((function(t){return new Promise((function(e,r){return o.validation(t,(function(n){return n?r(n):e(t)}))}))})):i,s=c,u=N(o,"holder","responseHandler");return u&&(s=c&&c.then?c.then((function(t){var e=u(null,t);return void 0===e?t:e}),(function(t){return u(t)})):u(c)),s&&s.catch&&s.catch(M),s}function j(){for(var t=arguments.length,e=new Array(t),r=0;r1&&void 0!==arguments[1]?arguments[1]:[];e?o(e):y()(i?j.sync:j,null,n.concat(o))(t,r)}));else{var c=J(s,2),u=c[0],l=c[1];y()(i?j.sync:j,null,[u,l,o])(t,r)}}));return u.catch(M),u}},t};return Object.keys(w).reduce((function(t,e){return P(t,w[e],e,w)}),j)}(p,a,v,q,E),!E.virtual&&!t.reducers[j]){var F=g(),T=i.cache?{sync:!1,syncing:!1,loading:!1,data:F,cache:{},request:null}:{sync:!1,syncing:!1,loading:!1,data:F,request:null},L=i.reducer?i.reducer.bind(t):null;t.reducers[j]=function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},r=arguments.length>2?arguments[2]:void 0,n=e.actionFetch,o=e.actionSuccess,a=e.actionFail,i=e.actionReset,c=e.actionCache,u=e.actionAbort;return function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:t,l=arguments.length>1?arguments[1]:void 0,p=l.request||{};switch(l.type){case n:return f({},e,{request:p,loading:!0,error:null,syncing:!!l.syncing});case o:return f({},e,{loading:!1,sync:!0,syncing:!1,error:null,data:l.data});case a:return f({},e,{loading:!1,error:l.error,syncing:!1});case i:var y=l.mutation;return"sync"===y?f({},e,{request:null,sync:!1}):f({},t);case u:return f({},e,{request:null,loading:!1,syncing:!1,error:l.error});case c:var d=l.id,b=l.data,v=e.cache[d]?e.cache[d].expire:null,g=s(l.expire,v);return f({},e,{cache:f({},e.cache,h({},d,{expire:g,data:b}))});default:return r?r(e,l):e}}}(T,q,L)}return t.events[j]=q,t}return Object.keys(t).reduce((function(e,r){return a(e,t[r],r)}),n)}ot.transformers=W,ot.async=function t(e){for(var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,n=arguments.length,o=new Array(n>2?n-2:0),a=2;a {
35 | const { user: { data: { token }}} = getState();
36 | // Add token to header request
37 | const headers = {
38 | Accept: "application/json",
39 | "Content-Type": "application/json",
40 | };
41 | if (token) {
42 | return { headers: { ...headers, Authorization: `Bearer ${token}` } };
43 | }
44 | return { headers };
45 | });
46 | ```
47 |
--------------------------------------------------------------------------------
/docs/DOCS.md:
--------------------------------------------------------------------------------
1 | ## Documentation
2 | ### Initialization redux-api endpoint
3 |
4 | ```js
5 | import reduxApi, {transformers} from "redux-api";
6 | ```
7 |
8 | #### reduxApi(options, baseConfig)
9 | - **Description**: create endpoint
10 | - **Param** **options** - configuration of rest-api endpoints
11 | - **Type**: Object
12 | - **Default**: {}
13 | - **Example**:
14 | Simple endpoint definition `GET /api/v1/entry` where response is Object
15 | ```js
16 | {
17 | entry: "/api/v1/entry",
18 | }
19 | // equivalent
20 | {
21 | entry: {
22 | url: "/api/v1/entry"
23 | }
24 | }
25 | // equivalent
26 | {
27 | entry: {
28 | url: "/api/v1/entry",
29 | transformer: transformers.object, //it's default value
30 | options: {} //it's default value
31 | }
32 | }
33 | // equivalent
34 | {
35 | entry: {
36 | url: "/api/v1/entry",
37 | transformer: transformers.object, //it's default value
38 | options: function(url, params, getState) { //it's default value
39 | return {};
40 | }
41 | }
42 | }
43 | ```
44 | **Param** **baseConfig** - additional base configuration
45 | **Param** baseConfig.prefix - custom prefix for ACTIONS if you use more then 1 restApi instance
46 | **Type**: String
47 | **Default**: ""
48 |
49 |
50 | ### Configuration options
51 | #### url
52 | - **Description**: url endpoint
53 | - **Type**: String
54 | - **Example**:
55 | ```js
56 | {
57 | entry: {
58 | url: "/api/v1/entry"
59 | }
60 | }
61 | ```
62 |
63 | #### urlOptions
64 | - **Description**: options for transforming urls
65 | - **Type**: Object
66 | - **Example**: Keys `delimiter` and `arrayFormat` are passed on to
67 | [qs#parse](https://github.com/ljharb/qs#parsing-objects) and
68 | [qs#stringify](https://github.com/ljharb/qs#stringifying):
69 | ```js
70 | {
71 | entry: {
72 | url: "/api/v1/entry",
73 | urlOptions: {
74 | delimiter: ";",
75 | arrayFormat: "brackets"
76 | }
77 | }
78 | }
79 | ```
80 | To pass different options to `#parse` and `#stringify`, use the `qsParseOptions` and `qsStringifyOptions` keys:
81 | ```js
82 | {
83 | entry: {
84 | url: "/api/v1/entry?a[]=5,a[]=6",
85 | urlOptions: {
86 | arrayFormat: "brackets",
87 | qsParseOptions: {
88 | delimiter: /[,;]/
89 | },
90 | qsStringifyOptions: {
91 | delimiter: ";"
92 | }
93 | }
94 | }
95 | }
96 | ```
97 | This would re-encode the url to `/api/v1/entry?a[]=5;a[]=6`.
98 |
99 | #### transformer
100 | - **Description**: function for rest response transformation
101 | - **Type**: Function
102 | - **Default**: transformers.object
103 | - **Example**: It's a good idea to write custom transformer
104 | for example you have response
105 | ```json
106 | { "title": "Hello", "message": "World" }
107 | ```
108 | Custom transformer
109 | ```js
110 | function customTransformer(data, prevData, action) {
111 | data || (data = {});
112 | return { title: (data.title || ""), message: (data.message || "")};
113 | }
114 | ```
115 |
116 | #### options
117 | - **Description**: options for rest-api backend. `function(url, options)`
118 | - **Type**: Object | Functions
119 | - **Default**: null
120 | - **Example**: if you use [isomorphic-fetch](https://www.npmjs.com/package/isomorphic-fetch) backend
121 | ```js
122 | options: {
123 | method: "post",
124 | headers: {
125 | "Accept": "application/json",
126 | "Content-Type": "application/json"
127 | }
128 | }
129 | // equivalent
130 | options: function() {
131 | return {
132 | method: "post",
133 | headers: {
134 | "Accept": "application/json",
135 | "Content-Type": "application/json"
136 | }
137 | };
138 | }
139 | ```
140 |
141 | #### cache
142 | - **Description**: cache response. By default cache is turn off. If cache = true - this means that cache is permanent. Also cache can be object. see example
143 | - **Type** Boolean, Object, null
144 | - **Default**: null
145 | - **Example**:
146 | ```js
147 | {
148 | permanent: {
149 | url: "/api/v1/permanent",
150 | cache: true
151 | },
152 | expires1: {
153 | url: "/api/v1/expires/1",
154 | cache: { expire: 360 }, // 360 seconds
155 | },
156 | expires2: {
157 | url: "/api/v1/expires/2",
158 | cache: {
159 | expire: new Date("...."), // use concrete Date
160 | id(params, params) {
161 | // here you can overwrite cache id for request
162 | return `you custom id for request`;
163 | }
164 | }
165 | }
166 | }
167 | ```
168 |
169 | #### broadcast
170 | - @deprecated
171 | - **Description**: list of actions which would emit after data fetching.
172 | - **Type**: Array
173 | - **Default**: null
174 | - **Example**:
175 | ```js
176 | import {ACTION_ENTRY_UPDATE} from "./constants";
177 | ....
178 | entry: {
179 | url: "/api/v1/entry",
180 | broadcast: [ ACTION_ENTRY_UPDATE ]
181 | }
182 | // in your redux reducer
183 | function (state, action) {
184 | switch (action.type) {
185 | case ACTION_ENTRY_UPDATE:
186 | return {
187 | ...state,
188 | data: action.data // fetching data
189 | };
190 | default:
191 | return state;
192 | }
193 | }
194 | ```
195 |
196 | #### reducer
197 | - **Description**: Define your custom reducer to catch other events and modify state of current entry
198 | ATTENTION: custom reducer can't catch default events for current entry.
199 | - **Type**: Function
200 | - **Default**: null
201 | - **Example**:
202 | ```js
203 | const rest = reduxApi({
204 | hello: "/api/hello",
205 | item: {
206 | url: "/api/item",
207 | reducer(state, action) {
208 | /*
209 | ATTENTION: this.events.item.actionSuccess and other default redux-api events never catch there
210 | */
211 | // context has instance
212 | if (action.type === "MY_CUSTOM_EVENT") {
213 | return { ...state, value: action.value };
214 | } else if (action.type === this.events.hello.actionSuccess) {
215 | return { ...state, value: action.value };
216 | } else {
217 | return state;
218 | }
219 | }
220 | }
221 | });
222 | ```
223 |
224 | #### virtual
225 | - **Description**: if virtual is `true` this endpoint doesn't create reducer and doesn't emit redux-api actions. All data broadcasting by actions from `broadcast` list.
226 | - **Type**: Array
227 | - **Default**: false
228 | - **Example**:
229 | It usefull, for example, when you need to manipulate list of items. But you don't want to persist information about each manipulation, you want to save it in list.
230 | ```js
231 | const rest = reduxApi({
232 | items: "/api/items",
233 | item: {
234 | url: "/api/item/:id",
235 | virtual: true, //reducer in this case doesn't generate
236 | postfetch: [
237 | function({ dispatch, actions }) {
238 | dispatch(actions.items()); // update list of items after modify any item
239 | }
240 | ]
241 | }
242 | });
243 | ```
244 | In this case you global state is look like this:
245 | ```js
246 | { items: [ ... ] }
247 | ```
248 |
249 | #### prefetch
250 | - **Description**: you can organize chain of calling events before the current endpoint will be executed
251 | - **Type**: Array
252 | - **Default**: null
253 | - **Example**:
254 |
255 | ```js
256 | {
257 | user: "/user/info",
258 | profile: "/user/:name",
259 | changeName: {
260 | url: "/user/changename",
261 | prefetch: [
262 | function({actions, dispatch, getState}, cb) {
263 | const {user: {data: {name}}} = getState();
264 | name ? cb() : dispatch(actions.user(cb));
265 | },
266 | function({actions, dispatch, getState}, cb) {
267 | const {user: {data: {name}}, profile: {data: {uuid}}} = getState();
268 | uuid ? cb() : dispatch(actions.profile({name}, cb));
269 | }
270 | ],
271 | options: function(url, params, getState) {
272 | const {profile: {data: {uuid}}} = getState();
273 | return { ...params, body: { ...params.body, uuid }};
274 | }
275 | },
276 | friends: {
277 | url: "/user/:name/friends",
278 | prefetch: [
279 | function({actions, dispatch, getState, requestOptions}, cb) {
280 | const {profile: {data: {uuid}}} = getState();
281 | const {pathvars: {name}} = requestOptions;
282 | uuid ? cb() : dispatch(actions.profile({name}, cb));
283 | }
284 | ],
285 | options: function(url, params, getState) {
286 | const {profile: {data: {uuid}}} = getState();
287 | return { ...params, body: { ...params.body, uuid }};
288 | }
289 | }
290 | }
291 | ```
292 |
293 | #### postfetch
294 | - **Description**: you can organize chain of calling events after the current endpoint will be successful executed
295 | - **Type**: Array
296 | - **Default**: null
297 | - **Example**:
298 | ```js
299 | {
300 | user: "/user/info",
301 | logout: {
302 | url: "/user/logout",
303 | postfetch: [
304 | function({data, actions, dispatch, getState, request}) {
305 | dispatch(actions.user.reset());
306 | }
307 | ]
308 | }
309 | }
310 | ```
311 |
312 | #### validation (data, callback)
313 | - **Param** **data** - response data
314 | > type: Object
315 |
316 | - **Param** **callback** - you need to execute this callback function to finish data validation
317 | > type: Function
318 |
319 | - **Example**:
320 | ```js
321 | {
322 | test: {
323 | url: "/api/test",
324 | validation: (data, cb) {
325 | // check data format
326 | let error;
327 | if (data instanceOf Array) {
328 | error = "Data must be array";
329 | }
330 | cb(error);
331 | }
332 | }
333 | }
334 | ```
335 |
336 | #### reducerName
337 | - **Description**: Sometimes though, you might want named actions that go back to the same reducer. For example:
338 | - **Type**: String
339 | - **Example**:
340 | ```js
341 | import reduxApi, {transformers} from "redux-api";
342 | const rest = reduxApi({
343 | getUser: {
344 | reducerName: "user"
345 | url: "/user/1", // return a user object
346 | }
347 | updateUser: {
348 | reducerName: "user"
349 | url: "/user/1/update",
350 | options: {
351 | method: "post"
352 | }
353 | }
354 | });
355 | const {actions} = rest;
356 |
357 | // In component with redux support (see example section)
358 | const {dispatch} = this.props;
359 | dispatch(rest.actions.getUser()); // GET "/api/v1/entry"
360 | dispatch(rest.actions.updateUser({}, {
361 | body: JSON.stringify({ name: "Hubot", login: "hubot"})
362 | })); // POST "/api/v1/entry/1" with body
363 |
364 | ```
365 | In the above example, both getUser, and updateUser update the same user reducer as they share the same reducerName
366 | For example used es7 javascript
367 |
368 | #### helpers
369 | - **Description**: you can create custom helper function which work with this rest endpoint but with different parameters.
370 | - **Type**: Object
371 | - **Example**:
372 | ```js
373 | {
374 | logger: "/api/logger",
375 | test: {
376 | url: "/api/test/:name/:id",
377 | helpers: {
378 | get(id, name) {
379 | return [{id, name}], {}]
380 | },
381 | post(id, name, data) {
382 | const {uuid} = this.getState().test;
383 | const urlparams = {id, name};
384 | const params = {body: {uuid, data}};
385 | return [urlparams, params];
386 | },
387 | // complicated async logic
388 | async() {
389 | const {dispatch} = this;
390 | return (cb)=> {
391 | dispatch(rest.actions.logger((err)=> {
392 | const args = [{id: 1, name: "admin"}];
393 | cb(err, args);
394 | }));
395 | };
396 | }
397 | }
398 | }
399 | }
400 | // using helpers
401 | rest.actions.test.get(1, "admin");
402 | // with callback
403 | rest.actions.test.post(1, "admin", {msg: "Hello"}, (err)=> {
404 | // end of action
405 | });
406 | rest.actions.test.async();
407 | ```
408 |
409 | #### crud
410 | - **Description**: autogenerate `helpers` ("get", "post", "put", "delete", "patch") for selected endpoint. Also you can overwrite autogenerate action with `helpers` definitions.
411 | - **Type**: Boolean
412 | - **Default**: false
413 | - **Example**:
414 | ```js
415 | {
416 | test: {
417 | url: "/test/:id",
418 | crud: true
419 | }
420 | }
421 |
422 | //using
423 | rest.actions.test.get({ id: 1})
424 | rest.actions.test.post({ id: 1}, { body: "data" }, (err, data)=> {
425 | //code
426 | });
427 | rest.actions.test.put({ id: 1}, { body: "data" })
428 | rest.actions.test.delete({ id: 1 });
429 | ```
430 |
431 | ### reduxApi object
432 |
433 | #### use(key, value)
434 | - **Description**: initialize `reduxApi` with custom properties
435 | - **Param** **key** - name of property
436 | - **Param** **value** - value of property
437 |
438 | #### list of properties
439 | #### fetch
440 | - **Description**: backend adapter. In current example we use `adaptersFetch` adapter for rest backend using `fetch` API for rest [isomorphic-fetch](https://www.npmjs.com/package/isomorphic-fetch)
441 | - **Example**:
442 | ```js
443 | import adapterFetch from "redux-api/lib/adapters/fetch";
444 | const rest = reduxApi({...});
445 | rest.use("fetch", adapterFetch(fetch));
446 | ```
447 |
448 | #### server
449 | - **Description**: redux api is isomorphic compatible see [examples/isomorphic](https://github.com/lexich/redux-api/tree/master/examples/isomorphic) By default `server===false` for client-size mode. If `server===true` redux-api works in server-size mode.
450 | - **Default** false
451 | ```js
452 | const rest = reduxApi({...});
453 | rest.use("server", true);
454 | ```
455 |
456 | #### rootUrl
457 | - **Description**: root url for every endpoint. very usefull for isomorphic(universal) app. For client-side use default rootUrl, and for backend use http://localhost:80 for example. For client-side for request `/api/get` will be `/api/get` and for backend will be `http://localhost:80/api/get`
458 | - **Type**: String | Functions
459 | - **Example**:
460 | ```js
461 | const rest = reduxApi({...});
462 | rest.use("rootUrl", "http://localhost:3000");
463 | ```
464 |
465 | Or a function
466 | ```js
467 | const rest = reduxApi({...});
468 | rest.use("rootUrl", function(url, params, getState) {
469 | return getState().config.rootUrl;
470 | });
471 | ```
472 |
473 | #### options
474 | - **Description**: Apply add options for each rest call.
475 | - **Type**: String | Functions
476 | - **Example**:
477 | ```js
478 | const rest = reduxApi({...});
479 | rest.use("options", function() {
480 | const headers = {
481 | 'User-Agent': 'foodsoft-shop', // @todo add version
482 | 'Accept': 'application/json'
483 | };
484 | return { headers: headers };
485 | });
486 | ```
487 |
488 | Or a function
489 | ```js
490 | const rest = reduxApi({...});
491 | rest.use("options", function(url, params getState) {
492 | return {
493 | headers: {
494 | 'X-Token': getState().user.accessToken
495 | }
496 | };
497 | });
498 | ```
499 |
500 | #### middlewareParser
501 | - **Description**: if you use middleware different from [redux-thunk](https://github.com/gaearon/redux-thunk) you can realize custom behaviour for argument parser.
502 | - **Example**:
503 | ```js
504 | // Custom middleware
505 | const cutsomThunkMiddleware = ({ dispatch, getState }) => next => action => {
506 | if (typeof action === 'function') {
507 | return action({ dispatch, getState });
508 | }
509 | return next(action);
510 | };
511 |
512 | // middlewareParser
513 | reduxApi({ ... }).use("middlewareParser",
514 | ({ dispatch, getState })=> {
515 | return { getState, dispatch };
516 | });
517 | ```
518 |
519 | #### responseHandler
520 | - **Description**: catch all http response from each redux-api endpoint. First argument is `Error` is response fail, second argument data from success response. It can be used for logging, error handling or data transformation.
521 | - **Example**:
522 | ```js
523 | reduxApi({ ... }).use("responseHandler",
524 | (err, data)=>
525 | err ? console.log("ERROR", err) : console.log("SUCCESS", data));
526 | ```
527 | ```js
528 | reduxApi({ ... }).use("responseHandler",
529 | (err, data)=> {
530 | if (err.message === 'Not allowed') {
531 | throw new NotAllowedError();
532 | } else {
533 | return data;
534 | }
535 | });
536 | ```
537 |
538 | #### init(adapter, isServer, rootUrl)
539 | - @deprecated
540 | - **Description**: `reduxApi` initializer returns not initialized object. You need to call `init` for initialize it.
541 | - **Type**: Function
542 | - **Param** **adapter** - backend adapter. In current example we use `adaptersFetch` adapter for rest backend using `fetch` API for rest [isomorphic-fetch](https://www.npmjs.com/package/isomorphic-fetch)
543 | - **Param** **isServer** - redux api is isomorphic compatible see [examples/isomorphic](https://github.com/lexich/redux-api/tree/master/examples/isomorphic) By default `isServer===false` for client-size mode. If `isServer===true` redux-api works in server-size mode.
544 | - **Param** **rootUrl** - root url for every endpoint. very usefull for isomorphic(universal) app. For client-side use default rootUrl, and for backend use http://localhost:80 for example. For client-side for request `/api/get` will be `/api/get` and for backend will be `http://localhost:80/api/get`.
545 | - **Example**:
546 |
547 | ```js
548 | import "isomorphic-fetch";
549 | import reduxApi from "redux-api";
550 | import adapterFetch from "redux-api/lib/adapters/fetch";
551 | const rest = reduxApi({
552 | ... //config
553 | });
554 | rest.init(adapterFetch(fetch), false, "http://localhost:3000");
555 | ```
556 |
557 | #### actions
558 | - **Description**: list of redux actions for rest manipulations
559 | - **Type**: Object
560 | - **Example**:
561 | ```js
562 | const rest = reduxApi({
563 | entries: "/api/v1/entry",
564 | entry: {
565 | url: "/api/v1/entry/:id",
566 | options: {
567 | method: "post"
568 | }
569 | }
570 | });
571 | // ....
572 | const {actions} = rest;
573 | /*
574 | initialState for store
575 | store = {
576 | entries: {
577 | loading: false, // request finish flag
578 | sync: false, // data has loaded minimum once
579 | data: {} // data
580 | },
581 | entry: { loading: false, sync: false, data: {} },
582 | }
583 | */
584 |
585 | // In component with redux support (see example section)
586 | const {dispatch} = this.props;
587 | dispatch(rest.actions.entries()); // GET "/api/v1/entry"
588 | dispatch(rest.actions.entry({id: 1}, {
589 | body: JSON.stringify({ name: "Hubot", login: "hubot"
590 | }})); // POST "/api/v1/entry/1" with body
591 | dispatch(rest.actions.entries.reset());
592 | dispatch(rest.actions.entries.sync());
593 | ```
594 |
595 | ### Actions sub methods
596 |
597 | #### sync(urlparams, params, callback)
598 | - **Description**: this method save you from twice requests flag `sync`. if `sync === true` request wouldn't execute. In server-side mode calls twice
599 | - **Param** **urlparams** - update url according Url schema
600 | - **Param** **params** - add additional params to rest request
601 | - **Param** **callback** - callback function when action ends
602 | - **Type**: Function
603 | - **Example**:
604 |
605 | ```js
606 | import {actions} from "./rest";
607 | function onEnter(state, replaceState, callback) {
608 | dispatch(rest.actions.entries.sync(callback));
609 | }
610 |
611 | ```
612 |
613 | ### abort()
614 | - **Description**: abort loading request
615 | - **Type**: null
616 | - **Example**:
617 | ```js
618 | import {actions} from "./rest";
619 | dispatch(actions.entries({ id: 1 }))
620 | actions.entries.abort() // abort previous request
621 | dispatch(actions.entries({ id: 2 }))
622 | ```
623 |
624 | ### force(urlparams, params, callback)
625 | - **Description**: abort previous request if it performs and after that perform new request. This method combines `abort` and direct call action methods.
626 | - **Type**: Function
627 | - **Example**:
628 | ```
629 | import {actions} from "./rest";
630 | dispatch(actions.entries({ id: 1 }))
631 | dispatch(actions.entries.force({ id: 2 }))
632 | ```
633 |
634 | #### reset(mutation)
635 | - **Description**: Reset state of current reducer and application abort request if it processed.
636 | - **Type**: Function
637 | - **Param** mutation: if `mutation` equal `sync`, it reset only `sync` flag in store.
638 | - **Example**:
639 | ```js
640 | import {actions} from "./rest";
641 | function onLeave(state, replaceState, cb) {
642 | dispatch(actions.entries.reset(cb));
643 | }
644 |
645 | ```
646 |
647 | #### request()
648 | - **Description**: Pure xhr request is without sending events or catching reducers.
649 | - **Type**: Function
650 | - **Example**:
651 | ```js
652 | import {actions} from "./rest";
653 | actions.entries.request().then((data)=> {
654 | ....
655 | });
656 | ```
657 |
658 | ### Url schema
659 | /api/v1/user/:id
660 | ```js
661 | rest.actions.user({id: 1}) // /api/v1/user/1
662 | ```
663 |
664 | /api/v1/user/(:id)
665 | ```js
666 | rest.actions.user({id: 1}) // /api/v1/user/1
667 | ```
668 |
669 | /api/v1/user/(:id)
670 | ```js
671 | rest.actions.user({id: 1, test: 2}) // /api/v1/user/1?test=2
672 | ```
673 |
674 | ### Events
675 | Each endpoint in redux-api infrastructure has own collection of methods.
676 | - actionFetch - emits when endpoint's call is started
677 | - actionSuccess - emits when endpoint's call finishes with success result
678 | - actionFail - emits when endpoint's call finishes with error result
679 | - actionReset - emits when reset action was called
680 |
681 | you can get access for anyone using next accessible properties
682 | ```js
683 | rest.events.user.actionFetch // actionSuccess actionFail actionReset
684 | ....
685 | ```
686 | It's very useful when you need to update external reducer using information from redux-api.
687 |
688 | ```js
689 | const initialState = { access: false };
690 | function accessReducer(state=initialState, action) {
691 | switch (action.type) {
692 | case UPDATE_ACCESS: // manual update
693 | return { ...state, access: action.access };
694 | case rest.events.user.actionSuccess: // user has own information about access
695 | return { ...state, access: action.data.access };
696 | default:
697 | state;
698 | }
699 | }
700 | ```
701 |
702 | ### Tools
703 | #### async
704 | - **Description**: helps to organize chain call of actions
705 | - **Example**:
706 | ```js
707 | import reduxApi, { async } from "redux-api";
708 | const rest = reduxApi({
709 | test: "/api/test",
710 | test2: "/api/test2",
711 | test3: "/api/test3"
712 | });
713 | async(dispatch,
714 | (cb)=> rest.actions.test(cb),
715 | rest.actions.test2
716 | ).then((data)=> async(rest.actions.test3));
717 | ```
718 |
719 | ### Store state schema
720 | ```js
721 | import reduxApi from "redux-api";
722 |
723 | const rest = reduxApi({
724 | user: "/user/1"
725 | });
726 | ```
727 | In the above example, an endpoint for a user object is created. The corresponding initial store state for this object is the following:
728 |
729 | ```js
730 | // initialState
731 | {
732 | user: {
733 | sync: false, // State was update once
734 | syncing: false, // State syncing is in progress
735 | loading: false, // State updating is in progress
736 | error: null, // response error
737 | data: [] // response data
738 | }
739 | }
740 | ```
741 | - The `sync` flag will be set to `true` after the first dispatched request is handled.
742 | - The `syncing` flag is set to `true` while a dispatched sync request is being handled, but only when the `sync` flag is `false`. This can only happen once when not manually resetting the `sync` flag during execution, since the `sync` flag is set to `true` after the first dispatched request is handled.
743 | - The `loading` flag is set to `true` while a dispatched request is being handled. After the dispatched request is handled, its value will be reset to `false`.
744 | - The `error` property contains the response error of a dispatched request after it is handled.
745 | - The `data` property contains the response data of a dispatched request after it is handled.
746 |
--------------------------------------------------------------------------------
/docs/Scoping.md:
--------------------------------------------------------------------------------
1 | If you want use multipoint redux-api or separate your reducers by scope.
2 | ```js
3 | const rest1 = reduxApi({
4 | test: "/test1",
5 | }, { prefix: "r1" }); // <-- important to scoping
6 |
7 | const rest2 = reduxApi({
8 | test: "/test2"
9 | }, { prefix: "r2" }); // <-- important to scoping
10 |
11 | const reducer = combineReducers({
12 | r1: combineReducers(rest1.reducers), // <-- the same scope (r1)
13 | r2: combineReducers(rest2.reducers), // <-- as in prefix (r2)
14 | r3: combineReducers(myReducers)
15 | });
16 |
17 | const createStoreWithMiddleware = applyMiddleware(thunk)(createStore);
18 | const store = createStoreWithMiddleware(reducer);
19 | // etc
20 |
21 | // You should remember that scoping modify structure of your state
22 | // state of rest1 is
23 | const dataOfRest1Test = store.getState().r1.test.data;
24 | const dataOfRest2Test = store.getState().r2.test.data;
25 | const other = store.getState().r3.other;
26 | ```
27 |
--------------------------------------------------------------------------------
/examples/isomorphic/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "stage-3", "react"]
3 | }
4 |
--------------------------------------------------------------------------------
/examples/isomorphic/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /dist/main.js
3 |
--------------------------------------------------------------------------------
/examples/isomorphic/README.md:
--------------------------------------------------------------------------------
1 | # Isomorphic example
2 | React + Redux + React-Router + Redux-api with webpack and express + github api
3 |
4 | Run server
5 | ```sh
6 | npm install
7 | npm start
8 | ```
9 |
10 | Point your browser to http://localhost:4444/
11 |
12 |
--------------------------------------------------------------------------------
/examples/isomorphic/app/client.jsx:
--------------------------------------------------------------------------------
1 | "use strict";
2 | import React from "react";
3 | import { render } from "react-dom";
4 |
5 | // React-Router
6 | import Router from "react-router";
7 | import { createHistory } from "history";
8 | import routes from "./routes/routes";
9 |
10 | // Redux
11 | import { createStore, applyMiddleware, combineReducers, compose } from "redux";
12 | import thunk from "redux-thunk";
13 | import { Provider } from "react-redux";
14 |
15 | // Redux-api
16 | import reduxApi from "./utils/rest";
17 | import adapterFetch from "redux-api/lib/adapters/fetch";
18 | import "isomorphic-fetch";
19 |
20 | // Initialize react-api
21 | reduxApi.use("fetch", adapterFetch(fetch));
22 |
23 | // Prepare store
24 | const reducer = combineReducers(reduxApi.reducers);
25 | const finalCreateStore = applyMiddleware(thunk)(createStore);
26 | const initialState = window.$REDUX_STATE;
27 | const store = initialState
28 | ? finalCreateStore(reducer, initialState)
29 | : finalCreateStore(reducer);
30 | delete window.$REDUX_STATE;
31 |
32 | const childRoutes = routes(store);
33 | const history = createHistory();
34 | const el = document.getElementById("react-main-mount");
35 |
36 | render(
37 |
38 |
39 | ,
40 | el
41 | );
42 |
--------------------------------------------------------------------------------
/examples/isomorphic/app/pages/Application.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default class Application extends React.Component {
4 | render() {
5 | return (
6 |
7 |
8 |
9 |
10 | Isomorphic Redux-api example with react-router
11 |
12 |
13 |
{this.props.children}
14 |
15 |
16 | );
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/examples/isomorphic/app/pages/Repo.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from "react";
2 | import { connect } from "react-redux";
3 | import { Link } from "react-router";
4 |
5 | class Repo extends React.Component {
6 | render() {
7 | const { repo } = this.props;
8 | const data = repo.data || {};
9 | const owner = data.owner || {};
10 | return !repo.loading ? (
11 |
12 |
13 |
14 |

15 |
16 |
17 |
18 |
{data.name}
19 | @{owner.login} Stars{" "}
20 | {data.stargazers_count} Forks {data.forks}
21 |
22 |
23 | ) : (
24 | Loading
25 | );
26 | }
27 | }
28 |
29 | Repo.propTypes = {
30 | repo: PropTypes.object.isRequired,
31 | dispatch: PropTypes.func.isRequired
32 | };
33 |
34 | function select(state) {
35 | return { repo: state.repo };
36 | }
37 |
38 | export default connect(select)(Repo);
39 |
--------------------------------------------------------------------------------
/examples/isomorphic/app/pages/User.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from "react";
2 | import { connect } from "react-redux";
3 | import { Link } from "react-router";
4 |
5 | class User extends React.Component {
6 | render() {
7 | const { userRepos } = this.props;
8 | const Repos = userRepos.data.map(item => (
9 |
14 | {item.name}
15 |
16 | ));
17 | return {Repos}
;
18 | }
19 | }
20 |
21 | User.propTypes = {
22 | userRepos: PropTypes.object.isRequired,
23 | dispatch: PropTypes.func.isRequired
24 | };
25 |
26 | function select(state) {
27 | return { userRepos: state.userRepos };
28 | }
29 |
30 | export default connect(select)(User);
31 |
--------------------------------------------------------------------------------
/examples/isomorphic/app/routes/routes.js:
--------------------------------------------------------------------------------
1 | import Application from "../pages/Application";
2 | import User from "../pages/User";
3 | import Repo from "../pages/Repo";
4 | import rest from "../utils/rest";
5 |
6 | const { actions } = rest;
7 |
8 | export default function routes({ dispatch }) {
9 | return {
10 | path: "/",
11 | component: Application,
12 | indexRoute: {
13 | path: "/",
14 | onEnter(state, replaceState) {
15 | replaceState(state, "/lexich");
16 | }
17 | },
18 | childRoutes: [
19 | {
20 | path: "/:user",
21 | component: User,
22 | onEnter(state, replaceState, callback) {
23 | const { user } = state.params;
24 | dispatch(actions.userRepos({ user }, null, callback));
25 | }
26 | },
27 | {
28 | path: "/:user/:repo",
29 | component: Repo,
30 | onEnter(state, replaceState, callback) {
31 | const { user, repo } = state.params;
32 | dispatch(actions.repo({ user, repo }, null, callback));
33 | }
34 | }
35 | ]
36 | };
37 | }
38 |
--------------------------------------------------------------------------------
/examples/isomorphic/app/server.jsx:
--------------------------------------------------------------------------------
1 | "use strict";
2 | import React from "react";
3 | import ReactDom from "react-dom/server";
4 | import express from "express";
5 | import path from "path";
6 |
7 | // Redux
8 | import { applyMiddleware, createStore, combineReducers } from "redux";
9 | import { Provider } from "react-redux";
10 | import thunk from "redux-thunk";
11 |
12 | // React-router
13 | import { RoutingContext, match } from "react-router";
14 | import { createMemoryHistory as createHistory } from "history";
15 |
16 | import routes from "./routes/routes";
17 |
18 | // Redux-api
19 | import "isomorphic-fetch";
20 | import reduxApi from "./utils/rest";
21 | import adapterFetch from "redux-api/lib/adapters/fetch";
22 |
23 | reduxApi.use("fetch", adapterFetch(fetch)).use("server", true);
24 |
25 | const history = createHistory();
26 |
27 | // Init express app
28 | const app = express();
29 |
30 | // Include static assets. Not advised for production
31 | app.use(express.static(path.join(__dirname, "..", "dist")));
32 |
33 | // Set view path
34 | app.set("views", path.join(__dirname, "..", "views"));
35 |
36 | // set up ejs for templating. You can use whatever
37 | app.set("view engine", path.join(__dirname, "..", "ejs"));
38 |
39 | app.use(function(req, res, next) {
40 | const location = history.createLocation(req.url);
41 | const createStoreWithMiddleware = applyMiddleware(thunk)(createStore);
42 | const reducer = combineReducers(reduxApi.reducers);
43 | const store = createStoreWithMiddleware(reducer);
44 | const childRoutes = routes(store);
45 | match(
46 | { routes: childRoutes, location },
47 | (error, redirectLocation, renderProps) => {
48 | if (redirectLocation) {
49 | res
50 | .status(301)
51 | .redirect(redirectLocation.pathname + redirectLocation.search);
52 | } else if (error) {
53 | res.status(500).send(error.message);
54 | } else if (renderProps === null) {
55 | res.status(404).render("404.ejs");
56 | } else {
57 | const html = ReactDom.renderToString(
58 |
59 |
60 |
61 | );
62 | res.render("index.ejs", {
63 | html,
64 | json: JSON.stringify(store.getState())
65 | });
66 | }
67 | }
68 | );
69 | });
70 |
71 | const server = app.listen(4444, function() {
72 | const { port } = server.address();
73 | console.log("Server started at http://localhost:%s", port);
74 | });
75 |
--------------------------------------------------------------------------------
/examples/isomorphic/app/utils/rest.js:
--------------------------------------------------------------------------------
1 | /* eslint import/no-unresolved: 0 */
2 | /* eslint import/extensions: 0 */
3 | import reduxApi from "redux-api";
4 | import map from "lodash/map";
5 |
6 | const headers = {
7 | "User-Agent": "redux-api"
8 | };
9 | const URL = "https://api.github.com";
10 |
11 | export default reduxApi({
12 | userRepos: {
13 | url: `${URL}/users/:user/repos`,
14 | options: { headers },
15 | cache: { expire: 5000 },
16 | transformer(data) {
17 | return map(data, item => {
18 | return {
19 | name: item.name,
20 | fullName: item.full_name,
21 | user: item.owner.login
22 | };
23 | });
24 | }
25 | },
26 | repo: {
27 | url: `${URL}/repos/:user/:repo`,
28 | options: { headers },
29 | cache: {
30 | expire: 5000
31 | }
32 | }
33 | });
34 |
--------------------------------------------------------------------------------
/examples/isomorphic/dist/404.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lexich/redux-api/e92b171dda6b1586777a0caacb0308b9f7d5039a/examples/isomorphic/dist/404.jpg
--------------------------------------------------------------------------------
/examples/isomorphic/dist/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lexich/redux-api/e92b171dda6b1586777a0caacb0308b9f7d5039a/examples/isomorphic/dist/favicon.ico
--------------------------------------------------------------------------------
/examples/isomorphic/dist/styles.css:
--------------------------------------------------------------------------------
1 | .Repo__media{
2 | max-width: 100px;
3 | }
4 |
--------------------------------------------------------------------------------
/examples/isomorphic/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "examples",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node_modules/.bin/webpack --process --watch & node_modules/.bin/pm2-dev start server.js",
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "author": {
11 | "name": "Efremov Alex",
12 | "email": "lexich121@gmail.com",
13 | "url": "https://github.com/lexich"
14 | },
15 | "license": "MIT",
16 | "dependencies": {
17 | "ejs": "^2.3.4",
18 | "express": "^4.13.3",
19 | "history": "^1.17.0",
20 | "isomorphic-fetch": "^2.2.0",
21 | "lodash": "^4.11.1",
22 | "react": "^0.14.3",
23 | "react-dom": "^0.14.3",
24 | "react-redux": "^4.0.2",
25 | "react-router": "^1.0.2",
26 | "redux": "^3.0.5",
27 | "redux-api": "^0.9.3",
28 | "redux-thunk": "^1.0.2",
29 | "babel-core": "^6.3.21"
30 | },
31 | "devDependencies": {
32 | "babel-loader": "^6.2.0",
33 | "babel-plugin-add-module-exports": "^0.1.2",
34 | "babel-preset-es2015": "^6.3.13",
35 | "babel-preset-react": "^6.3.13",
36 | "babel-preset-stage-3": "^6.3.13",
37 | "pm2": "^0.15.10",
38 | "webpack": "^1.12.9"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/examples/isomorphic/server.js:
--------------------------------------------------------------------------------
1 | require("babel-core/register")({
2 | ignore: /node_modules/
3 | });
4 |
5 | require("./app/server");
6 |
--------------------------------------------------------------------------------
/examples/isomorphic/views/404.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | React Isomorphic Server Side Rendering Example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | 404 Not found
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/examples/isomorphic/views/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | React Isomorphic Server Side Rendering Example
5 |
6 |
7 |
8 |
9 |
10 | <%- html %>
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/examples/isomorphic/webpack.config.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var webpack = require("webpack");
4 | var path = require("path");
5 |
6 | var plugins = [
7 | new webpack.DefinePlugin({
8 | "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV)
9 | }),
10 | new webpack.optimize.OccurenceOrderPlugin()
11 | ];
12 |
13 | if (process.env.NODE_ENV === "production") {
14 | plugins.push(
15 | new webpack.optimize.UglifyJsPlugin({
16 | compressor: {
17 | screw_ie8: true,
18 | warnings: false
19 | }
20 | })
21 | );
22 | }
23 |
24 | module.exports = {
25 | module: {
26 | loaders: [
27 | { test: /\.(js|jsx)$/, loaders: ["babel-loader"], exclude: /node_modules/ }
28 | ]
29 | },
30 | entry: {
31 | main: "./app/client.jsx"
32 | },
33 | output: {
34 | path: "dist",
35 | filename: "main.js"
36 | },
37 | debug: true,
38 | devtool: "eval-source-map",
39 | plugins: plugins,
40 | resolve: {
41 | // Alias redux-api for using version from source instead of npm
42 | alias: {
43 | "redux-api/lib": path.resolve(
44 | path.join(__dirname, "..", "..", "src")
45 | ),
46 | "redux-api": path.resolve(
47 | path.join(__dirname, "..", "..", "src")
48 | )
49 | },
50 | extensions: ["", ".js", ".jsx"]
51 | }
52 | };
53 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-api",
3 | "version": "0.12.0",
4 | "author": {
5 | "name": "Efremov Alex",
6 | "email": "lexich121@gmail.com",
7 | "url": "https://github.com/lexich"
8 | },
9 | "main": "lib/index.js",
10 | "license": "MIT",
11 | "description": "Flux REST API for redux infrastructure",
12 | "repository": "http://github.com/lexich/redux-api",
13 | "scripts": {
14 | "test": "npm run eslint && npm run mocha && npm run yaspeller",
15 | "yaspeller": "node_modules/.bin/yaspeller .",
16 | "mocha": "node_modules/.bin/mocha --require @babel/register test/*_spec.js",
17 | "build": "rm -rf dist lib && npm run browser-dev && npm run browser-min && npm run compile",
18 | "cover": "./node_modules/.bin/nyc npm run mocha",
19 | "coveralls": "nyc report --reporter=text-lcov | coveralls",
20 | "eslint": "node_modules/.bin/eslint src test examples/isomorphic/app examples/isomorphic/server.js",
21 | "compile": "node_modules/.bin/babel src --out-dir lib --source-maps true",
22 | "prettier": "prettier --write \"{src,test,examples/isomorphic/app}/**/*.{js,jsx}\"",
23 | "browser-dev": "cross-env NODE_ENV=development node_modules/.bin/webpack",
24 | "browser-min": "cross-env NODE_ENV=production node_modules/.bin/webpack",
25 | "release": "node_modules/.bin/standard-version --no-verify && git push --follow-tags origin master; npm publish",
26 | "precommit": "npm run prettier && npm test",
27 | "prepush": "npm test && npm run build"
28 | },
29 | "dependencies": {
30 | "fast-apply": "0.0.3",
31 | "qs": "^6.9.1"
32 | },
33 | "devDependencies": {
34 | "@babel/cli": "^7.8.4",
35 | "@babel/core": "^7.8.4",
36 | "@babel/preset-env": "^7.8.4",
37 | "@babel/preset-react": "^7.8.3",
38 | "@babel/register": "^7.8.3",
39 | "babel-eslint": "10.0.3",
40 | "babel-loader": "8.0.6",
41 | "babel-plugin-add-module-exports": "1.0.2",
42 | "babel-plugin-syntax-dynamic-import": "^6.18.0",
43 | "chai": "4.2.0",
44 | "coveralls": "3.0.9",
45 | "cross-env": "7.0.0",
46 | "eslint": "6.8.0",
47 | "eslint-config-airbnb": "18.0.1",
48 | "eslint-config-prettier": "6.10.0",
49 | "eslint-plugin-import": "2.20.1",
50 | "eslint-plugin-jsx-a11y": "6.2.3",
51 | "eslint-plugin-react": "7.18.3",
52 | "husky": "4.2.3",
53 | "lodash": "4.17.15",
54 | "mocha": "7.0.1",
55 | "nyc": "^15.0.0",
56 | "prettier": "1.19.1",
57 | "redux": "4.0.5",
58 | "redux-thunk": "2.3.0",
59 | "standard-version": "8.0.1",
60 | "webpack": "4.41.6",
61 | "webpack-cli": "^3.3.11",
62 | "yaspeller": "6.0.2"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/PubSub.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | export default class PubSub {
4 | constructor() {
5 | this.container = [];
6 | }
7 |
8 | push(cb) {
9 | cb instanceof Function && this.container.push(cb);
10 | }
11 |
12 | resolve(data) {
13 | const container = this.container;
14 | this.container = [];
15 | container.forEach(cb => cb(null, data));
16 | }
17 |
18 | reject(err) {
19 | const container = this.container;
20 | this.container = [];
21 | container.forEach(cb => cb(err));
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/actionFn.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import fastApply from "fast-apply";
4 | import libUrl from "url";
5 | import urlTransform from "./urlTransform";
6 | import merge from "./utils/merge";
7 | import get from "./utils/get";
8 | import fetchResolver from "./fetchResolver";
9 | import PubSub from "./PubSub";
10 | import createHolder from "./createHolder";
11 | import {
12 | none,
13 | extractArgs,
14 | defaultMiddlewareArgsParser,
15 | CRUD
16 | } from "./helpers";
17 | import { getCacheManager } from "./utils/cache";
18 |
19 | /**
20 | * Constructor for create action
21 | * @param {String} url endpoint's url
22 | * @param {String} name action name
23 | * @param {Object} options action configuration
24 | * @param {Object} ACTIONS map of actions
25 | * @param {[type]} fetchAdapter adapter for fetching data
26 | * @return {Function+Object} action function object
27 | */
28 | export default function actionFn(url, name, options, ACTIONS = {}, meta = {}) {
29 | const {
30 | actionFetch,
31 | actionSuccess,
32 | actionFail,
33 | actionReset,
34 | actionCache,
35 | actionAbort
36 | } = ACTIONS;
37 | const pubsub = new PubSub();
38 | const requestHolder = createHolder();
39 |
40 | function getOptions(urlT, params, getState) {
41 | const globalOptions = !meta.holder
42 | ? {}
43 | : meta.holder.options instanceof Function
44 | ? meta.holder.options(urlT, params, getState)
45 | : meta.holder.options;
46 | const baseOptions = !(options instanceof Function)
47 | ? options
48 | : options(urlT, params, getState);
49 | return merge({}, globalOptions, baseOptions, params);
50 | }
51 |
52 | function getUrl(pathvars, params, getState) {
53 | const resultUrlT = urlTransform(url, pathvars, meta.urlOptions);
54 | let urlT = resultUrlT;
55 | let rootUrl = get(meta, "holder", "rootUrl");
56 | rootUrl = !(rootUrl instanceof Function)
57 | ? rootUrl
58 | : rootUrl(urlT, params, getState);
59 | if (rootUrl) {
60 | const rootUrlObject = libUrl.parse(rootUrl);
61 | const urlObject = libUrl.parse(urlT);
62 | if (!urlObject.host) {
63 | const urlPath =
64 | (rootUrlObject.path ? rootUrlObject.path.replace(/\/$/, "") : "") +
65 | "/" +
66 | (urlObject.path ? urlObject.path.replace(/^\//, "") : "");
67 | urlT = `${rootUrlObject.protocol}//${rootUrlObject.host}${urlPath}`;
68 | }
69 | }
70 | return urlT;
71 | }
72 |
73 | function fetch(
74 | pathvars,
75 | params,
76 | options = {},
77 | getState = none,
78 | dispatch = none
79 | ) {
80 | const urlT = getUrl(pathvars, params, getState);
81 | const opts = getOptions(urlT, params, getState);
82 | let id = meta.reducerName || "";
83 | const cacheManager = getCacheManager(options.expire, meta.cache);
84 |
85 | if (cacheManager && getState !== none) {
86 | const state = getState();
87 | const cache = get(state, meta.prefix, meta.reducerName, "cache");
88 | id += "_" + cacheManager.id(pathvars, params);
89 | const data = cacheManager.getData(
90 | cache && id && cache[id] !== undefined && cache[id]
91 | );
92 | if (data !== undefined) {
93 | return Promise.resolve(data);
94 | }
95 | }
96 | const response = meta.fetch(urlT, opts);
97 | if (cacheManager && dispatch !== none && id) {
98 | response.then(data => {
99 | dispatch({ type: actionCache, id, data, expire: cacheManager.expire });
100 | });
101 | }
102 | return response;
103 | }
104 |
105 | function abort() {
106 | const defer = requestHolder.pop();
107 | const err = new Error("Application abort request");
108 | defer && defer.reject(err);
109 | return err;
110 | }
111 |
112 | /**
113 | * Fetch data from server
114 | * @param {Object} pathvars path vars for url
115 | * @param {Object} params fetch params
116 | * @param {Function} getState helper meta function
117 | */
118 | function request(
119 | pathvars,
120 | params,
121 | options,
122 | getState = none,
123 | dispatch = none
124 | ) {
125 | const response = fetch(pathvars, params, options, getState, dispatch);
126 | const result = !meta.validation
127 | ? response
128 | : response.then(
129 | data =>
130 | new Promise((resolve, reject) =>
131 | meta.validation(data, err => (err ? reject(err) : resolve(data)))
132 | )
133 | );
134 | let ret = result;
135 | const responseHandler = get(meta, "holder", "responseHandler");
136 | if (responseHandler) {
137 | if (result && result.then) {
138 | ret = result.then(
139 | data => {
140 | const res = responseHandler(null, data);
141 | if (res === undefined) {
142 | return data;
143 | } else {
144 | return res;
145 | }
146 | },
147 | err => responseHandler(err)
148 | );
149 | } else {
150 | ret = responseHandler(result);
151 | }
152 | }
153 | ret && ret.catch && ret.catch(none);
154 | return ret;
155 | }
156 |
157 | /**
158 | * Fetch data from server
159 | * @param {Object} pathvars path vars for url
160 | * @param {Object} params fetch params
161 | * @param {Function} callback) callback execute after end request
162 | */
163 | function fn(...args) {
164 | const [pathvars, params, callback] = extractArgs(args);
165 | const syncing = params ? !!params.syncing : false;
166 | params && delete params.syncing;
167 | pubsub.push(callback);
168 | return (...middlewareArgs) => {
169 | const middlewareParser =
170 | get(meta, "holder", "middlewareParser") || defaultMiddlewareArgsParser;
171 | const { dispatch, getState } = middlewareParser(...middlewareArgs);
172 | const state = getState();
173 | const isLoading = get(state, meta.prefix, meta.reducerName, "loading");
174 | if (isLoading) {
175 | return Promise.reject("isLoading");
176 | }
177 | const requestOptions = { pathvars, params };
178 | const prevData = get(state, meta.prefix, meta.reducerName, "data");
179 | dispatch({ type: actionFetch, syncing, request: requestOptions });
180 | const fetchResolverOpts = {
181 | dispatch,
182 | getState,
183 | request: requestOptions,
184 | actions: meta.actions,
185 | prefetch: meta.prefetch
186 | };
187 | if (Object.defineProperty) {
188 | Object.defineProperty(fetchResolverOpts, "requestOptions", {
189 | get() {
190 | /* eslint no-console: 0 */
191 | console.warn("Deprecated option, use `request` option");
192 | return requestOptions;
193 | }
194 | });
195 | } else {
196 | fetchResolverOpts.requestOptions = requestOptions;
197 | }
198 |
199 | const result = new Promise((done, fail) => {
200 | fetchResolver(0, fetchResolverOpts, err => {
201 | if (err) {
202 | pubsub.reject(err);
203 | return fail(err);
204 | }
205 | new Promise((resolve, reject) => {
206 | requestHolder.set({
207 | resolve,
208 | reject,
209 | promise: request(pathvars, params, {}, getState, dispatch).then(
210 | resolve,
211 | reject
212 | )
213 | });
214 | }).then(
215 | d => {
216 | requestHolder.pop();
217 | const data = meta.transformer(d, prevData, {
218 | type: actionSuccess,
219 | request: requestOptions
220 | });
221 | dispatch({
222 | data,
223 | origData: d,
224 | type: actionSuccess,
225 | syncing: false,
226 | request: requestOptions
227 | });
228 | if (meta.broadcast) {
229 | meta.broadcast.forEach(type => {
230 | dispatch({
231 | type,
232 | data,
233 | origData: d,
234 | request: requestOptions
235 | });
236 | });
237 | }
238 | if (meta.postfetch) {
239 | meta.postfetch.forEach(postfetch => {
240 | postfetch instanceof Function &&
241 | postfetch({
242 | data,
243 | getState,
244 | dispatch,
245 | actions: meta.actions,
246 | request: requestOptions
247 | });
248 | });
249 | }
250 | pubsub.resolve(data);
251 | done(data);
252 | },
253 | error => {
254 | dispatch({
255 | error,
256 | type: actionFail,
257 | loading: false,
258 | syncing: false,
259 | request: requestOptions
260 | });
261 | pubsub.reject(error);
262 | fail(error);
263 | }
264 | );
265 | });
266 | });
267 | result.catch(none);
268 | return result;
269 | };
270 | }
271 |
272 | /*
273 | Pure rest request
274 | */
275 | fn.request = function(pathvars, params, options) {
276 | return request(pathvars, params, options || {});
277 | };
278 |
279 | /**
280 | * Reset store to initial state
281 | */
282 | fn.reset = mutation => {
283 | abort();
284 | return mutation === "sync"
285 | ? { type: actionReset, mutation }
286 | : { type: actionReset };
287 | };
288 |
289 | /*
290 | Abort request
291 | */
292 | fn.abort = function() {
293 | const error = abort();
294 | return { type: actionAbort, error };
295 | };
296 |
297 | fn.force = function(...args) {
298 | return (dispatch, getState) => {
299 | const state = getState();
300 | const isLoading = get(state, meta.prefix, meta.reducerName, "loading");
301 | if (isLoading) {
302 | dispatch(fn.abort());
303 | }
304 | return fn(...args)(dispatch, getState);
305 | };
306 | };
307 |
308 | /**
309 | * Sync store with server. In server mode works as usual method.
310 | * If data have already synced, data would not fetch after call this method.
311 | * @param {Object} pathvars path vars for url
312 | * @param {Object} params fetch params
313 | * @param {Function} callback) callback execute after end request
314 | */
315 | fn.sync = (...args) => {
316 | const [pathvars, params, callback] = extractArgs(args);
317 | const isServer = meta.holder ? meta.holder.server : false;
318 | return (dispatch, getState) => {
319 | const state = getState();
320 | const store = get(state, meta.prefix, name);
321 | if (!isServer && store && store.sync) {
322 | callback(null, store.data);
323 | return;
324 | }
325 | const modifyParams = { ...params, syncing: true };
326 | return fn(pathvars, modifyParams, callback)(dispatch, getState);
327 | };
328 | };
329 |
330 | let helpers = meta.helpers || {};
331 | if (meta.crud) {
332 | helpers = { ...CRUD, ...helpers };
333 | }
334 | const fnHelperCallback = (memo, func, helpername) => {
335 | if (memo[helpername]) {
336 | throw new Error(
337 | `Helper name: "${helpername}" for endpoint "${name}" has been already reserved`
338 | );
339 | }
340 | const { sync, call } = func instanceof Function ? { call: func } : func;
341 | memo[helpername] = (...args) => (dispatch, getState) => {
342 | const index = args.length - 1;
343 | const callbackFn = args[index] instanceof Function ? args[index] : none;
344 | const helpersResult = fastApply(
345 | call,
346 | { getState, dispatch, actions: meta.actions },
347 | args
348 | );
349 | const result = new Promise((resolve, reject) => {
350 | const callback = (err, data) => {
351 | err ? reject(err) : resolve(data);
352 | callbackFn(err, data);
353 | };
354 | // If helper alias using async functionality
355 | if (helpersResult instanceof Function) {
356 | helpersResult((error, newArgs = []) => {
357 | if (error) {
358 | callback(error);
359 | } else {
360 | fastApply(
361 | sync ? fn.sync : fn,
362 | null,
363 | newArgs.concat(callback)
364 | )(dispatch, getState);
365 | }
366 | });
367 | } else {
368 | // if helper alias is synchronous
369 | const [pathvars, params] = helpersResult;
370 | fastApply(sync ? fn.sync : fn, null, [pathvars, params, callback])(
371 | dispatch,
372 | getState
373 | );
374 | }
375 | });
376 | result.catch(none);
377 | return result;
378 | };
379 | return memo;
380 | };
381 |
382 | return Object.keys(helpers).reduce(
383 | (memo, key) => fnHelperCallback(memo, helpers[key], key, helpers),
384 | fn
385 | );
386 | }
387 |
--------------------------------------------------------------------------------
/src/adapters/fetch.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | function processData(data) {
4 | try {
5 | return JSON.parse(data);
6 | } catch (err) {
7 | return data;
8 | }
9 | }
10 |
11 | function toJSON(resp) {
12 | if (resp.text) {
13 | return resp.text().then(processData);
14 | } else if (resp instanceof Promise) {
15 | return resp.then(processData);
16 | } else {
17 | return Promise.resolve(resp).then(processData);
18 | }
19 | }
20 |
21 | export default function(fetch) {
22 | return (url, opts) =>
23 | fetch(url, opts).then(resp => {
24 | // Normalize IE9's response to HTTP 204 when Win error 1223.
25 | const status = resp.status === 1223 ? 204 : resp.status;
26 | const statusText = resp.status === 1223 ? "No Content" : resp.statusText;
27 |
28 | if (status >= 400) {
29 | return Promise.reject({ status, statusText });
30 | } else {
31 | return toJSON(resp).then(data => {
32 | if (status >= 200 && status < 300) {
33 | return data;
34 | } else {
35 | return Promise.reject(data);
36 | }
37 | });
38 | }
39 | });
40 | }
41 |
--------------------------------------------------------------------------------
/src/async.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * @param {[type]} dispatch [description]
4 | * @param {...[type]} args [description]
5 | * @return {[type]} [description]
6 | * @example
7 | * async(dispatch,
8 | * cb=> actions.test(1, cb),
9 | * actions.test2
10 | * ).then(()=> async(dispatch, actions.test3))
11 | */
12 | export default function async(
13 | dispatch,
14 | currentFunction = null,
15 | ...restFunctions
16 | ) {
17 | return new Promise((resolve, reject) => {
18 | if (!currentFunction) {
19 | reject("no chain function");
20 | } else {
21 | dispatch(
22 | currentFunction((err, data) => {
23 | err ? reject(err) : resolve(data);
24 | }) || {}
25 | );
26 | }
27 | }).then(data => {
28 | if (restFunctions.length) {
29 | return async(dispatch, ...restFunctions);
30 | } else {
31 | return data;
32 | }
33 | });
34 | }
35 |
--------------------------------------------------------------------------------
/src/createHolder.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | export default function() {
4 | let data;
5 | let hasData = false;
6 | return {
7 | set(val) {
8 | if (!hasData) {
9 | data = val;
10 | hasData = true;
11 | return true;
12 | }
13 | return false;
14 | },
15 | empty() {
16 | return !hasData;
17 | },
18 | pop() {
19 | if (hasData) {
20 | hasData = false;
21 | const result = data;
22 | data = null;
23 | return result;
24 | }
25 | }
26 | };
27 | }
28 |
--------------------------------------------------------------------------------
/src/fetchResolver.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | function none() {}
4 |
5 | export default function fetchResolver(index = 0, opts = {}, cb = none) {
6 | if (!opts.prefetch || index >= opts.prefetch.length) {
7 | cb();
8 | } else {
9 | opts.prefetch[index](opts, err =>
10 | err ? cb(err) : fetchResolver(index + 1, opts, cb)
11 | );
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/helpers.js:
--------------------------------------------------------------------------------
1 | export function none() {}
2 |
3 | export function extractArgs(args) {
4 | let pathvars;
5 | let params = {};
6 | let callback;
7 | if (args[0] instanceof Function) {
8 | callback = args[0];
9 | } else if (args[1] instanceof Function) {
10 | pathvars = args[0];
11 | callback = args[1];
12 | } else {
13 | pathvars = args[0];
14 | params = args[1];
15 | callback = args[2] || none;
16 | }
17 | return [pathvars, params, callback];
18 | }
19 |
20 | export function helperCrudFunction(name) {
21 | return (...args) => {
22 | const [pathvars, params, cb] = extractArgs(args);
23 | return [pathvars, { ...params, method: name.toUpperCase() }, cb];
24 | };
25 | }
26 |
27 | export function defaultMiddlewareArgsParser(dispatch, getState) {
28 | return { dispatch, getState };
29 | }
30 |
31 | export const CRUD = ["get", "post", "put", "delete", "patch"].reduce(
32 | (memo, name) => {
33 | memo[name] = helperCrudFunction(name);
34 | return memo;
35 | },
36 | {}
37 | );
38 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /* eslint no-void: 0 */
4 |
5 | import reducerFn from "./reducerFn";
6 | import actionFn from "./actionFn";
7 | import transformers from "./transformers";
8 | import async from "./async";
9 | import cacheManager from "./utils/cache";
10 | // export { transformers, async };
11 |
12 | /**
13 | * Default configuration for each endpoint
14 | * @type {Object}
15 | */
16 | const defaultEndpointConfig = {
17 | transformer: transformers.object
18 | };
19 |
20 | const PREFIX = "@@redux-api";
21 | /**
22 | * Entry api point
23 | * @param {Object} config Rest api configuration
24 | * @param {Object} baseConfig baseConfig settings for Rest api
25 | * @param {Function} fetch Adapter for rest requests
26 | * @param {Boolean} isServer false by default (fif you want to use it for isomorphic apps)
27 | * @return {actions, reducers} { actions, reducers}
28 | * @example ```js
29 | * const api = reduxApi({
30 | * test: "/plain/url",
31 | * testItem: "/plain/url/:id",
32 | * testModify: {
33 | * url: "/plain/url/:endpoint",
34 |
35 | * transformer: (data)=> !data ?
36 | * { title: "", message: "" } :
37 | * { title: data.title, message: data.message },
38 | * options: {
39 | * method: "post"
40 | * headers: {
41 | * "Accept": "application/json",
42 | * "Content-Type": "application/json"
43 | * }
44 | * }
45 | * }
46 | * });
47 | * // register reducers
48 | *
49 | * // call actions
50 | * dispatch(api.actions.test());
51 | * dispatch(api.actions.testItem({id: 1}));
52 | * dispatch(api.actions.testModify({endpoint: "upload-1"}, {
53 | * body: JSON.stringify({title: "Hello", message: "World"})
54 | * }));
55 | * ```
56 | */
57 |
58 | export default function reduxApi(config, baseConfig) {
59 | config || (config = {});
60 |
61 | const fetchHolder = {
62 | fetch: null,
63 | server: false,
64 | rootUrl: null,
65 | middlewareParser: null,
66 | options: {},
67 | responseHandler: null
68 | };
69 |
70 | const cfg = {
71 | use(key, value) {
72 | fetchHolder[key] = value;
73 |
74 | return this;
75 | },
76 | init(fetch, isServer = false, rootUrl) {
77 | /* eslint no-console: 0 */
78 | console.warn("Deprecated method, use `use` method");
79 | this.use("fetch", fetch);
80 | this.use("server", isServer);
81 | this.use("rootUrl", rootUrl);
82 | return this;
83 | },
84 | actions: {},
85 | reducers: {},
86 | events: {}
87 | };
88 | function fnConfigCallback(memo, value, key) {
89 | const opts =
90 | typeof value === "object"
91 | ? { ...defaultEndpointConfig, reducerName: key, ...value }
92 | : { ...defaultEndpointConfig, reducerName: key, url: value };
93 |
94 | if (opts.broadcast !== void 0) {
95 | /* eslint no-console: 0 */
96 | console.warn(
97 | "Deprecated `broadcast` option. you shoud use `events`" +
98 | "to catch redux-api events (see https://github.com/lexich/redux-api/blob/master/DOCS.md#Events)"
99 | );
100 | }
101 |
102 | const {
103 | url,
104 | urlOptions,
105 | options,
106 | transformer,
107 | broadcast,
108 | crud,
109 | reducerName,
110 | prefetch,
111 | postfetch,
112 | validation,
113 | helpers
114 | } = opts;
115 |
116 | const prefix = (baseConfig && baseConfig.prefix) || "";
117 |
118 | const ACTIONS = {
119 | actionFetch: `${PREFIX}@${prefix}${reducerName}`,
120 | actionSuccess: `${PREFIX}@${prefix}${reducerName}_success`,
121 | actionFail: `${PREFIX}@${prefix}${reducerName}_fail`,
122 | actionReset: `${PREFIX}@${prefix}${reducerName}_delete`,
123 | actionCache: `${PREFIX}@${prefix}${reducerName}_cache`,
124 | actionAbort: `${PREFIX}@${prefix}${reducerName}_abort`
125 | };
126 |
127 | const fetch = opts.fetch
128 | ? opts.fetch
129 | : function(...args) {
130 | return fetchHolder.fetch.apply(this, args);
131 | };
132 |
133 | const meta = {
134 | holder: fetchHolder,
135 | virtual: !!opts.virtual,
136 | actions: memo.actions,
137 | cache: cacheManager(opts.cache),
138 | urlOptions,
139 | fetch,
140 | broadcast,
141 | reducerName,
142 | prefetch,
143 | postfetch,
144 | validation,
145 | helpers,
146 | transformer,
147 | prefix,
148 | crud
149 | };
150 |
151 | memo.actions[key] = actionFn(url, key, options, ACTIONS, meta);
152 |
153 | if (!meta.virtual && !memo.reducers[reducerName]) {
154 | const data = transformer();
155 | const sync = false;
156 | const syncing = false;
157 | const loading = false;
158 | const initialState = opts.cache
159 | ? { sync, syncing, loading, data, cache: {}, request: null }
160 | : { sync, syncing, loading, data, request: null };
161 |
162 | const reducer = opts.reducer ? opts.reducer.bind(memo) : null;
163 | memo.reducers[reducerName] = reducerFn(initialState, ACTIONS, reducer);
164 | }
165 | memo.events[reducerName] = ACTIONS;
166 | return memo;
167 | }
168 |
169 | return Object.keys(config).reduce(
170 | (memo, key) => fnConfigCallback(memo, config[key], key, config),
171 | cfg
172 | );
173 | }
174 |
175 | reduxApi.transformers = transformers;
176 | reduxApi.async = async;
177 |
--------------------------------------------------------------------------------
/src/reducerFn.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /* eslint no-case-declarations: 0 */
4 | import { setExpire } from "./utils/cache";
5 |
6 | /**
7 | * Reducer contructor
8 | * @param {Object} initialState default initial state
9 | * @param {Object} actions actions map
10 | * @param {Function} reducer custom reducer function
11 | * @return {Function} reducer function
12 | */
13 | export default function reducerFn(initialState, actions = {}, reducer) {
14 | const {
15 | actionFetch,
16 | actionSuccess,
17 | actionFail,
18 | actionReset,
19 | actionCache,
20 | actionAbort
21 | } = actions;
22 | return (state = initialState, action) => {
23 | const request = action.request || {};
24 | switch (action.type) {
25 | case actionFetch:
26 | return {
27 | ...state,
28 | request,
29 | loading: true,
30 | error: null,
31 | syncing: !!action.syncing
32 | };
33 | case actionSuccess:
34 | return {
35 | ...state,
36 | loading: false,
37 | sync: true,
38 | syncing: false,
39 | error: null,
40 | data: action.data
41 | };
42 | case actionFail:
43 | return {
44 | ...state,
45 | loading: false,
46 | error: action.error,
47 | syncing: false
48 | };
49 | case actionReset:
50 | const { mutation } = action;
51 | return mutation === "sync"
52 | ? {
53 | ...state,
54 | request: null,
55 | sync: false
56 | }
57 | : { ...initialState };
58 | case actionAbort:
59 | return {
60 | ...state,
61 | request: null,
62 | loading: false,
63 | syncing: false,
64 | error: action.error
65 | };
66 | case actionCache:
67 | const { id, data } = action;
68 | const cacheExpire = state.cache[id] ? state.cache[id].expire : null;
69 | const expire = setExpire(action.expire, cacheExpire);
70 | return {
71 | ...state,
72 | cache: { ...state.cache, [id]: { expire, data } }
73 | };
74 | default:
75 | return reducer ? reducer(state, action) : state;
76 | }
77 | };
78 | }
79 |
--------------------------------------------------------------------------------
/src/transformers.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const toString = Object.prototype.toString;
4 | const OBJECT = "[object Object]";
5 |
6 | /**
7 | * Default responce transformens
8 | */
9 | export default {
10 | array(data) {
11 | return !data ? [] : Array.isArray(data) ? data : [data];
12 | },
13 | object(data) {
14 | if (!data) {
15 | return {};
16 | }
17 | return toString.call(data) === OBJECT ? data : { data };
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/src/urlTransform.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import qs from "qs";
4 | import { parse } from "url";
5 | import omit from "./utils/omit";
6 | import merge from "./utils/merge";
7 |
8 | /* eslint no-useless-escape: 0 */
9 | const rxClean = /(\(:[^\)]+\)|:[^\/]+\/?)/g;
10 |
11 | /**
12 | * Url modification
13 | * @param {String} url url template
14 | * @param {Object} params params for url template
15 | * @param {Object} options transformation options, accepts +delimiter+, +arrayFormat+,
16 | * +qsStringifyOptions+ and +qsParseOptions+
17 | * @return {String} result url
18 | */
19 | export default function urlTransform(url, params, options) {
20 | if (!url) {
21 | return "";
22 | }
23 | params || (params = {});
24 | const usedKeys = {};
25 | const urlWithParams = Object.keys(params).reduce((url, key) => {
26 | const value = params[key];
27 | const rx = new RegExp(`(\\(:${key}\\)|:${key})(\/?)`, "g");
28 | return url.replace(rx, (_, _1, slash) => {
29 | usedKeys[key] = value;
30 | return value ? value + slash : value;
31 | });
32 | }, url);
33 |
34 | if (!urlWithParams) {
35 | return urlWithParams;
36 | }
37 | const { protocol, host, path } = parse(urlWithParams);
38 | const cleanURL = host
39 | ? `${protocol}//${host}${path.replace(rxClean, "")}`
40 | : path.replace(rxClean, "");
41 | const usedKeysArray = Object.keys(usedKeys);
42 | if (usedKeysArray.length !== Object.keys(params).length) {
43 | const urlObject = cleanURL.split("?");
44 | options || (options = {});
45 | const { arrayFormat, delimiter } = options;
46 | const qsParseOptions = {
47 | arrayFormat,
48 | delimiter,
49 | ...options.qsParseOptions
50 | };
51 | const mergeParams = merge(
52 | urlObject[1] && qs.parse(urlObject[1], qsParseOptions),
53 | omit(params, usedKeysArray)
54 | );
55 | const qsStringifyOptions = {
56 | arrayFormat,
57 | delimiter,
58 | ...options.qsStringifyOptions
59 | };
60 | const urlStringParams = qs.stringify(mergeParams, qsStringifyOptions);
61 | return `${urlObject[0]}?${urlStringParams}`;
62 | }
63 | return cleanURL;
64 | }
65 |
--------------------------------------------------------------------------------
/src/utils/cache.js:
--------------------------------------------------------------------------------
1 | export const MockNowDate = {
2 | date: undefined,
3 | push(date) {
4 | this.date = date;
5 | },
6 | pop() {
7 | if (this.date) {
8 | const d = this.date;
9 | this.date = undefined;
10 | return new Date(d);
11 | } else {
12 | return new Date();
13 | }
14 | }
15 | };
16 |
17 | export const Manager = {
18 | expire: false,
19 | getData(cache) {
20 | if (!cache) {
21 | return;
22 | }
23 | const { expire, data } = cache;
24 | if (expire === false || expire === undefined || expire === null) {
25 | return data;
26 | }
27 | if (expire instanceof Date) {
28 | if (expire.valueOf() > MockNowDate.pop().valueOf()) {
29 | return data;
30 | }
31 | }
32 | },
33 | id(params) {
34 | if (!params) {
35 | return "";
36 | }
37 | return Object.keys(params).reduce(
38 | (memo, key) => memo + `${key}=${params[key]};`,
39 | ""
40 | );
41 | }
42 | };
43 |
44 | export function setExpire(value, oldDate) {
45 | let expire = value;
46 | if (typeof expire === "number" || expire instanceof Number) {
47 | const d = MockNowDate.pop();
48 | d.setSeconds(d.getSeconds() + expire);
49 | expire = d;
50 | }
51 | if (oldDate instanceof Date && expire instanceof Date) {
52 | if (expire.valueOf() < oldDate.valueOf()) {
53 | expire = oldDate;
54 | }
55 | }
56 | return expire;
57 | }
58 |
59 | export function getCacheManager(expire, cache) {
60 | if (expire !== undefined) {
61 | const ret = { ...Manager, ...cache };
62 | if (ret.expire !== false) {
63 | ret.expire = setExpire(expire, ret.expire);
64 | }
65 | return ret;
66 | } else if (cache) {
67 | return { ...Manager, ...cache };
68 | } else {
69 | return null;
70 | }
71 | }
72 |
73 | export default function(cache) {
74 | if (!cache) {
75 | return null;
76 | }
77 | if (cache === true) {
78 | return Manager;
79 | } else {
80 | return { ...Manager, ...cache };
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/utils/get.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /* eslint no-void: 0 */
4 | function isEmpty(name) {
5 | return name === "" || name === null || name === void 0;
6 | }
7 |
8 | function get(obj, ...path) {
9 | return path.reduce(
10 | (memo, name) =>
11 | Array.isArray(name)
12 | ? get(memo, ...name)
13 | : isEmpty(name)
14 | ? memo
15 | : memo && memo[name],
16 | obj
17 | );
18 | }
19 |
20 | export default get;
21 |
--------------------------------------------------------------------------------
/src/utils/merge.js:
--------------------------------------------------------------------------------
1 | /* eslint no-void: 0 */
2 | const toString = Object.prototype.toString;
3 | const OBJECT = "[object Object]";
4 | const ARRAY = "[object Array]";
5 |
6 | export function mergePair(a, b) {
7 | if (a === void 0) {
8 | return b;
9 | }
10 | if (b === void 0) {
11 | return a;
12 | }
13 |
14 | const aType = toString.call(a);
15 | const bType = toString.call(b);
16 | if (aType === ARRAY) {
17 | return a.concat(b);
18 | }
19 | if (bType === ARRAY) {
20 | return [a].concat(b);
21 | }
22 | if (aType !== OBJECT || bType !== OBJECT) {
23 | return b;
24 | }
25 | return Object.keys(b).reduce((memo, key) => {
26 | memo[key] = mergePair(a[key], b[key]);
27 | return memo;
28 | }, a);
29 | }
30 |
31 | export default function(...args) {
32 | return args.length ? args.reduce(mergePair) : null;
33 | }
34 |
--------------------------------------------------------------------------------
/src/utils/omit.js:
--------------------------------------------------------------------------------
1 | export default function(object, props) {
2 | if (!Array.isArray(props)) {
3 | return { ...object };
4 | }
5 |
6 | return Object.keys(object || {}).reduce((memo, key) => {
7 | if (props.indexOf(key) === -1) {
8 | memo[key] = object[key];
9 | }
10 | return memo;
11 | }, {});
12 | }
13 |
--------------------------------------------------------------------------------
/test/PubSub_spec.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /* global describe, it */
4 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */
5 | import { expect } from "chai";
6 | import PubSub from "../src/PubSub";
7 |
8 | describe("PubSub", function() {
9 | it("constructor", function() {
10 | const pubsub = new PubSub();
11 | expect(pubsub.container).to.be.instanceOf(Array);
12 | expect(pubsub.container).to.have.length(0);
13 | });
14 | it("push", function() {
15 | const pubsub = new PubSub();
16 | pubsub.push();
17 | expect(pubsub.container).to.have.length(0);
18 | pubsub.push(function() {});
19 | expect(pubsub.container).to.have.length(1);
20 | });
21 | it("reject", function() {
22 | const expectArr = [];
23 | function ok1(err) {
24 | expectArr.push({ t: "ok1", err });
25 | }
26 | function ok2(err) {
27 | expectArr.push({ t: "ok2", err });
28 | }
29 | const pubsub = new PubSub();
30 | pubsub.push(ok1);
31 | pubsub.push(ok2);
32 | expect(pubsub.container).to.have.length(2);
33 | pubsub.reject("err");
34 | expect(pubsub.container).to.have.length(0);
35 | expect(expectArr).to.eql([
36 | { t: "ok1", err: "err" },
37 | { t: "ok2", err: "err" }
38 | ]);
39 | });
40 | it("resolve", function() {
41 | const expectArr = [];
42 | function ok1(err, data) {
43 | expectArr.push({ t: "ok1", err, data });
44 | }
45 | function ok2(err, data) {
46 | expectArr.push({ t: "ok2", err, data });
47 | }
48 | const pubsub = new PubSub();
49 | pubsub.push(ok1);
50 | pubsub.push(ok2);
51 | expect(pubsub.container).to.have.length(2);
52 | pubsub.resolve("ok");
53 | expect(pubsub.container).to.have.length(0);
54 | expect(expectArr).to.eql([
55 | { t: "ok1", err: null, data: "ok" },
56 | { t: "ok2", err: null, data: "ok" }
57 | ]);
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/test/actionFn_spec.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /* global describe, it */
4 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}], no-void: 0 */
5 | import { expect } from "chai";
6 | import isFunction from "lodash/isFunction";
7 | import actionFn from "../src/actionFn";
8 |
9 | function fetchSuccess() {
10 | return new Promise(function(resolve) {
11 | resolve({ msg: "hello" });
12 | });
13 | }
14 |
15 | function getState() {
16 | return {
17 | test: { loading: false, syncing: false, sync: false, data: {} }
18 | };
19 | }
20 |
21 | const ERROR = new Error("Error");
22 |
23 | function fetchFail() {
24 | return new Promise(function(resolve, reject) {
25 | reject(ERROR);
26 | });
27 | }
28 |
29 | function transformer(data) {
30 | return data;
31 | }
32 |
33 | const ACTIONS = {
34 | actionFetch: "actionFetch",
35 | actionSuccess: "actionSuccess",
36 | actionFail: "actionFail",
37 | actionReset: "actionReset",
38 | actionCache: "actionCache"
39 | };
40 |
41 | describe("actionFn", function() {
42 | it("check null params", function() {
43 | const api = actionFn();
44 | expect(isFunction(api)).to.be.true;
45 | });
46 |
47 | it("check sync method", function() {
48 | let executeCounter = 0;
49 | const api = actionFn("/test", "test", null, ACTIONS, {
50 | transformer,
51 | fetch: () => {
52 | executeCounter += 1;
53 | return fetchSuccess();
54 | }
55 | });
56 |
57 | const async1 = new Promise(resolve => {
58 | const initialState = getState();
59 | initialState.test.sync = true;
60 |
61 | api.sync(resolve)(
62 | function() {},
63 | () => initialState
64 | );
65 | expect(executeCounter).to.be.eql(0);
66 | });
67 |
68 | const expectedEvent = [
69 | {
70 | type: ACTIONS.actionFetch,
71 | syncing: true,
72 | request: { pathvars: undefined, params: {} }
73 | },
74 | {
75 | type: ACTIONS.actionSuccess,
76 | data: { msg: "hello" },
77 | origData: { msg: "hello" },
78 | syncing: false,
79 | request: { pathvars: undefined, params: {} }
80 | }
81 | ];
82 | const async2 = new Promise(resolve => {
83 | api.sync(resolve)(msg => {
84 | expect(expectedEvent).to.have.length.above(0);
85 | const exp = expectedEvent.shift();
86 | expect(msg).to.eql(exp);
87 | }, getState);
88 | }).then(() => {
89 | expect(executeCounter).to.be.eql(1);
90 | expect(expectedEvent).to.have.length(0);
91 | });
92 |
93 | return Promise.all([async1, async2]);
94 | });
95 |
96 | it("check request method", function() {
97 | let urlFetch;
98 | let paramsFetch;
99 | const api = actionFn("/test/:id", "test", null, ACTIONS, {
100 | transformer,
101 | fetch: (url, params) => {
102 | urlFetch = url;
103 | paramsFetch = params;
104 | return fetchSuccess();
105 | }
106 | });
107 | const async = api.request({ id: 2 }, { hello: "world" });
108 | expect(async).to.be.an.instanceof(Promise);
109 | return async.then(data => {
110 | expect(data).to.eql({ msg: "hello" });
111 | expect(urlFetch).to.eql("/test/2");
112 | expect(paramsFetch).to.eql({ hello: "world" });
113 | });
114 | });
115 |
116 | it("check normal usage", function() {
117 | const api = actionFn("/test", "test", null, ACTIONS, {
118 | transformer,
119 | fetch: fetchSuccess
120 | });
121 | expect(api.reset()).to.eql({ type: ACTIONS.actionReset });
122 | const expectedEvent = [
123 | {
124 | type: ACTIONS.actionFetch,
125 | syncing: false,
126 | request: { pathvars: undefined, params: {} }
127 | },
128 | {
129 | type: ACTIONS.actionSuccess,
130 | data: { msg: "hello" },
131 | origData: { msg: "hello" },
132 | syncing: false,
133 | request: { pathvars: undefined, params: {} }
134 | }
135 | ];
136 | return new Promise(resolve => {
137 | const action = api(resolve);
138 | expect(isFunction(action)).to.be.true;
139 | function dispatch(msg) {
140 | expect(expectedEvent).to.have.length.above(0);
141 | const exp = expectedEvent.shift();
142 | expect(msg).to.eql(exp);
143 | }
144 | action(dispatch, getState);
145 | }).then(() => {
146 | expect(expectedEvent).to.have.length(0);
147 | });
148 | });
149 |
150 | it("check reset helper with mutation", function() {
151 | const api = actionFn("/test", "test", null, ACTIONS, {
152 | transformer,
153 | fetch: fetchSuccess
154 | });
155 | expect(api.reset()).to.eql({ type: ACTIONS.actionReset });
156 | expect(api.reset("sync")).to.eql({
157 | type: ACTIONS.actionReset,
158 | mutation: "sync"
159 | });
160 | expect(api.reset("other")).to.eql({ type: ACTIONS.actionReset });
161 | });
162 |
163 | it("check fail fetch", function() {
164 | const api = actionFn("/test", "test", null, ACTIONS, {
165 | transformer,
166 | fetch: fetchFail
167 | });
168 | const expectedEvent = [
169 | {
170 | type: ACTIONS.actionFetch,
171 | syncing: false,
172 | request: { pathvars: undefined, params: {} }
173 | },
174 | {
175 | type: ACTIONS.actionFail,
176 | error: ERROR,
177 | syncing: false,
178 | request: { pathvars: undefined, params: {} }
179 | }
180 | ];
181 | function dispatch(msg) {
182 | expect(expectedEvent).to.have.length.above(0);
183 | const exp = expectedEvent.shift();
184 | expect(msg.type).to.eql(exp.type);
185 | expect(msg.syncing).to.eql(exp.syncing);
186 | expect(msg.request).to.eql(exp.request);
187 | expect(msg.error).to.eql(exp.error);
188 | }
189 | return new Promise(resolve => {
190 | api(resolve)(dispatch, getState);
191 | }).then(
192 | () => {
193 | expect(expectedEvent).to.have.length(0);
194 | },
195 | err => expect(null).to.eql(err)
196 | );
197 | });
198 |
199 | it("check options param", function() {
200 | let callOptions = 0;
201 | let checkOptions = null;
202 | const api = actionFn(
203 | "/test/:id",
204 | "test",
205 | function(url, params, _getState) {
206 | expect(_getState).to.exist;
207 | expect(getState === _getState).to.be.true;
208 | callOptions += 1;
209 | return { ...params, test: 1 };
210 | },
211 | ACTIONS,
212 | {
213 | transformer,
214 | fetch(url, opts) {
215 | checkOptions = opts;
216 | return fetchSuccess();
217 | }
218 | }
219 | );
220 | function dispatch() {}
221 | return new Promise(resolve => {
222 | api("", { params: 1 }, resolve)(dispatch, getState);
223 | expect(callOptions).to.eql(1);
224 | expect(checkOptions).to.eql({ params: 1, test: 1 });
225 | });
226 | });
227 |
228 | it("check server mode", function() {
229 | function getServerState() {
230 | return {
231 | test: { loading: false, syncing: false, sync: true, data: {} }
232 | };
233 | }
234 | const api = actionFn("/test/:id", "test", null, ACTIONS, {
235 | transformer,
236 | fetch: fetchSuccess,
237 | holder: {
238 | server: true
239 | }
240 | });
241 |
242 | const expectedEvent = [
243 | {
244 | type: ACTIONS.actionFetch,
245 | syncing: true,
246 | request: { pathvars: undefined, params: {} }
247 | },
248 | {
249 | type: ACTIONS.actionSuccess,
250 | syncing: false,
251 | data: { msg: "hello" },
252 | origData: { msg: "hello" },
253 | request: { pathvars: undefined, params: {} }
254 | }
255 | ];
256 | return new Promise(resolve => {
257 | api.sync(resolve)(function(msg) {
258 | expect(expectedEvent).to.have.length.above(0);
259 | const exp = expectedEvent.shift();
260 | expect(msg).to.eql(exp);
261 | }, getServerState);
262 | }).then(() => {
263 | expect(expectedEvent).to.have.length(0);
264 | });
265 | });
266 |
267 | it("check broadcast option", function() {
268 | const BROADCAST_ACTION = "BROADCAST_ACTION";
269 | const expectedEvent = [
270 | {
271 | type: ACTIONS.actionFetch,
272 | syncing: false,
273 | request: { pathvars: undefined, params: {} }
274 | },
275 | {
276 | type: ACTIONS.actionSuccess,
277 | data: { msg: "hello" },
278 | origData: { msg: "hello" },
279 | syncing: false,
280 | request: { pathvars: undefined, params: {} }
281 | },
282 | {
283 | type: BROADCAST_ACTION,
284 | data: { msg: "hello" },
285 | origData: { msg: "hello" },
286 | request: { pathvars: undefined, params: {} }
287 | }
288 | ];
289 | const meta = {
290 | transformer,
291 | fetch: fetchSuccess,
292 | broadcast: [BROADCAST_ACTION]
293 | };
294 | const api = actionFn("/test/:id", "test", null, ACTIONS, meta);
295 |
296 | return new Promise(resolve => {
297 | api(resolve)(function(msg) {
298 | expect(expectedEvent).to.have.length.above(0);
299 | const exp = expectedEvent.shift();
300 | expect(msg).to.eql(exp);
301 | }, getState);
302 | }).then(() => {
303 | expect(expectedEvent).to.have.length(0);
304 | });
305 | });
306 | it("check validation with request method", function() {
307 | let expData;
308 | let counter = 0;
309 | const meta = {
310 | transformer,
311 | fetch: fetchSuccess,
312 | validation(data, cb) {
313 | counter += 1;
314 | expData = data;
315 | cb();
316 | }
317 | };
318 | const api = actionFn("/test/:id", "test", null, ACTIONS, meta);
319 | return api.request({ id: 1 }).then(data => {
320 | expect(data).to.eql({ msg: "hello" });
321 | expect(counter).to.eql(1);
322 | expect(expData).to.eql({ msg: "hello" });
323 | });
324 | });
325 | it("check success validation", function() {
326 | let expData;
327 | let counter = 0;
328 | const meta = {
329 | transformer,
330 | fetch: fetchSuccess,
331 | validation(data, cb) {
332 | counter += 1;
333 | expData = data;
334 | cb();
335 | }
336 | };
337 | const expectedEvent = [
338 | {
339 | type: ACTIONS.actionFetch,
340 | syncing: false,
341 | request: { pathvars: undefined, params: {} }
342 | },
343 | {
344 | type: ACTIONS.actionSuccess,
345 | data: { msg: "hello" },
346 | origData: { msg: "hello" },
347 | syncing: false,
348 | request: { pathvars: undefined, params: {} }
349 | }
350 | ];
351 |
352 | const api = actionFn("/test/:id", "test", null, ACTIONS, meta);
353 | return new Promise(resolve => {
354 | api(resolve)(function(msg) {
355 | expect(expectedEvent).to.have.length.above(0);
356 | const exp = expectedEvent.shift();
357 | expect(msg).to.eql(exp);
358 | }, getState);
359 | }).then(() => {
360 | expect(expectedEvent).to.have.length(0);
361 | expect(counter).to.eql(1);
362 | expect(expData).to.eql({ msg: "hello" });
363 | });
364 | });
365 | it("check unsuccess validation", function() {
366 | let expData;
367 | let counter = 0;
368 | const meta = {
369 | transformer,
370 | fetch: fetchSuccess,
371 | validation(data, cb) {
372 | counter += 1;
373 | expData = data;
374 | cb("invalid");
375 | }
376 | };
377 | const expectedEvent = [
378 | {
379 | type: ACTIONS.actionFetch,
380 | syncing: false,
381 | request: { pathvars: undefined, params: {} }
382 | },
383 | {
384 | type: ACTIONS.actionFail,
385 | error: "invalid",
386 | syncing: false,
387 | request: { pathvars: undefined, params: {} }
388 | }
389 | ];
390 | const api = actionFn("/test/:id", "test", null, ACTIONS, meta);
391 | return new Promise(resolve => {
392 | api(resolve)(function(msg) {
393 | expect(expectedEvent).to.have.length.above(0);
394 | const exp = expectedEvent.shift();
395 | expect(msg.type).to.eql(exp.type);
396 | expect(msg.syncing).to.eql(exp.syncing);
397 | expect(msg.request).to.eql(exp.request);
398 | expect(msg.error).to.eql(exp.error);
399 | }, getState);
400 | }).then(
401 | () => {
402 | expect(expectedEvent).to.have.length(0);
403 | expect(counter).to.eql(1);
404 | expect(expData).to.eql({ msg: "hello" });
405 | },
406 | err => expect(null).to.eql(err)
407 | );
408 | });
409 | it("check postfetch option", function() {
410 | let expectedOpts;
411 | const meta = {
412 | transformer,
413 | fetch: fetchSuccess,
414 | postfetch: [
415 | function(opts) {
416 | expectedOpts = opts;
417 | opts.dispatch({ type: "One", data: opts.data });
418 | },
419 | function(opts) {
420 | opts.dispatch({ type: "Two", data: opts.data });
421 | }
422 | ],
423 | actions: { hello: "a" }
424 | };
425 | const api = actionFn("/test/:id", "test", null, ACTIONS, meta);
426 | const expectedEvent = [
427 | {
428 | type: ACTIONS.actionFetch,
429 | syncing: false,
430 | request: { pathvars: undefined, params: {} }
431 | },
432 | {
433 | type: ACTIONS.actionSuccess,
434 | data: { msg: "hello" },
435 | origData: { msg: "hello" },
436 | syncing: false,
437 | request: { pathvars: undefined, params: {} }
438 | },
439 | {
440 | type: "One",
441 | data: { msg: "hello" }
442 | },
443 | {
444 | type: "Two",
445 | data: { msg: "hello" }
446 | }
447 | ];
448 | function dispatch(msg) {
449 | expect(expectedEvent).to.have.length.above(0);
450 | const exp = expectedEvent.shift();
451 | expect(msg).to.eql(exp);
452 | }
453 | return new Promise(resolve => {
454 | api(resolve)(dispatch, getState);
455 | }).then(() => {
456 | expect(expectedOpts).to.exist;
457 | expect(expectedOpts).to.include.keys(
458 | "data",
459 | "getState",
460 | "dispatch",
461 | "actions",
462 | "request"
463 | );
464 | expect(expectedOpts.getState).to.eql(getState);
465 | expect(expectedOpts.dispatch).to.eql(dispatch);
466 | expect(expectedOpts.actions).to.eql({ hello: "a" });
467 | expect(expectedOpts.request).to.eql({ params: {}, pathvars: void 0 });
468 | });
469 | });
470 | it("check prefetch option", function() {
471 | const checkPrefetch = [];
472 | const meta = {
473 | transformer,
474 | fetch: fetchSuccess,
475 | prefetch: [
476 | function(opts, cb) {
477 | checkPrefetch.push(["one", opts]);
478 | cb();
479 | },
480 | function(opts, cb) {
481 | checkPrefetch.push(["two", opts]);
482 | cb();
483 | }
484 | ]
485 | };
486 | const requestOptions = { pathvars: undefined, params: {} };
487 | const api = actionFn("/test/:id", "test", null, ACTIONS, meta);
488 | const expectedEvent = [
489 | {
490 | type: ACTIONS.actionFetch,
491 | syncing: false,
492 | request: requestOptions
493 | },
494 | {
495 | type: ACTIONS.actionSuccess,
496 | data: { msg: "hello" },
497 | origData: { msg: "hello" },
498 | syncing: false,
499 | request: requestOptions
500 | }
501 | ];
502 | function dispatch(msg) {
503 | expect(expectedEvent).to.have.length.above(0);
504 | const exp = expectedEvent.shift();
505 | expect(msg).to.eql(exp);
506 | }
507 | const expOpts = {
508 | dispatch,
509 | getState,
510 | request: requestOptions,
511 | actions: undefined,
512 | prefetch: meta.prefetch
513 | };
514 | return new Promise(resolve => {
515 | api(resolve)(dispatch, getState);
516 | }).then(
517 | () => {
518 | expect(expectedEvent).to.have.length(0);
519 | expect(checkPrefetch).to.eql([
520 | ["one", expOpts],
521 | ["two", expOpts]
522 | ]);
523 | },
524 | err => expect(null).to.eql(err)
525 | );
526 | });
527 | it("check incorrect helpers name", function() {
528 | expect(() =>
529 | actionFn("/test/:id", "test", null, ACTIONS, {
530 | helpers: {
531 | reset() {}
532 | }
533 | })
534 | ).to.throw(
535 | Error,
536 | 'Helper name: "reset" for endpoint "test" has been already reserved'
537 | );
538 | expect(() =>
539 | actionFn("/test/:id", "test", null, ACTIONS, {
540 | helpers: {
541 | sync() {}
542 | }
543 | })
544 | ).to.throw(
545 | Error,
546 | 'Helper name: "sync" for endpoint "test" has been already reserved'
547 | );
548 | });
549 | it("check that helpers returns Promise", function() {
550 | const api = actionFn("/test/:id", "test", null, ACTIONS, {
551 | transformer,
552 | fetch: fetchSuccess,
553 | helpers: {
554 | test: () => cb => cb(null, [{ id: 1 }, { async: true }])
555 | }
556 | });
557 | const result = api.test()(() => {}, getState);
558 | expect(result).to.be.an.instanceof(Promise);
559 | });
560 | it("check helpers with async functionality", function() {
561 | const meta = {
562 | transformer,
563 | fetch(url, opts) {
564 | return new Promise(resolve => resolve({ url, opts }));
565 | },
566 | helpers: {
567 | asyncSuccess: () => cb => cb(null, [{ id: 1 }, { async: true }]),
568 | asyncFail: () => cb => cb("Error")
569 | }
570 | };
571 | const api = actionFn("/test/:id", "test", null, ACTIONS, meta);
572 | const expectedEvent1 = [
573 | {
574 | type: ACTIONS.actionFetch,
575 | syncing: false,
576 | request: { pathvars: { id: 1 }, params: { async: true } }
577 | },
578 | {
579 | type: ACTIONS.actionSuccess,
580 | syncing: false,
581 | data: { url: "/test/1", opts: { async: true } },
582 | origData: { url: "/test/1", opts: { async: true } },
583 | request: { pathvars: { id: 1 }, params: { async: true } }
584 | }
585 | ];
586 | const wait1 = new Promise(resolve => {
587 | api.asyncSuccess(resolve)(function(msg) {
588 | expect(expectedEvent1).to.have.length.above(0);
589 | const exp = expectedEvent1.shift();
590 | expect(msg).to.eql(exp);
591 | }, getState);
592 | });
593 | let errorMsg;
594 | const wait2 = new Promise(resolve => {
595 | api.asyncFail(function(err) {
596 | errorMsg = err;
597 | resolve();
598 | })(function() {}, getState);
599 | });
600 | return Promise.all([wait1, wait2]).then(() => {
601 | expect(expectedEvent1).to.have.length(0);
602 | expect(errorMsg).to.eql("Error");
603 | });
604 | });
605 |
606 | it("check crud option", function() {
607 | const meta = {
608 | transformer,
609 | crud: true,
610 | fetch(url, opts) {
611 | return new Promise(resolve => resolve({ url, opts }));
612 | }
613 | };
614 | const api = actionFn("/test/:id", "test", null, ACTIONS, meta);
615 | const expectedEvent = [
616 | {
617 | type: ACTIONS.actionFetch,
618 | syncing: false,
619 | request: {
620 | pathvars: { id: 1 },
621 | params: { method: "GET" }
622 | }
623 | },
624 | {
625 | type: ACTIONS.actionFetch,
626 | syncing: false,
627 | request: {
628 | pathvars: { id: 2 },
629 | params: { body: "Hello", method: "POST" }
630 | }
631 | },
632 | {
633 | type: ACTIONS.actionFetch,
634 | syncing: false,
635 | request: {
636 | pathvars: { id: 3 },
637 | params: { body: "World", method: "PUT" }
638 | }
639 | },
640 | {
641 | type: ACTIONS.actionFetch,
642 | syncing: false,
643 | request: {
644 | pathvars: { id: 4 },
645 | params: { method: "DELETE" }
646 | }
647 | },
648 | {
649 | type: ACTIONS.actionFetch,
650 | syncing: false,
651 | request: {
652 | pathvars: { id: 5 },
653 | params: { body: "World", method: "PATCH" }
654 | }
655 | },
656 | {
657 | type: ACTIONS.actionSuccess,
658 | syncing: false,
659 | data: { url: "/test/1", opts: { method: "GET" } },
660 | origData: { url: "/test/1", opts: { method: "GET" } },
661 | request: {
662 | pathvars: { id: 1 },
663 | params: { method: "GET" }
664 | }
665 | },
666 | {
667 | type: ACTIONS.actionSuccess,
668 | syncing: false,
669 | data: {
670 | url: "/test/2",
671 | opts: { body: "Hello", method: "POST" }
672 | },
673 | origData: {
674 | url: "/test/2",
675 | opts: { body: "Hello", method: "POST" }
676 | },
677 | request: {
678 | pathvars: { id: 2 },
679 | params: { body: "Hello", method: "POST" }
680 | }
681 | },
682 | {
683 | type: ACTIONS.actionSuccess,
684 | syncing: false,
685 | data: {
686 | url: "/test/3",
687 | opts: { body: "World", method: "PUT" }
688 | },
689 | origData: {
690 | url: "/test/3",
691 | opts: { body: "World", method: "PUT" }
692 | },
693 | request: {
694 | pathvars: { id: 3 },
695 | params: { body: "World", method: "PUT" }
696 | }
697 | },
698 | {
699 | type: ACTIONS.actionSuccess,
700 | syncing: false,
701 | data: {
702 | url: "/test/4",
703 | opts: { method: "DELETE" }
704 | },
705 | origData: {
706 | url: "/test/4",
707 | opts: { method: "DELETE" }
708 | },
709 | request: {
710 | pathvars: { id: 4 },
711 | params: { method: "DELETE" }
712 | }
713 | },
714 | {
715 | type: ACTIONS.actionSuccess,
716 | syncing: false,
717 | data: {
718 | url: "/test/5",
719 | opts: { body: "World", method: "PATCH" }
720 | },
721 | origData: {
722 | url: "/test/5",
723 | opts: { body: "World", method: "PATCH" }
724 | },
725 | request: {
726 | pathvars: { id: 5 },
727 | params: { body: "World", method: "PATCH" }
728 | }
729 | }
730 | ];
731 |
732 | const getQuery = new Promise(resolve => {
733 | api.get({ id: 1 }, resolve)(function(msg) {
734 | expect(expectedEvent).to.have.length.above(0);
735 | const exp = expectedEvent.shift();
736 | expect(msg).to.eql(exp);
737 | }, getState);
738 | });
739 |
740 | const postQuery = new Promise(resolve => {
741 | api.post(
742 | { id: 2 },
743 | { body: "Hello" },
744 | resolve
745 | )(function(msg) {
746 | expect(expectedEvent).to.have.length.above(0);
747 | const exp = expectedEvent.shift();
748 | expect(msg).to.eql(exp);
749 | }, getState);
750 | });
751 | const putQuery = new Promise(resolve => {
752 | api.put(
753 | { id: 3 },
754 | { body: "World" },
755 | resolve
756 | )(function(msg) {
757 | expect(expectedEvent).to.have.length.above(0);
758 | const exp = expectedEvent.shift();
759 | expect(msg).to.eql(exp);
760 | }, getState);
761 | });
762 | const deleteQuery = new Promise(resolve => {
763 | api.delete({ id: 4 }, resolve)(function(msg) {
764 | expect(expectedEvent).to.have.length.above(0);
765 | const exp = expectedEvent.shift();
766 | expect(msg).to.eql(exp);
767 | }, getState);
768 | });
769 | const patchQuery = new Promise(resolve => {
770 | api.patch(
771 | { id: 5 },
772 | { body: "World" },
773 | resolve
774 | )(function(msg) {
775 | expect(expectedEvent).to.have.length.above(0);
776 | const exp = expectedEvent.shift();
777 | expect(msg).to.eql(exp);
778 | }, getState);
779 | });
780 |
781 | return Promise.all([
782 | getQuery,
783 | postQuery,
784 | putQuery,
785 | deleteQuery,
786 | patchQuery
787 | ]).then(() => expect(expectedEvent).to.have.length(0));
788 | });
789 |
790 | it("check crud option with overwrite", function() {
791 | const meta = {
792 | transformer,
793 | crud: true,
794 | fetch(url, opts) {
795 | return new Promise(resolve => resolve({ url, opts }));
796 | },
797 | helpers: {
798 | get() {
799 | return [{ id: "overwrite" }];
800 | }
801 | }
802 | };
803 | const api = actionFn("/test/:id", "test", null, ACTIONS, meta);
804 | const expectedEvent = [
805 | {
806 | type: ACTIONS.actionFetch,
807 | syncing: false,
808 | request: { pathvars: { id: "overwrite" }, params: undefined }
809 | },
810 | {
811 | type: ACTIONS.actionSuccess,
812 | syncing: false,
813 | data: { url: "/test/overwrite", opts: null },
814 | origData: { url: "/test/overwrite", opts: null },
815 | request: { pathvars: { id: "overwrite" }, params: undefined }
816 | }
817 | ];
818 |
819 | return new Promise(resolve => {
820 | api.get({ id: 1 }, resolve)(function(msg) {
821 | expect(expectedEvent).to.have.length.above(0);
822 | const exp = expectedEvent.shift();
823 | expect(msg).to.eql(exp);
824 | }, getState);
825 | }).then(() => expect(expectedEvent).to.have.length(0));
826 | });
827 |
828 | it("check crud option with overwrite 2", function() {
829 | const meta = {
830 | transformer,
831 | crud: true,
832 | fetch(url, opts) {
833 | return new Promise(resolve => resolve({ url, opts }));
834 | },
835 | helpers: {
836 | get(param) {
837 | return [{ id: param.id }, null];
838 | }
839 | }
840 | };
841 | const api = actionFn("/test/", "test", null, ACTIONS, meta);
842 | const expectedEvent = [
843 | {
844 | type: ACTIONS.actionFetch,
845 | syncing: false,
846 | request: {
847 | pathvars: { id: 1 },
848 | params: null
849 | }
850 | },
851 | {
852 | type: ACTIONS.actionSuccess,
853 | syncing: false,
854 | data: { url: "/test/?id=1", opts: null },
855 | origData: { url: "/test/?id=1", opts: null },
856 | request: { pathvars: { id: 1 }, params: null }
857 | }
858 | ];
859 |
860 | return new Promise(resolve => {
861 | api.get({ id: 1 }, resolve)(function(msg) {
862 | expect(expectedEvent).to.have.length.above(0);
863 | const exp = expectedEvent.shift();
864 | expect(msg).to.eql(exp);
865 | }, getState);
866 | }).then(() => expect(expectedEvent).to.have.length(0));
867 | });
868 |
869 | it("check merge params", function() {
870 | let params;
871 | const meta = {
872 | transformer,
873 | fetch: (urlparams, _params) => {
874 | params = _params;
875 | return fetchSuccess();
876 | }
877 | };
878 | const opts = { headers: { One: 1 } };
879 | const api = actionFn("/test", "test", opts, ACTIONS, meta);
880 | return api.request(null, { headers: { Two: 2 } }).then(() => {
881 | expect(params).to.eql({
882 | headers: {
883 | One: 1,
884 | Two: 2
885 | }
886 | });
887 | });
888 | });
889 |
890 | it("check urlOptions", function() {
891 | let urlFetch;
892 | const api = actionFn("/test", "test", null, ACTIONS, {
893 | transformer,
894 | fetch: url => {
895 | urlFetch = url;
896 | return fetchSuccess();
897 | },
898 | urlOptions: {
899 | delimiter: ",",
900 | arrayFormat: "repeat"
901 | }
902 | });
903 | const async = api.request({ id: [1, 2] });
904 | expect(async).to.be.an.instanceof(Promise);
905 | return async.then(() => {
906 | expect(urlFetch).to.eql("/test?id=1,id=2");
907 | });
908 | });
909 |
910 | it("check responseHandler success", function() {
911 | const resp = [];
912 | const api = actionFn("/test", "test", null, ACTIONS, {
913 | transformer,
914 | fetch() {
915 | return fetchSuccess();
916 | },
917 | holder: {
918 | responseHandler(err, data) {
919 | resp.push({ err, data });
920 | }
921 | }
922 | });
923 | return api.request().then(() => {
924 | expect(resp).to.eql([{ err: null, data: { msg: "hello" } }]);
925 | });
926 | });
927 |
928 | it("check responseHandler error", function() {
929 | const resp = [];
930 | const api = actionFn("/test", "test", null, ACTIONS, {
931 | transformer,
932 | fetch() {
933 | return fetchFail();
934 | },
935 | holder: {
936 | responseHandler(err, data) {
937 | resp.push({ err, data });
938 | }
939 | }
940 | });
941 | return api.request().then(null, () => {
942 | expect(resp).to.have.length(1);
943 | expect(resp[0].data).to.not.exist;
944 | expect(resp[0].err).to.be.an.instanceof(Error);
945 | });
946 | });
947 |
948 | it("chained callbacks all resolve", function() {
949 | const meta = {
950 | transformer,
951 | crud: true,
952 | fetch(url, opts) {
953 | return new Promise(resolve => resolve({ url, opts }));
954 | }
955 | };
956 | const api = actionFn("/test/:id", "test", null, ACTIONS, meta);
957 |
958 | let callCount = 0;
959 |
960 | function spy(resolve) {
961 | callCount += 1;
962 | resolve();
963 | }
964 | function none() {}
965 |
966 | function chainedAction(resolve) {
967 | api.get({ id: 1 }, () => spy(resolve))(none, getState);
968 | }
969 |
970 | return new Promise(resolve =>
971 | api.get({ id: 1 }, () => chainedAction(resolve))(none, getState)
972 | ).then(() => expect(callCount).to.have.length.equal(1));
973 | });
974 | });
975 |
--------------------------------------------------------------------------------
/test/adapters_fetch_spec.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /* global describe, it */
4 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */
5 | import { expect, assert } from "chai";
6 | import fetch from "../src/adapters/fetch";
7 |
8 | describe("fetch adapters", function() {
9 | it("check", function() {
10 | let jsonCall = 0;
11 | const fetchApi = (url, opts) =>
12 | new Promise(resolve => {
13 | expect(url).to.eql("url");
14 | expect(opts).to.eql("opts");
15 | resolve({
16 | status: 200,
17 | text() {
18 | jsonCall += 1;
19 | return Promise.resolve("{}");
20 | }
21 | });
22 | });
23 | return fetch(fetchApi)("url", "opts").then(() => {
24 | expect(jsonCall).to.eql(1);
25 | });
26 | });
27 | it("should return the error response as content", function() {
28 | const fetchApi = (url, opts) =>
29 | new Promise(resolve => {
30 | expect(url).to.eql("url");
31 | expect(opts).to.eql("opts");
32 | resolve({
33 | status: 404,
34 | statusText: "Not Found"
35 | });
36 | });
37 | return fetch(fetchApi)("url", "opts").catch(error => {
38 | expect(error).to.eql({ status: 404, statusText: "Not Found" });
39 | });
40 | });
41 | // Sometimes IE9 translates HTTP 204 (No Content)
42 | // into its own internal Status Code 1223.
43 | // redux-api rightly treats this an error unless
44 | // the status is coerced into 204 No Content
45 | it("should normalise IE9 1223 response into 204 No Content ", () => {
46 | const fetchApi = (url, opts) =>
47 | new Promise(resolve => {
48 | expect(url).to.eql("url");
49 | expect(opts).to.eql("opts");
50 | resolve({
51 | status: 1223,
52 | text() {
53 | return Promise.resolve();
54 | }
55 | });
56 | });
57 |
58 | return fetch(fetchApi)("url", "opts")
59 | .then(() => {
60 | assert.isOk(true, "response status 1223 normalised");
61 | })
62 | .catch(() => {
63 | assert.isNotOk("response status 1223 not normalized");
64 | });
65 | });
66 | });
67 |
--------------------------------------------------------------------------------
/test/cache_spec.js:
--------------------------------------------------------------------------------
1 | /* global describe, it */
2 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}], no-void: 0 */
3 |
4 | import { expect } from "chai";
5 | import cache, {
6 | Manager,
7 | setExpire,
8 | getCacheManager,
9 | MockNowDate
10 | } from "../src/utils/cache";
11 |
12 | describe("cache-manager", () => {
13 | it("check empty call", () => {
14 | expect(cache()).to.not.exist;
15 | expect(cache(null)).to.not.exist;
16 | expect(cache(false)).to.not.exist;
17 | });
18 |
19 | it("check cache as true", () => {
20 | const manager = cache(true);
21 | expect(manager === Manager).to.be.true;
22 | expect(manager.id).to.exist;
23 | expect(manager.id({ a: 1, b: 2 })).to.eql("a=1;b=2;");
24 | });
25 |
26 | it("check cache rewrite id", () => {
27 | const manager = cache({
28 | id(params, opts) {
29 | return Manager.id(params) + opts;
30 | }
31 | });
32 | expect(manager.id({ a: 1, b: 2 }, "extra")).to.eql("a=1;b=2;extra");
33 | });
34 |
35 | it("check setExpire", () => {
36 | const date = new Date();
37 | const SECOND = 1000;
38 | MockNowDate.push(date);
39 | const res1 = +setExpire(1, date) - +date;
40 | expect(res1).to.eql(1 * SECOND);
41 |
42 | MockNowDate.push(date);
43 | const res2 = setExpire(false, date);
44 | expect(res2).to.eql(false);
45 |
46 | const date1000 = new Date(date);
47 | date1000.setSeconds(1000 + date1000.getSeconds());
48 | const res3 = setExpire(date1000, date, date);
49 | expect(res3).to.eql(date1000);
50 |
51 | MockNowDate.push(date);
52 | const res4 = setExpire(1);
53 | expect(res4).to.be.instanceof(Date);
54 | expect(+res4 - +date).to.eql(1 * SECOND);
55 | });
56 |
57 | it("check getCacheManager null check", () => {
58 | const ret1 = getCacheManager();
59 | expect(ret1).to.be.null;
60 | });
61 |
62 | it("check getCacheManager only expire without cache", () => {
63 | const ret2 = getCacheManager(1);
64 | expect(ret2.expire).to.be.false; // can't rewrite false expire
65 | expect(ret2.getData).to.be.instanceof(Function);
66 | expect(ret2.id).to.be.instanceof(Function);
67 | });
68 |
69 | it("check getCacheManager full check", () => {
70 | const date = new Date();
71 | const ret3 = getCacheManager(1, { expire: date });
72 |
73 | expect(ret3.expire).to.be.instanceof(Date);
74 | expect(ret3.getData).to.be.instanceof(Function);
75 | expect(ret3.id).to.be.instanceof(Function);
76 | });
77 |
78 | it("check getCacheManager check only cache", () => {
79 | const date = new Date();
80 | const cache = { expire: date };
81 | const ret3 = getCacheManager(undefined, cache);
82 |
83 | expect(ret3.expire).to.be.eql(cache.expire);
84 | expect(ret3.getData).to.be.instanceof(Function);
85 | expect(ret3.id).to.be.instanceof(Function);
86 | });
87 |
88 | it("check Manager.getData empty args", () => {
89 | expect(Manager.getData()).to.not.exist;
90 | });
91 |
92 | it("check Manager.getData with only data cache", () => {
93 | expect(Manager.getData({ data: "Test" })).to.eql("Test");
94 | expect(Manager.getData({ data: "Test", expire: false })).to.eql("Test");
95 | expect(Manager.getData({ data: "Test", expire: null })).to.eql("Test");
96 | });
97 |
98 | it("check Manager.getData with only data cache", () => {
99 | const now = new Date();
100 | const before = new Date(now);
101 | before.setSeconds(before.getSeconds() - 1);
102 | const after = new Date(now);
103 | after.setSeconds(after.getSeconds() + 1);
104 |
105 | MockNowDate.push(now);
106 | expect(Manager.getData({ data: "Test", expire: after })).to.eql("Test");
107 |
108 | MockNowDate.push(now);
109 | expect(Manager.getData({ data: "Test", expire: before })).to.not.exist;
110 | });
111 | });
112 |
--------------------------------------------------------------------------------
/test/createHolder_spec.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /* global describe, it, beforeEach */
4 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */
5 | import { expect } from "chai";
6 | import createHolder from "../src/createHolder";
7 |
8 | describe("Holder", function() {
9 | beforeEach(function() {
10 | this.holder = createHolder();
11 | });
12 | it("default state", function() {
13 | expect(this.holder.empty()).to.be.true;
14 | expect(this.holder.pop()).to.be.undefined;
15 | });
16 | it("normal usage", function() {
17 | const ptr = {};
18 | expect(this.holder.empty()).to.be.true;
19 | expect(this.holder.set(ptr)).to.be.true;
20 | expect(this.holder.empty()).to.be.false;
21 |
22 | expect(this.holder.set(1)).to.be.false;
23 | expect(this.holder.set(null)).to.be.false;
24 | expect(this.holder.pop() === ptr).to.be.true;
25 | expect(this.holder.empty()).to.be.true;
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/test/fetchResolver_spec.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /* global describe, it */
4 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */
5 | import { expect } from "chai";
6 | import isFunction from "lodash/isFunction";
7 | import fetchResolver from "../src/fetchResolver";
8 |
9 | describe("fetchResolver", function() {
10 | it("check import", function() {
11 | expect(isFunction(fetchResolver)).to.be.true;
12 | });
13 | it("check null params", function() {
14 | expect(fetchResolver()).to.be.undefined;
15 | });
16 | it("check with incorrect index§", function() {
17 | expect(fetchResolver(999)).to.be.undefined;
18 | });
19 | it("call without callback", function() {
20 | expect(
21 | fetchResolver(0, {
22 | prefetch: [(opts, cb) => cb()]
23 | })
24 | ).to.be.undefined;
25 | });
26 | it("check normal usage", function() {
27 | const result = [];
28 | const opts = {
29 | prefetch: [
30 | function(opts, cb) {
31 | result.push(["one", opts]);
32 | cb();
33 | },
34 | function(opts, cb) {
35 | result.push(["two", opts]);
36 | cb();
37 | }
38 | ]
39 | };
40 | fetchResolver(0, opts, () => result.push("ok"));
41 | expect(result).to.eql([["one", opts], ["two", opts], "ok"]);
42 | });
43 | it("check usage without prefetch options", function() {
44 | let counter = 0;
45 | fetchResolver(0, {}, () => {
46 | counter += 1;
47 | });
48 | expect(counter).to.eql(1);
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/test/get_spec.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /* global describe, it */
4 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}], no-void: 0 */
5 | import { expect } from "chai";
6 | import get from "../src/utils/get";
7 |
8 | describe("get", function() {
9 | it("check `get` full path", function() {
10 | const obj = {
11 | a: { b: { c: 2 } }
12 | };
13 | const c = get(obj, "a", "b", "c");
14 | expect(c).to.eql(2);
15 | });
16 |
17 | it("check `get` with empty path", function() {
18 | const obj = {
19 | a: { b: { c: { 0: 2 } } }
20 | };
21 | const c = get(obj, "", "a", null, "b", void 0, "c", 0);
22 | expect(c).to.eql(2);
23 | });
24 |
25 | it("check `get` incorrect path", function() {
26 | const obj = {
27 | a: { b: { c: 2 } }
28 | };
29 | const c = get(obj, "c", "b", "a");
30 | expect(c).to.not.exist;
31 | });
32 |
33 | it("check `get` array path", function() {
34 | const obj = {
35 | a: { b: { c: 2 } }
36 | };
37 | const c = get(obj, ["a", "b"], "c");
38 | expect(c).to.eql(2);
39 | });
40 |
41 | it("check `get` array incorrect path", function() {
42 | const obj = {
43 | a: { b: { c: 2 } }
44 | };
45 | const c = get(obj, ["c", "b"], "a");
46 | expect(c).to.not.exist;
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/test/index_spec.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /* global describe, it, xit */
4 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */
5 | import { expect } from "chai";
6 | import isFunction from "lodash/isFunction";
7 | import size from "lodash/size";
8 | import reduxApi from "../src/index";
9 | import transformers from "../src/transformers";
10 |
11 | function getState() {
12 | return { test: { loading: false, data: {} } };
13 | }
14 |
15 | describe("index", function() {
16 | it("check transformers", function() {
17 | expect(transformers.array()).to.eql([]);
18 | expect(transformers.array({ id: 1 })).to.eql([{ id: 1 }]);
19 | expect(transformers.array([1])).to.eql([1]);
20 |
21 | expect(transformers.object()).to.eql({});
22 | expect(transformers.object({ id: 1 })).to.eql({ id: 1 });
23 | expect(transformers.object([1])).to.eql({ data: [1] });
24 | expect(transformers.object("test")).to.eql({ data: "test" });
25 | expect(transformers.object(1)).to.eql({ data: 1 });
26 | expect(transformers.object(true)).to.eql({ data: true });
27 | });
28 | it("check null params", function() {
29 | expect(isFunction(reduxApi)).to.be.true;
30 | const api = reduxApi();
31 | expect(api.actions).to.eql({});
32 | expect(api.reducers).to.eql({});
33 | });
34 | it("check rootUrl", function() {
35 | const urls = [];
36 | function fetchUrl(url) {
37 | urls.push(url);
38 | return new Promise(resolve => resolve({ msg: "hello" }));
39 | }
40 | const res = reduxApi({
41 | test1: "/url1/",
42 | test2: "url2",
43 | test3: "",
44 | test4: "/(:id)"
45 | })
46 | .use("fetch", fetchUrl)
47 | .use("server", false)
48 | .use("rootUrl", "http://api.com/root");
49 |
50 | const res2 = reduxApi({
51 | test1: "/url1/",
52 | test2: "url2",
53 | test3: "",
54 | test4: "/(:id)"
55 | })
56 | .use("fetch", fetchUrl)
57 | .use("server", false)
58 | .use("rootUrl", "http://api.ru/");
59 |
60 | const res3 = reduxApi({
61 | test1: "/url1/"
62 | })
63 | .use("fetch", fetchUrl)
64 | .use("server", false)
65 | .use("rootUrl", (url, params /* , getState */) => {
66 | expect(url).to.eql("/url1/");
67 | expect(params).to.eql({ a: "b" });
68 | return "http://api.net/";
69 | });
70 |
71 | const act = res.actions;
72 | const act2 = res2.actions;
73 | const act3 = res3.actions;
74 | return Promise.all([
75 | act.test1.request(),
76 | act.test2.request(),
77 | act.test3.request(),
78 | act.test4.request({ id: 1 }),
79 | act2.test1.request(),
80 | act2.test2.request(),
81 | act2.test3.request(),
82 | act2.test4.request({ id: 2 }),
83 | act3.test1.request({}, { a: "b" })
84 | ]).then(() => {
85 | expect([
86 | "http://api.com/root/url1/",
87 | "http://api.com/root/url2",
88 | "http://api.com/root/",
89 | "http://api.com/root/1",
90 | "http://api.ru/url1/",
91 | "http://api.ru/url2",
92 | "http://api.ru/",
93 | "http://api.ru/2",
94 | "http://api.net/url1/"
95 | ]).to.eql(urls);
96 | });
97 | });
98 | it("check string url", function() {
99 | function fetchSuccess(url, data) {
100 | expect(url).to.eql("/plain/url");
101 | expect(data).to.eql({});
102 | return new Promise(function(resolve) {
103 | resolve({ msg: "hello" });
104 | });
105 | }
106 | const res = reduxApi({
107 | test: "/plain/url"
108 | }).use("fetch", fetchSuccess);
109 | expect(size(res.actions)).to.eql(1);
110 | expect(size(res.events)).to.eql(1);
111 |
112 | expect(size(res.reducers)).to.eql(1);
113 | expect(res.actions.test).to.exist;
114 | expect(res.events.test).to.have.keys(
115 | "actionFetch",
116 | "actionSuccess",
117 | "actionFail",
118 | "actionReset",
119 | "actionCache",
120 | "actionAbort"
121 | );
122 | expect(res.reducers.test).to.exist;
123 | const expectedEvent = [
124 | {
125 | type: "@@redux-api@test",
126 | syncing: false,
127 | request: { pathvars: undefined, params: {} }
128 | },
129 | {
130 | type: "@@redux-api@test_success",
131 | data: { msg: "hello" },
132 | origData: { msg: "hello" },
133 | syncing: false,
134 | request: { pathvars: undefined, params: {} }
135 | }
136 | ];
137 | return new Promise(resolve => {
138 | const action = res.actions.test(resolve);
139 | function dispatch(msg) {
140 | expect(expectedEvent).to.have.length.above(0);
141 | const exp = expectedEvent.shift();
142 | expect(msg).to.eql(exp);
143 | }
144 | action(dispatch, getState);
145 | }).then(() => {
146 | expect(expectedEvent).to.have.length(0);
147 | });
148 | });
149 | it("check object url", function() {
150 | function fetchSuccess(url, options) {
151 | expect(url).to.eql("/plain/url/1");
152 | expect(options).to.eql({
153 | headers: {
154 | Accept: "application/json"
155 | }
156 | });
157 | return new Promise(function(resolve) {
158 | resolve({ msg: "hello" });
159 | });
160 | }
161 | const res = reduxApi({
162 | test: {
163 | url: "/plain/url/:id",
164 | options: {
165 | headers: {
166 | Accept: "application/json"
167 | }
168 | }
169 | }
170 | }).use("fetch", fetchSuccess);
171 | expect(res.actions.test).to.exist;
172 | expect(res.reducers.test).to.exist;
173 |
174 | const expectedEvent = [
175 | {
176 | type: "@@redux-api@test",
177 | syncing: false,
178 | request: { pathvars: { id: 1 }, params: {} }
179 | },
180 | {
181 | type: "@@redux-api@test_success",
182 | data: { msg: "hello" },
183 | origData: { msg: "hello" },
184 | syncing: false,
185 | request: { pathvars: { id: 1 }, params: {} }
186 | }
187 | ];
188 | return new Promise(resolve => {
189 | const action = res.actions.test({ id: 1 }, resolve);
190 | function dispatch(msg) {
191 | expect(expectedEvent).to.have.length.above(0);
192 | const exp = expectedEvent.shift();
193 | expect(msg).to.eql(exp);
194 | }
195 | action(dispatch, getState);
196 | }).then(() => {
197 | expect(expectedEvent).to.have.length(0);
198 | });
199 | });
200 | it("use provided reducerName when avaliable", function() {
201 | const res = reduxApi({
202 | test: {
203 | reducerName: "foo",
204 | url: "/plain/url/:id",
205 | options: {
206 | headers: {
207 | Accept: "application/json"
208 | }
209 | }
210 | }
211 | }).use("fetch", function fetchSuccess() {});
212 | expect(res.actions.test).to.exist;
213 | expect(res.reducers.test).to.not.exist;
214 | expect(res.reducers.foo).to.exist;
215 | });
216 |
217 | xit("check virtual option with broadcast", function() {
218 | const BROADCAST_ACTION = "BROADCAST_ACTION";
219 | const res = reduxApi({
220 | test: {
221 | url: "/api",
222 | broadcast: [BROADCAST_ACTION],
223 | virtual: true
224 | }
225 | }).use("fetch", function fetchSuccess() {});
226 | expect(res.actions.test).to.exist;
227 | expect(res.reducers.test).to.not.exist;
228 | });
229 |
230 | it("check prefetch options", function() {
231 | const expectUrls = [];
232 | function fetchSuccess(url) {
233 | expectUrls.push(url);
234 | return new Promise(resolve => resolve({ url }));
235 | }
236 | const res = reduxApi({
237 | test: "/test",
238 | test1: {
239 | url: "/test1",
240 | prefetch: [
241 | function(opts, cb) {
242 | opts.actions.test(cb)(opts.dispatch, opts.getState);
243 | }
244 | ]
245 | }
246 | }).use("fetch", fetchSuccess);
247 | return new Promise(resolve => {
248 | const action = res.actions.test1(resolve);
249 | action(function() {}, getState);
250 | }).then(() => {
251 | expect(expectUrls).to.eql(["/test", "/test1"]);
252 | });
253 | });
254 |
255 | it("check helpers", function() {
256 | const result = [];
257 | function getState() {
258 | return {
259 | params: { id: 9, name: "kitty" },
260 | hello: { loading: false, data: {} }
261 | };
262 | }
263 | function dispatch() {}
264 | const res = reduxApi({
265 | hello: {
266 | url: "/test/:name/:id",
267 | helpers: {
268 | test1(id, name) {
269 | return [{ id, name }];
270 | },
271 | test2() {
272 | const { id, name } = this.getState().params;
273 | return [{ id, name }];
274 | },
275 | testSync: {
276 | sync: true,
277 | call(id) {
278 | return [{ id, name: "admin" }, { method: "post" }];
279 | }
280 | }
281 | }
282 | }
283 | }).use("fetch", function(url, opts) {
284 | result.push({ url, opts });
285 | return new Promise(resolve => resolve({ hello: "world" }));
286 | });
287 | const a1 = new Promise(resolve => {
288 | res.actions.hello.test1(2, "lexich", resolve)(dispatch, getState);
289 | });
290 | const a2 = new Promise(resolve => {
291 | res.actions.hello.test2(resolve)(dispatch, getState);
292 | });
293 | const a3 = new Promise(resolve => {
294 | const mockSync = res.actions.hello.sync;
295 | let counter = 0;
296 | res.actions.hello.sync = function(...args) {
297 | counter += 1;
298 | return mockSync.apply(this, args);
299 | };
300 | res.actions.hello.testSync(1, resolve)(dispatch, getState);
301 | expect(counter).to.eql(1);
302 | });
303 | return Promise.all([a1, a2, a3]).then(() => {
304 | expect(result).to.eql([
305 | { url: "/test/lexich/2", opts: {} },
306 | { url: "/test/kitty/9", opts: {} },
307 | { url: "/test/admin/1", opts: { method: "post" } }
308 | ]);
309 | });
310 | });
311 | it("check global options", () => {
312 | let expOpts;
313 | const rest = reduxApi({
314 | test: {
315 | options: {
316 | headers: {
317 | "X-Header": 1
318 | }
319 | },
320 | url: "/api/test"
321 | }
322 | })
323 | .use("options", {
324 | headers: {
325 | Accept: "application/json"
326 | }
327 | })
328 | .use("fetch", (url, options) => {
329 | expOpts = options;
330 | });
331 | rest.actions.test.request();
332 | expect(expOpts).to.eql({
333 | headers: {
334 | Accept: "application/json",
335 | "X-Header": 1
336 | }
337 | });
338 | });
339 | it("check global options as function", () => {
340 | let expOpts;
341 | const rest = reduxApi({
342 | test: {
343 | options: {
344 | headers: {
345 | "X-Header": 1
346 | }
347 | },
348 | url: "/api/test/(:id)"
349 | }
350 | })
351 | .use("options", (url, params /* , getState */) => {
352 | expect(url).to.eql("/api/test/1");
353 | expect(params).to.eql({ a: "b" });
354 | return {
355 | headers: {
356 | Accept: "application/json"
357 | }
358 | };
359 | })
360 | .use("fetch", (url, options) => {
361 | expOpts = options;
362 | });
363 | rest.actions.test.request({ id: 1 }, { a: "b" });
364 | expect(expOpts).to.eql({
365 | a: "b",
366 | headers: {
367 | Accept: "application/json",
368 | "X-Header": 1
369 | }
370 | });
371 | });
372 |
373 | it("check crud option", () => {
374 | const rest = reduxApi({
375 | test: { url: "/test", crud: true }
376 | });
377 | expect(rest.actions.test).to.include.keys(
378 | "get",
379 | "post",
380 | "delete",
381 | "put",
382 | "patch"
383 | );
384 | expect(rest.actions.test).to.include.keys("request", "reset", "sync");
385 | });
386 |
387 | it("check responseHandler option", () => {
388 | const error1 = new Error("bar");
389 | const error2 = new Error("baz");
390 | function fetchSuccess() {
391 | return new Promise(resolve => resolve({ msg: "hello" }));
392 | }
393 | function fetchError() {
394 | return new Promise((resolve, reject) => reject(error2));
395 | }
396 |
397 | let calledSucess = false;
398 | const resSuccess = reduxApi({
399 | hello: "/test/"
400 | })
401 | .use("fetch", fetchSuccess)
402 | .use("responseHandler", (err, data) => {
403 | expect(err).to.equal(null);
404 | expect(data).to.deep.equal({ msg: "hello" });
405 |
406 | calledSucess = true;
407 |
408 | return { modified: true };
409 | });
410 |
411 | let calledError = false;
412 | const resError = reduxApi({
413 | hello: "/test/"
414 | })
415 | .use("fetch", fetchError)
416 | .use("responseHandler", (err, data) => {
417 | expect(err).to.equal(error2);
418 | expect(data).to.equal(undefined);
419 |
420 | calledError = true;
421 |
422 | throw error1;
423 | });
424 |
425 | let calledWithoutReturn = false;
426 | const resWithoutReturn = reduxApi({
427 | hello: "/test/"
428 | })
429 | .use("fetch", fetchSuccess)
430 | .use("responseHandler", (/* err, data */) => {
431 | calledWithoutReturn = true;
432 | });
433 |
434 | return Promise.all([
435 | resSuccess.actions.hello.request().then(res => {
436 | expect(calledSucess).to.true;
437 | expect(res).to.deep.equal({ modified: true });
438 | }),
439 |
440 | resError.actions.hello
441 | .request()
442 | .catch(err => {
443 | return { inCatch: true, err };
444 | })
445 | .then(res => {
446 | expect(calledError).to.true;
447 | expect(res.inCatch).to.true;
448 | expect(res.err).to.equal(error1);
449 | }),
450 |
451 | resWithoutReturn.actions.hello.request().then(res => {
452 | expect(calledWithoutReturn).to.true;
453 | expect(res).to.deep.equal({ msg: "hello" });
454 | })
455 | ]);
456 | });
457 | });
458 |
--------------------------------------------------------------------------------
/test/merge_spec.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /* global describe, it */
4 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}], no-void: 0 */
5 | import { expect } from "chai";
6 | import merge from "../src/utils/merge";
7 |
8 | describe("merge", function() {
9 | it("check null args", function() {
10 | expect(merge()).to.be.null;
11 | expect(merge(void 0)).to.be.undefined;
12 | expect(merge(null)).to.be.null;
13 | expect(merge(null, null)).to.be.null;
14 | expect(merge(null, null, null)).to.be.null;
15 | });
16 |
17 | it("check number", function() {
18 | expect(merge(1)).to.eql(1);
19 | expect(merge(0)).to.eql(0);
20 | expect(merge(1, 0)).to.eql(0);
21 | expect(merge(1, 2)).to.eql(2);
22 | expect(merge(1, 2, 3)).to.eql(3);
23 | });
24 |
25 | it("check string", function() {
26 | expect(merge("Hello")).to.eql("Hello");
27 | expect(merge("Hello", "World")).to.eql("World");
28 | expect(merge("Hello", "World", "Kitty")).to.eql("Kitty");
29 | });
30 |
31 | it("check boolean", function() {
32 | expect(merge(true)).to.eql(true);
33 | expect(merge(true, false)).to.eql(false);
34 | });
35 |
36 | it("merge plain object", function() {
37 | expect(merge({ a: 1 }, { b: 2 })).to.eql({ a: 1, b: 2 });
38 | expect(merge({ a: 1 }, { b: 2 }, { c: 3 })).to.eql({ a: 1, b: 2, c: 3 });
39 | expect(merge({ a: { c: 2 } }, { b: 2 })).to.eql({ a: { c: 2 }, b: 2 });
40 | });
41 |
42 | it("deep merge object", function() {
43 | expect(merge({ a: { b: 1 } }, { a: { c: 2 } })).to.eql({
44 | a: { b: 1, c: 2 }
45 | });
46 | });
47 |
48 | it("merge null with object", function() {
49 | expect(merge(void 0, { a: 1 })).to.eql({ a: 1 });
50 |
51 | expect(merge({ a: 1 }, void 0)).to.eql({ a: 1 });
52 | });
53 |
54 | it("merge array with item", function() {
55 | expect(merge({ id: [1, 2] }, { id: 3 })).to.eql({ id: [1, 2, 3] });
56 |
57 | expect(merge({ id: 3 }, { id: [1, 2] })).to.eql({ id: [3, 1, 2] });
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/test/omit_spec.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /* global describe, it */
4 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */
5 | import { expect } from "chai";
6 | import omit from "../src/utils/omit";
7 |
8 | describe("omit", function() {
9 | it("check without params", function() {
10 | const object = { a: 1, b: 2, c: 3 };
11 | const result = omit(object);
12 | expect(result).to.eql(object);
13 | expect(result !== object).to.be.true;
14 | });
15 | it("check without params", function() {
16 | const object = { a: 1, b: 2, c: 3 };
17 | const result = omit(object, []);
18 | expect(result).to.eql(object);
19 | expect(result !== object).to.be.true;
20 | });
21 | it("check omit", function() {
22 | const object = { a: 1, b: 2, c: 3 };
23 | const result = omit(object, ["a", "b"]);
24 | expect(result).to.eql({ c: 3 });
25 | });
26 | it("check omit", function() {
27 | const object = { a: 1, b: 2, c: 3 };
28 | const result = omit(object, ["a", "b", "d"]);
29 | expect(result).to.eql({ c: 3 });
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/test/reducerFn_spec.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /* global describe, it */
4 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */
5 | import { expect } from "chai";
6 | import isFunction from "lodash/isFunction";
7 | import reducerFn from "../src/reducerFn";
8 |
9 | describe("reducerFn", function() {
10 | it("check null params", function() {
11 | expect(isFunction(reducerFn)).to.be.true;
12 | const fn = reducerFn();
13 | expect(isFunction(fn)).to.be.true;
14 | });
15 | it("check", function() {
16 | const initialState = { loading: false, data: { msg: "Hello" } };
17 | const actions = {
18 | actionFetch: "actionFetch",
19 | actionSuccess: "actionSuccess",
20 | actionFail: "actionFail",
21 | actionReset: "actionReset"
22 | };
23 | const fn = reducerFn(initialState, actions);
24 | const res1 = fn(initialState, { type: actions.actionFetch });
25 | expect({
26 | loading: true,
27 | error: null,
28 | data: { msg: "Hello" },
29 | syncing: false,
30 | request: {}
31 | }).to.eql(res1);
32 |
33 | const res2 = fn(initialState, { type: actions.actionSuccess, data: true });
34 | expect({
35 | loading: false,
36 | error: null,
37 | data: true,
38 | sync: true,
39 | syncing: false
40 | }).to.eql(res2);
41 |
42 | const res3 = fn(initialState, { type: actions.actionFail, error: "Error" });
43 | expect({
44 | loading: false,
45 | error: "Error",
46 | data: { msg: "Hello" },
47 | syncing: false
48 | }).to.eql(res3);
49 |
50 | const res4 = fn(initialState, { type: actions.actionReset });
51 | expect(res4).to.deep.eq(initialState);
52 |
53 | const res5 = fn(undefined, { type: "fake" });
54 | expect(res5).to.deep.eq(initialState);
55 | });
56 |
57 | it("check with path variables", function() {
58 | const initialState = { loading: false, data: { msg: "Hello" } };
59 | const actions = {
60 | actionFetch: "actionFetch",
61 | actionSuccess: "actionSuccess",
62 | actionFail: "actionFail",
63 | actionReset: "actionReset"
64 | };
65 | const fn = reducerFn(initialState, actions);
66 |
67 | const res1 = fn(initialState, {
68 | type: actions.actionFetch,
69 | request: { pathvars: { id: 42 } }
70 | });
71 | expect({
72 | loading: true,
73 | error: null,
74 | data: { msg: "Hello" },
75 | syncing: false,
76 | request: {
77 | pathvars: { id: 42 }
78 | }
79 | }).to.eql(res1);
80 |
81 | const res2 = fn(res1, { type: actions.actionSuccess, data: true });
82 | expect({
83 | loading: false,
84 | error: null,
85 | data: true,
86 | sync: true,
87 | syncing: false,
88 | request: {
89 | pathvars: { id: 42 }
90 | }
91 | }).to.eql(res2);
92 |
93 | const res3 = fn(res1, { type: actions.actionFail, error: "Error" });
94 | expect({
95 | loading: false,
96 | error: "Error",
97 | data: { msg: "Hello" },
98 | syncing: false,
99 | request: {
100 | pathvars: { id: 42 }
101 | }
102 | }).to.eql(res3);
103 |
104 | const res4 = fn(res2, { type: actions.actionReset });
105 | expect(res4).to.deep.eq(initialState);
106 |
107 | const res5 = fn(undefined, { type: "fake" });
108 | expect(res5).to.deep.eq(initialState);
109 | });
110 |
111 | it("check with body", function() {
112 | const initialState = {
113 | loading: false,
114 | request: null,
115 | data: { msg: "Hello" }
116 | };
117 | const actions = {
118 | actionFetch: "actionFetch",
119 | actionSuccess: "actionSuccess",
120 | actionFail: "actionFail",
121 | actionReset: "actionReset"
122 | };
123 | const fn = reducerFn(initialState, actions);
124 |
125 | const res1 = fn(initialState, {
126 | type: actions.actionFetch,
127 | request: {
128 | pathvars: { other: "var" },
129 | params: {
130 | method: "post",
131 | body: { hello: "world", it: { should: { store: " the body" } } }
132 | }
133 | }
134 | });
135 | expect({
136 | loading: true,
137 | error: null,
138 | data: { msg: "Hello" },
139 | syncing: false,
140 | request: {
141 | pathvars: { other: "var" },
142 | params: {
143 | method: "post",
144 | body: { hello: "world", it: { should: { store: " the body" } } }
145 | }
146 | }
147 | }).to.eql(res1);
148 |
149 | const res2 = fn(res1, { type: actions.actionSuccess, data: true });
150 | expect({
151 | loading: false,
152 | error: null,
153 | data: true,
154 | sync: true,
155 | syncing: false,
156 | request: {
157 | pathvars: { other: "var" },
158 | params: {
159 | method: "post",
160 | body: { hello: "world", it: { should: { store: " the body" } } }
161 | }
162 | }
163 | }).to.eql(res2);
164 |
165 | const res3 = fn(res1, { type: actions.actionFail, error: "Error" });
166 | expect({
167 | loading: false,
168 | error: "Error",
169 | data: { msg: "Hello" },
170 | syncing: false,
171 | request: {
172 | pathvars: { other: "var" },
173 | params: {
174 | method: "post",
175 | body: {
176 | hello: "world",
177 | it: { should: { store: " the body" } }
178 | }
179 | }
180 | }
181 | }).to.eql(res3);
182 |
183 | const res4 = fn(res2, { type: actions.actionReset });
184 | expect(res4).to.deep.eq(initialState);
185 |
186 | const res5 = fn(undefined, { type: "fake" });
187 | expect(res5).to.deep.eq(initialState);
188 | });
189 |
190 | it("check injected reducer", function() {
191 | const initialState = { loading: false, data: { msg: "Hello" } };
192 | const actions = {
193 | actionFetch: "actionFetch",
194 | actionSuccess: "actionSuccess",
195 | actionFail: "actionFail",
196 | actionReset: "actionReset"
197 | };
198 | const fn = reducerFn(initialState, actions, (state, action) => {
199 | if (action.type === "CUSTOM") {
200 | return { ...state, data: "custom" };
201 | } else {
202 | return state;
203 | }
204 | });
205 | const res0 = fn(initialState, { type: "NO_WAY" });
206 | expect(res0 === initialState).to.be.true;
207 |
208 | const res1 = fn(initialState, { type: "CUSTOM" });
209 | expect(res1).to.eql({ loading: false, data: "custom" });
210 | });
211 | });
212 |
--------------------------------------------------------------------------------
/test/redux_spec.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /* global describe, it */
4 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}], no-void: 0 */
5 | import {
6 | expect
7 | } from "chai";
8 | import {
9 | createStore,
10 | combineReducers,
11 | applyMiddleware
12 | } from "redux";
13 | import thunk from "redux-thunk";
14 | import after from "lodash/after";
15 | import reduxApi from "../src";
16 | import async from "../src/async"
17 | import {
18 | Manager
19 | } from "../src/utils/cache";
20 |
21 | function storeHelper(rest) {
22 | const reducer = combineReducers(rest.reducers);
23 | const createStoreWithMiddleware = applyMiddleware(thunk)(createStore);
24 | return createStoreWithMiddleware(reducer);
25 | }
26 |
27 | function none() {}
28 |
29 | describe("redux", () => {
30 | it("check redux", () => {
31 | const rest = reduxApi({
32 | test: "/api/url"
33 | }).use("fetch", url => {
34 | return new Promise(resolve => resolve(url));
35 | });
36 | const store = storeHelper(rest);
37 | return new Promise(resolve => {
38 | store.dispatch(rest.actions.test(resolve));
39 | }).then(() => {
40 | expect(store.getState().test.data).to.eql({
41 | data: "/api/url"
42 | });
43 | });
44 | });
45 | it("check async function with redux", () => {
46 | const rest = reduxApi({
47 | test: "/api/url",
48 | test2: "/api/url2"
49 | }).use("fetch", url => {
50 | return new Promise(resolve => resolve(url));
51 | });
52 | const store = storeHelper(rest);
53 | return async (
54 | store.dispatch,
55 | cb => rest.actions.test(cb),
56 | rest.actions.test2
57 | ).then(d => {
58 | expect(d.data).to.eql("/api/url2");
59 | expect(store.getState().test.data).to.eql({
60 | data: "/api/url"
61 | });
62 | expect(store.getState().test2.data).to.eql({
63 | data: "/api/url2"
64 | });
65 | });
66 | });
67 | it("check async 2", done => {
68 | const rest = reduxApi({
69 | test: "/api/url"
70 | }).use("fetch", url => new Promise(resolve => resolve(url)));
71 | const store = storeHelper(rest);
72 |
73 | function testAction() {
74 | return (dispatch, getState) => {
75 | async (dispatch, rest.actions.test)
76 | .then(data => {
77 | expect(getState().test.data).to.eql(data);
78 | done();
79 | })
80 | .catch(done);
81 | };
82 | }
83 | store.dispatch(testAction());
84 | });
85 |
86 | it("check custom middlewareParser", () => {
87 | const rest = reduxApi({
88 | test: "/api/url"
89 | })
90 | .use("fetch", url => new Promise(resolve => resolve(url)))
91 | .use("middlewareParser", ({
92 | getState,
93 | dispatch
94 | }) => ({
95 | getState,
96 | dispatch
97 | }));
98 | const reducer = combineReducers(rest.reducers);
99 |
100 | const cutsomThunkMiddleware = ({
101 | dispatch,
102 | getState
103 | }) => next => action => {
104 | if (typeof action === "function") {
105 | return action({
106 | dispatch,
107 | getState
108 | });
109 | }
110 | return next(action);
111 | };
112 | const createStoreWithMiddleware = applyMiddleware(cutsomThunkMiddleware)(
113 | createStore
114 | );
115 | const store = createStoreWithMiddleware(reducer);
116 | return new Promise(resolve => {
117 | store.dispatch(rest.actions.test(resolve));
118 | }).then(
119 | () => {
120 | expect(store.getState().test.data).to.eql({
121 | data: "/api/url"
122 | });
123 | },
124 | err => expect(null).to.eql(err)
125 | );
126 | });
127 |
128 | it("check double call", done => {
129 | const rest = reduxApi({
130 | test: "/test"
131 | }).use(
132 | "fetch",
133 | url =>
134 | new Promise(resolve => {
135 | setTimeout(() => resolve({
136 | url
137 | }), 100);
138 | })
139 | );
140 |
141 | const expectedAction = [{
142 | type: "@@redux-api@test",
143 | syncing: true,
144 | request: {
145 | pathvars: undefined,
146 | params: {}
147 | }
148 | },
149 | {
150 | data: {
151 | url: "/test"
152 | },
153 | origData: {
154 | url: "/test"
155 | },
156 | type: "@@redux-api@test_success",
157 | syncing: false,
158 | request: {
159 | pathvars: undefined,
160 | params: {}
161 | }
162 | }
163 | ];
164 | const reducer = combineReducers({
165 | ...rest.reducers,
166 | debug(state = {}, action) {
167 | if (!/^@@redux\//.test(action.type)) {
168 | const exp = expectedAction.shift();
169 | expect(action).to.eql(exp);
170 | }
171 | return state;
172 | }
173 | });
174 | const createStoreWithMiddleware = applyMiddleware(thunk)(createStore);
175 | const store = createStoreWithMiddleware(reducer);
176 |
177 | const next = after(2, function () {
178 | store.dispatch(
179 | rest.actions.test.sync((err, data) => {
180 | expect(data).to.eql({
181 | url: "/test"
182 | });
183 | expect(expectedAction).to.have.length(0);
184 | done();
185 | })
186 | );
187 | });
188 |
189 | store
190 | .dispatch(
191 | rest.actions.test.sync((err, data) => {
192 | expect(data).to.eql({
193 | url: "/test"
194 | });
195 | next();
196 | })
197 | )
198 | .catch(none);
199 | store
200 | .dispatch(
201 | rest.actions.test.sync((err, data) => {
202 | expect(data).to.eql({
203 | url: "/test"
204 | });
205 | next();
206 | })
207 | )
208 | .catch(none);
209 | });
210 |
211 | it("check abort request", () => {
212 | const timeoutPromise = (url, timeout) =>
213 | new Promise(resolve => setTimeout(() => resolve(url), timeout));
214 |
215 | const rest = reduxApi({
216 | test: "/test"
217 | }).use("fetch", url => timeoutPromise(url, 100));
218 |
219 | const reducer = combineReducers(rest.reducers);
220 | const createStoreWithMiddleware = applyMiddleware(thunk)(createStore);
221 | const store = createStoreWithMiddleware(reducer);
222 |
223 | expect({
224 | sync: false,
225 | syncing: false,
226 | loading: false,
227 | data: {},
228 | request: null
229 | }).to.eql(store.getState().test, "Initial state");
230 | const retAborting = store.dispatch(rest.actions.test()).then(
231 | () => expect(false).to.eql(true, "Should be error"),
232 | err => {
233 | expect(err.message).to.eql("Application abort request");
234 | const { error, ...props } = store.getState().test;
235 | expect({
236 | syncing: false,
237 | loading: false,
238 | data: {},
239 | request: null,
240 | sync: false,
241 | }).to.eql(props);
242 | return true;
243 | }
244 | );
245 | expect({
246 | sync: false,
247 | syncing: false,
248 | loading: true,
249 | data: {},
250 | error: null,
251 | request: {
252 | params: undefined,
253 | pathvars: undefined
254 | }
255 | }).to.eql(
256 | store.getState().test,
257 | "State doesn't change, request in process"
258 | );
259 | store.dispatch(rest.actions.test.reset());
260 | expect({
261 | sync: false,
262 | syncing: false,
263 | loading: false,
264 | data: {},
265 | request: null
266 | }).to.eql(store.getState().test, "State after reset");
267 | return retAborting;
268 | });
269 |
270 | it("check reducer option", () => {
271 | let context;
272 | const rest = reduxApi({
273 | external: "/external",
274 | test: {
275 | url: "/test",
276 | reducer(state, action) {
277 | context = this;
278 | if (action.type === this.events.external.actionSuccess) {
279 | return {
280 | ...state,
281 | data: action.data
282 | };
283 | } else {
284 | return state;
285 | }
286 | }
287 | }
288 | }).use("fetch", url => {
289 | return new Promise(resolve => {
290 | resolve({
291 | url
292 | });
293 | });
294 | });
295 |
296 | const store = storeHelper(rest);
297 | expect(store.getState()).to.eql({
298 | external: {
299 | sync: false,
300 | syncing: false,
301 | loading: false,
302 | data: {},
303 | request: null
304 | },
305 | test: {
306 | sync: false,
307 | syncing: false,
308 | loading: false,
309 | data: {},
310 | request: null
311 | }
312 | });
313 |
314 | return new Promise(done => {
315 | store.dispatch(rest.actions.external(done));
316 | }).then(err => {
317 | expect(err).to.not.exist;
318 | expect(context).to.include.keys("actions", "reducers", "events");
319 | expect(store.getState()).to.eql({
320 | external: {
321 | sync: true,
322 | syncing: false,
323 | loading: false,
324 | request: {
325 | params: {},
326 | pathvars: undefined
327 | },
328 | data: {
329 | url: "/external"
330 | },
331 | error: null
332 | },
333 | test: {
334 | sync: false,
335 | syncing: false,
336 | loading: false,
337 | request: null,
338 | data: {
339 | url: "/external"
340 | }
341 | }
342 | });
343 | });
344 | });
345 | it('check reset "sync"', () => {
346 | const rest = reduxApi({
347 | test: "/api/url"
348 | }).use("fetch", url => {
349 | return new Promise(resolve => resolve(url));
350 | });
351 | const store = storeHelper(rest);
352 | return new Promise(resolve => {
353 | store.dispatch(rest.actions.test(resolve));
354 | }).then(() => {
355 | expect(store.getState().test).to.eql({
356 | sync: true,
357 | syncing: false,
358 | loading: false,
359 | data: {
360 | data: "/api/url"
361 | },
362 | request: {
363 | params: {},
364 | pathvars: undefined
365 | },
366 | error: null
367 | });
368 | store.dispatch(rest.actions.test.reset("sync"));
369 | expect(store.getState().test).to.eql({
370 | request: null,
371 | sync: false,
372 | syncing: false,
373 | loading: false,
374 | data: {
375 | data: "/api/url"
376 | },
377 | error: null
378 | });
379 | });
380 | });
381 | it("check result of dispatch", function () {
382 | const rest = reduxApi({
383 | test: "/api/url"
384 | }).use("fetch", url => {
385 | return new Promise(resolve => resolve(url));
386 | });
387 | const store = storeHelper(rest);
388 | const result = store.dispatch(rest.actions.test());
389 | expect(result instanceof Promise).to.be.true;
390 | return result.then(data => {
391 | expect(data).to.eql({
392 | data: "/api/url"
393 | });
394 | });
395 | });
396 | it("check all arguments for transformer", function () {
397 | const expectedArgs = [
398 | [void 0, void 0, void 0],
399 | [
400 | "/api/test1",
401 | void 0,
402 | {
403 | type: "@@redux-api@test1_success",
404 | request: {
405 | pathvars: void 0,
406 | params: void 0
407 | }
408 | }
409 | ],
410 | [
411 | "/api/test2",
412 | "/api/test1",
413 | {
414 | type: "@@redux-api@test1_success",
415 | request: {
416 | pathvars: void 0,
417 | params: void 0
418 | }
419 | }
420 | ],
421 | "none"
422 | ];
423 | const rest = reduxApi({
424 | test1: {
425 | url: "/api/test1",
426 | transformer(data, prevData, opts) {
427 | expect([data, prevData, opts]).to.eql(expectedArgs.shift());
428 | return data;
429 | }
430 | },
431 | test2: {
432 | url: "/api/test2",
433 | reducerName: "test1",
434 | transformer(data, prevData, opts) {
435 | expect([data, prevData, opts]).to.eql(expectedArgs.shift());
436 | return data;
437 | }
438 | }
439 | }).use("fetch", url => new Promise(resolve => resolve(url)));
440 |
441 | const store = storeHelper(rest);
442 | return store
443 | .dispatch(rest.actions.test1())
444 | .then(() => store.dispatch(rest.actions.test2()))
445 | .then(() => expect(expectedArgs).to.eql(["none"]));
446 | });
447 |
448 | it("multiple endpoints", function () {
449 | const fetch = url => Promise.resolve(url);
450 |
451 | const expectedData = [
452 | [void 0, void 0],
453 | ["/test1", {}]
454 | ];
455 | const actualData = [];
456 |
457 | const rest1 = reduxApi({
458 | test: {
459 | url: "/test1",
460 | transformer(data, prevData) {
461 | actualData.push([data, prevData]);
462 | return data ? {
463 | data
464 | } : {};
465 | }
466 | }
467 | }, {
468 | prefix: "r1"
469 | }).use("fetch", fetch);
470 |
471 | const rest2 = reduxApi({
472 | test: "/test2"
473 | }, {
474 | prefix: "r2"
475 | }).use("fetch", fetch);
476 |
477 | const reducer = combineReducers({
478 | r1: combineReducers(rest1.reducers),
479 | r2: combineReducers(rest2.reducers)
480 | });
481 |
482 | const expectedArgs = [
483 | [
484 | "@@redux-api@r1test",
485 | {
486 | r1: {
487 | test: {
488 | sync: false,
489 | syncing: false,
490 | loading: true,
491 | data: {},
492 | error: null,
493 | request: {
494 | params: undefined,
495 | pathvars: undefined
496 | }
497 | }
498 | },
499 | r2: {
500 | test: {
501 | sync: false,
502 | syncing: false,
503 | loading: false,
504 | data: {},
505 | request: null
506 | }
507 | }
508 | }
509 | ],
510 | [
511 | "@@redux-api@r1test_success",
512 | {
513 | r1: {
514 | test: {
515 | sync: true,
516 | syncing: false,
517 | loading: false,
518 | data: {
519 | data: "/test1"
520 | },
521 | error: null,
522 | request: {
523 | params: undefined,
524 | pathvars: undefined
525 | }
526 | }
527 | },
528 | r2: {
529 | test: {
530 | sync: false,
531 | syncing: false,
532 | loading: false,
533 | request: null,
534 | data: {}
535 | }
536 | }
537 | }
538 | ],
539 | [
540 | "@@redux-api@r2test",
541 | {
542 | r1: {
543 | test: {
544 | sync: true,
545 | syncing: false,
546 | loading: false,
547 | data: {
548 | data: "/test1"
549 | },
550 | error: null,
551 | request: {
552 | params: undefined,
553 | pathvars: undefined
554 | }
555 | }
556 | },
557 | r2: {
558 | test: {
559 | sync: false,
560 | syncing: false,
561 | loading: true,
562 | data: {},
563 | error: null,
564 | request: {
565 | params: undefined,
566 | pathvars: undefined
567 | }
568 | }
569 | }
570 | }
571 | ],
572 | [
573 | "@@redux-api@r2test_success",
574 | {
575 | r1: {
576 | test: {
577 | sync: true,
578 | syncing: false,
579 | loading: false,
580 | data: {
581 | data: "/test1"
582 | },
583 | error: null,
584 | request: {
585 | params: undefined,
586 | pathvars: undefined
587 | }
588 | }
589 | },
590 | r2: {
591 | test: {
592 | sync: true,
593 | syncing: false,
594 | loading: false,
595 | data: {
596 | data: "/test2"
597 | },
598 | error: null,
599 | request: {
600 | params: undefined,
601 | pathvars: undefined
602 | }
603 | }
604 | }
605 | }
606 | ]
607 | ];
608 | const receiveArgs = [];
609 |
610 | function midleware({
611 | getState
612 | }) {
613 | return next => action => {
614 | const result = next(action);
615 | if (typeof action !== "function") {
616 | receiveArgs.push([action.type, getState()]);
617 | }
618 | return result;
619 | };
620 | }
621 |
622 | const createStoreWithMiddleware = applyMiddleware(
623 | midleware,
624 | thunk
625 | )(createStore);
626 | const store = createStoreWithMiddleware(reducer);
627 |
628 | return store
629 | .dispatch(rest1.actions.test())
630 | .then(() => store.dispatch(rest2.actions.test()))
631 | .then(() => {
632 | expect(receiveArgs).to.have.length(4);
633 | expect(expectedArgs[0]).to.eql(receiveArgs[0]);
634 | expect(expectedArgs[1]).to.eql(receiveArgs[1]);
635 | expect(expectedArgs[2]).to.eql(receiveArgs[2]);
636 | expect(expectedArgs[3]).to.eql(receiveArgs[3]);
637 |
638 | expect(actualData).to.have.length(2);
639 | expect(actualData[0]).to.eql(expectedData[0]);
640 | expect(actualData[1]).to.eql(expectedData[1]);
641 | });
642 | });
643 |
644 | it("check default cache options", function () {
645 | let requestCount = 0;
646 | const rest = reduxApi({
647 | test: {
648 | url: "/api/:id1/:id2",
649 | cache: true
650 | }
651 | }).use("fetch", url => {
652 | requestCount += 1;
653 | return new Promise(resolve => resolve(url));
654 | });
655 | const store = storeHelper(rest);
656 | return store
657 | .dispatch(rest.actions.test({
658 | id1: 1,
659 | id2: 2
660 | }))
661 | .then(() => {
662 | const state = store.getState();
663 | expect(state.test.cache).to.eql({
664 | "test_id1=1;id2=2;": {
665 | data: "/api/1/2",
666 | expire: false
667 | }
668 | });
669 | return store.dispatch(rest.actions.test({
670 | id1: 1,
671 | id2: 2
672 | }));
673 | })
674 | .then(() => {
675 | expect(requestCount).to.eql(1);
676 | });
677 | });
678 |
679 | it("check cache options with rewrite id", function () {
680 | let requestCount = 0;
681 | const rest = reduxApi({
682 | test: {
683 | url: "/api/:id1/:id2",
684 | cache: {
685 | id(urlparams) {
686 | return Manager.id(urlparams) + "test";
687 | }
688 | }
689 | }
690 | }).use("fetch", url => {
691 | requestCount += 1;
692 | return new Promise(resolve => resolve(url));
693 | });
694 | const store = storeHelper(rest);
695 | return store
696 | .dispatch(rest.actions.test({
697 | id1: 1,
698 | id2: 2
699 | }))
700 | .then(() => {
701 | const state = store.getState();
702 | expect(state.test.cache).to.eql({
703 | "test_id1=1;id2=2;test": {
704 | data: "/api/1/2",
705 | expire: false
706 | }
707 | });
708 | return store.dispatch(rest.actions.test({
709 | id1: 1,
710 | id2: 2
711 | }));
712 | })
713 | .then(() => {
714 | expect(requestCount).to.eql(1);
715 | });
716 | });
717 |
718 | it("check cache options with expire=0 request", function () {
719 | let requestCount = 0;
720 | const rest = reduxApi({
721 | test: {
722 | url: "/api/:id1/:id2",
723 | cache: {
724 | expire: 0
725 | }
726 | }
727 | }).use("fetch", url => {
728 | requestCount += 1;
729 | return new Promise(resolve => resolve(url));
730 | });
731 | const store = storeHelper(rest);
732 | return store
733 | .dispatch(rest.actions.test({
734 | id1: 1,
735 | id2: 2
736 | }))
737 | .then(() => {
738 | const state = store.getState();
739 | const d = state.test.cache["test_id1=1;id2=2;"];
740 | expect(d).to.exist;
741 | expect(d.data).to.eql("/api/1/2");
742 | return store.dispatch(rest.actions.test({
743 | id1: 1,
744 | id2: 2
745 | }));
746 | })
747 | .then(() => {
748 | expect(requestCount).to.eql(2);
749 | });
750 | });
751 |
752 | it("check double call crud alias", function () {
753 | let fetchCounter = 0;
754 | const rest = reduxApi({
755 | test: {
756 | url: "/api/test",
757 | crud: true
758 | }
759 | }).use("fetch", url => {
760 | fetchCounter += 1;
761 | return new Promise(resolve => resolve(url));
762 | });
763 |
764 | const store = storeHelper(rest);
765 | let counter = 0;
766 |
767 | function callback() {
768 | counter += 1;
769 | }
770 |
771 | return store
772 | .dispatch(rest.actions.test.get(callback))
773 | .then(() => store.dispatch(rest.actions.test.put(callback)))
774 | .then(() => {
775 | expect(fetchCounter).to.eql(2, "fetch should be perform twice");
776 | expect(counter).to.eql(2, "call should be perform twice");
777 | });
778 | });
779 |
780 | it("check abort", () => {
781 | const rest = reduxApi({
782 | test: "/api/url"
783 | }).use("fetch", url => {
784 | return new Promise(resolve => setTimeout(() => resolve(url), 10));
785 | });
786 | const store = storeHelper(rest);
787 |
788 | const ret1 = new Promise((resolve, reject) =>
789 | store.dispatch(rest.actions.test()).then(
790 | () => reject("Abort should generate error"),
791 | err => {
792 | try {
793 | expect("Application abort request").to.eql(err.message);
794 | expect({
795 | sync: false,
796 | syncing: false,
797 | loading: false,
798 | data: {},
799 | request: {
800 | params: undefined,
801 | pathvars: undefined
802 | },
803 | error: err
804 | }).to.eql(store.getState().test);
805 | resolve();
806 | } catch (e) {
807 | reject(e);
808 | }
809 | }
810 | )
811 | );
812 |
813 | store.dispatch(rest.actions.test.abort());
814 | const ret2 = store.dispatch(rest.actions.test());
815 |
816 | return Promise.all([ret1, ret2]).then(() => {
817 | expect(store.getState().test.data).to.eql({
818 | data: "/api/url"
819 | });
820 | });
821 | });
822 |
823 | it("check force", () => {
824 | const rest = reduxApi({
825 | test: "/api/url"
826 | }).use("fetch", url => {
827 | return new Promise(resolve => setTimeout(() => resolve(url), 10));
828 | });
829 | const store = storeHelper(rest);
830 | const ret1 = store.dispatch(rest.actions.test()).then(
831 | () => Promise.reject("Abort shout generate error"),
832 | err => {
833 | try {
834 | expect("Application abort request").to.eql(err.message);
835 | expect({
836 | sync: false,
837 | syncing: false,
838 | loading: false,
839 | data: {},
840 | request: {
841 | params: undefined,
842 | pathvars: undefined
843 | },
844 | error: err
845 | }).to.eql(store.getState().test);
846 | } catch (err) {
847 | return Promise.reject(err);
848 | }
849 | }
850 | );
851 | const ret2 = store.dispatch(rest.actions.test.force());
852 | return Promise.all([ret1, ret2]).then(() => {
853 | expect(store.getState().test.data).to.eql({
854 | data: "/api/url"
855 | });
856 | });
857 | });
858 |
859 | it("check pathvars", () => {
860 | const rest = reduxApi({
861 | test: "/api/url/:id"
862 | }).use("fetch", url => {
863 | return new Promise(resolve => resolve(url));
864 | });
865 | const store = storeHelper(rest);
866 | const INIT_STATE = {
867 | test: {
868 | request: null,
869 | sync: false,
870 | syncing: false,
871 | loading: false,
872 | data: {}
873 | }
874 | };
875 | expect(INIT_STATE).to.eql(store.getState());
876 |
877 | const STATE_1 = {
878 | test: {
879 | sync: true,
880 | syncing: false,
881 | loading: false,
882 | data: {
883 | data: "/api/url/1"
884 | },
885 | request: {
886 | pathvars: {
887 | id: 1
888 | },
889 | params: {
890 | body: "Test",
891 | headers: ["JSON"]
892 | }
893 | },
894 | error: null
895 | }
896 | };
897 | const STATE_2 = {
898 | test: {
899 | sync: true,
900 | syncing: false,
901 | loading: false,
902 | data: {
903 | data: "/api/url/2"
904 | },
905 | request: {
906 | pathvars: {
907 | id: 2
908 | },
909 | params: {
910 | body: "Test2",
911 | headers: ["XML"]
912 | }
913 | },
914 | error: null
915 | }
916 | };
917 |
918 | return store
919 | .dispatch(
920 | rest.actions.test({
921 | id: 1
922 | }, {
923 | body: "Test",
924 | headers: ["JSON"]
925 | })
926 | )
927 | .then(() => {
928 | expect(STATE_1).to.eql(store.getState());
929 | return store.dispatch(
930 | rest.actions.test({
931 | id: 2
932 | }, {
933 | body: "Test2",
934 | headers: ["XML"]
935 | })
936 | );
937 | })
938 | .then(() => {
939 | expect(STATE_2).to.eql(store.getState());
940 | });
941 | });
942 | });
943 |
--------------------------------------------------------------------------------
/test/urlTransform_spec.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /* global describe, it */
4 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */
5 | import { expect } from "chai";
6 | import urlTransform from "../src/urlTransform";
7 |
8 | describe("urlTransform", function() {
9 | it("check null params", function() {
10 | expect(urlTransform()).to.eql("");
11 | expect(urlTransform(null)).to.eql("");
12 | expect(urlTransform("")).to.eql("");
13 | expect(urlTransform("/test")).to.eql("/test");
14 | });
15 |
16 | it("check replace path", function() {
17 | expect(urlTransform("/test/:id", { id: 1 })).to.eql("/test/1");
18 | expect(urlTransform("/test/:id/hey/:id", { id: 1 })).to.eql(
19 | "/test/1/hey/1"
20 | );
21 | });
22 |
23 | it("check replace path with hostname", function() {
24 | expect(urlTransform("http://localhost:1234/test/:id", { id: 1 })).to.eql(
25 | "http://localhost:1234/test/1"
26 | );
27 | expect(
28 | urlTransform("http://localhost:1234/test/:id/hey/:id", { id: 1 })
29 | ).to.eql("http://localhost:1234/test/1/hey/1");
30 | expect(
31 | urlTransform("http://localhost:1234/test/:id/hey/:id?hello=1", { id: 1 })
32 | ).to.eql("http://localhost:1234/test/1/hey/1?hello=1");
33 | expect(
34 | urlTransform("http://localhost:1234/test/:id/hey/:id?hello=1&world=2", {
35 | id: 1
36 | })
37 | ).to.eql("http://localhost:1234/test/1/hey/1?hello=1&world=2");
38 | });
39 |
40 | it("check optional params path", function() {
41 | expect(urlTransform("/test/:id", { id: 1 })).to.eql("/test/1");
42 | expect(urlTransform("/test/(:id)", { id: 1 })).to.eql("/test/1");
43 | expect(urlTransform("/test/(:id)")).to.eql("/test/");
44 | });
45 |
46 | it("check non-pretty params in path", function() {
47 | expect(urlTransform("/test/(:id)", { id1: 1 })).to.eql("/test/?id1=1");
48 | expect(urlTransform("/test/?hello=1&(:id)", { id1: 1 })).to.eql(
49 | "/test/?hello=1&id1=1"
50 | );
51 | expect(urlTransform("/test/?hello=2(:id)", { id1: 1 })).to.eql(
52 | "/test/?hello=2&id1=1"
53 | );
54 | });
55 |
56 | it("check clean params", function() {
57 | expect(urlTransform("/test/:id")).to.eql("/test/");
58 | expect(urlTransform("/test/:id/")).to.eql("/test/");
59 | expect(urlTransform("/test/(:id)")).to.eql("/test/");
60 | });
61 |
62 | it("accepts url transform options", function() {
63 | const urlOptions = { arrayFormat: "repeat", delimiter: ";" };
64 | expect(urlTransform("/test", { id: [1, 2] }, urlOptions)).to.eql(
65 | "/test?id=1;id=2"
66 | );
67 | expect(urlTransform("/test?id=1;id=2", null, urlOptions)).to.eql(
68 | "/test?id=1;id=2"
69 | );
70 | expect(urlTransform("/test?id=1", { id: [2, 3] }, urlOptions)).to.eql(
71 | "/test?id=1;id=2;id=3"
72 | );
73 | expect(urlTransform("/test?id=1;id=2", { id: [2, 3] }, urlOptions)).to.eql(
74 | "/test?id=1;id=2;id=2;id=3"
75 | );
76 | });
77 |
78 | it("accepts qsParseOptions", function() {
79 | const urlOptions = {
80 | arrayFormat: "repeat",
81 | qsParseOptions: { arrayFormat: "indices" }
82 | };
83 | expect(urlTransform("/t?id[0]=1&id[1]=2", { a: 0 }, urlOptions)).to.eql(
84 | "/t?id=1&id=2&a=0"
85 | );
86 | expect(
87 | urlTransform("/t?id[0]=1&id[1]=2", { a: [1, 2] }, urlOptions)
88 | ).to.eql("/t?id=1&id=2&a=1&a=2");
89 | });
90 |
91 | it("accepts qsStringifyOptions", function() {
92 | const urlOptions = {
93 | arrayFormat: "brackets",
94 | qsStringifyOptions: { arrayFormat: "repeat" }
95 | };
96 | expect(urlTransform("/test", { id: [1, 2] }, {})).to.eql(
97 | "/test?id%5B0%5D=1&id%5B1%5D=2"
98 | );
99 | expect(urlTransform("/test", { id: [1, 2] }, urlOptions)).to.eql(
100 | "/test?id=1&id=2"
101 | );
102 | });
103 | });
104 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const webpack = require("webpack");
4 | const path = require("path");
5 |
6 | const plugins = [
7 | new webpack.DefinePlugin({
8 | "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV)
9 | })
10 | ];
11 |
12 | if (process.env.NODE_ENV === "production") {
13 | plugins.push(
14 | new webpack.LoaderOptionsPlugin({
15 | minimize: true
16 | })
17 | );
18 | }
19 |
20 | module.exports = {
21 | mode: process.env.NODE_ENV,
22 | entry: "./src/index",
23 | output: {
24 | library: "redux-api",
25 | libraryTarget: "umd",
26 | umdNamedDefine: true,
27 | filename: process.env.NODE_ENV === "production" ? "redux-api.min.js" : "redux-api.js",
28 | path: path.resolve(__dirname, "dist")
29 | },
30 | optimization: {
31 | minimize: true
32 | },
33 | devtool: "hidden-source-map",
34 | plugins,
35 | module: {
36 | rules: [{
37 | test: /\.js$/,
38 | exclude: /node_modules/,
39 | use: {
40 | loader: "babel-loader",
41 | }
42 | }]
43 | },
44 | resolve: {
45 | extensions: [".js"]
46 | }
47 | };
48 |
--------------------------------------------------------------------------------