├── .babelrc
├── .eslintrc
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── circle.yml
├── example
├── public
│ └── index.html
├── src
│ ├── actions.js
│ ├── client.js
│ ├── components
│ │ ├── Content.js
│ │ ├── Input.js
│ │ └── User.js
│ ├── containers
│ │ └── App.js
│ ├── reducer.js
│ ├── resources.js
│ ├── server.js
│ └── store
│ │ └── configureStore.js
└── webpack.config.js
├── package.json
├── src
├── createResourceAction.js
├── helpers
│ ├── assign.js
│ ├── assignPolyfill.js
│ ├── fetchResource
│ │ ├── index.js
│ │ ├── normalizeHeaders.js
│ │ ├── serializeDataForContentType.js
│ │ ├── serializeFormData.js
│ │ └── setHeaders.js
│ ├── includes.js
│ ├── mapKeys.js
│ ├── omit.js
│ ├── parseUrl.js
│ ├── splitParams.js
│ └── tryResult.js
├── index.js
└── resourceMiddleware.js
└── test
├── support
├── httpRequestTest.js
└── jsdom.js
└── tests
├── createResourceAction.test.js
└── helpers
├── assignPolyfill.test.js
├── fetchResource
├── fetchResource.test.js
├── normalizeHeaders.test.js
├── serializeDataForContentType.test.js
├── serializeFormData.test.js
└── setHeaders.test.js
├── includes.test.js
├── mapKeys.test.js
├── omit.test.js
├── parseUrl.test.js
├── splitParams.test.js
└── tryResult.test.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "react"],
3 | "plugins": [
4 | "syntax-async-functions",
5 | "transform-export-extensions",
6 | "transform-object-rest-spread",
7 | "transform-regenerator"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "rules": {
4 | "indent": [
5 | 2,
6 | 2
7 | ],
8 | "quotes": [
9 | 2,
10 | "single"
11 | ],
12 | "linebreak-style": [
13 | 2,
14 | "unix"
15 | ],
16 | "no-console": 0,
17 | "no-unused-vars": [2, {"vars": "all", "varsIgnorePattern": "React"}],
18 | "semi": [
19 | 2,
20 | "always"
21 | ]
22 | },
23 | "env": {
24 | "es6": true,
25 | "node": true,
26 | "browser": true
27 | },
28 | "extends": "eslint:recommended",
29 | "plugins": [
30 | "react"
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # node-waf configuration
20 | .lock-wscript
21 |
22 | # Compiled binary addons (http://nodejs.org/api/addons.html)
23 | build/Release
24 |
25 | # Dependency directory
26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
27 | node_modules
28 | lib/
29 | example/public/bundle.js
30 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 | ### v1.1.0 - 2015-12-30
3 |
4 | **New Features**
5 |
6 | * Headers can be supplied to the object literal definition for a resource
7 | * Form data can be sent with POST requests when using
8 | `application/x-www-form-urlencoded` for the `Content-Type` header
9 |
10 | ### v1.0.0 - 2015-12-16
11 |
12 | **Initial Release**
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Jeremy Fairbank
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # redux-resource
2 |
3 | [](https://circleci.com/gh/jfairbank/redux-resource)
4 |
5 | Easily create actions for managing server resources like fetching, creating, or
6 | updating. Provide action types used in your reducer function for updating your
7 | redux store based on results from the server.
8 |
9 | ### \*\* DEPRECATED \*\*
10 |
11 | This package is no longer maintained. The redux-resource package name will be
12 | given to a different team managing a similar type of package for managing remote
13 | resources in Redux. More info to come soon.
14 |
15 | ## Install
16 |
17 | $ npm install redux-resource
18 |
19 | ## Usage
20 |
21 | Use `createResourceAction` to create an action for fetching a resource. Supply a
22 | URL (with optional path parameter placeholders) along with three action types to
23 | represent the act of sending the request, receiving a successful response from
24 | the server, and receiving an error from the server. Make sure to use these
25 | action types in your reducer function!
26 |
27 | ```js
28 | import { createResourceAction } from 'redux-resource';
29 |
30 | const fetchTodo = createResourceAction(
31 | '/todos/:id', 'FETCH_TODO', 'RCV_TODO', 'ERR_RCV_TODO'
32 | );
33 | ```
34 |
35 | When calling the action, pass in any parameters to fill in the placeholders or
36 | add additional search query parameters.
37 |
38 | ```js
39 | // make request to '/todos/42'
40 | store.dispatch(fetchTodo({ id: 42 }));
41 |
42 | // make request to '/todos/42?extraParam=hi'
43 | store.dispatch(fetchTodo({
44 | id: 1,
45 | extraParam: 'hi'
46 | }));
47 | ```
48 |
49 | `createResourceAction` defaults to `GET` requests. To use other verbs swap out
50 | the URL string with an object literal that defines the URL and HTTP verb.
51 |
52 | ```js
53 | const addTodo = createResourceAction(
54 | { url: '/todos/add', method: 'POST' },
55 | 'ADD_TODO', 'ADDED_TODO', 'ERR_ADDING_TODO'
56 | );
57 | ```
58 |
59 | You can also supply headers inside the object literal. This is useful if your
60 | request content type needs to be something other than `application/json`.
61 |
62 | ```js
63 | const resource = {
64 | url: '/todos/add',
65 | method: 'POST',
66 | headers: {
67 | 'Content-Type': 'application/x-www-form-urlencoded'
68 | }
69 | };
70 |
71 | const addTodoFromForm = createResourceAction(
72 | resource, 'ADD_TODO', 'ADDED_TODO', 'ERR_ADDING_TODO'
73 | );
74 | ```
75 |
76 | In addition to taking parameters, the action can take request data. You can
77 | pass in `null` for params if you don't have any.
78 |
79 | ```js
80 | // make request to '/todos/add' with JSON '{"title":"Finish tests"}'
81 | store.dispatch(
82 | addTodo(null, { title: 'Finish tests' })
83 | );
84 |
85 | // make request to '/todos/add' with form data 'title="Add%20more%20features"'
86 | store.dispatch(
87 | addTodoFromForm(null, { title: 'Add more features' })
88 | );
89 | ```
90 |
91 | Actions return a thunk (an anonymous function). To use the actions with your
92 | store you **must** include the middleware. (redux-resource uses
93 | [redux-thunk](https://github.com/gaearon/redux-thunk) under the hood.)
94 |
95 | ```js
96 | // configureStore.js
97 |
98 | import { applyMiddleware, createStore } from 'redux';
99 | import { resourceMiddleware } from 'redux-resource';
100 | import myReducer from './myReducer';
101 |
102 | const createStoreWithMiddleWare = applyMiddleware(resourceMiddleware)(createStore);
103 | const store = createStoreWithMiddleWare(myReducer);
104 | ```
105 |
106 | Make sure to add the action types to your reducer function!
107 |
108 | ```js
109 | function reducer(state = INITIAL_STATE, action) {
110 | switch(action.type) {
111 | case 'FETCH_TODO':
112 | // ...
113 |
114 | case 'RCV_TODO':
115 | return { ...state, todo: action.payload };
116 |
117 | case 'ADD_TODO':
118 | // ...
119 |
120 | case 'ERR_ADDING_TODO':
121 | return { ...state, error: action.payload };
122 |
123 | // etc.
124 |
125 | default:
126 | return state;
127 | }
128 | }
129 | ```
130 |
131 | Dispatching the actions will return a promise that resolves when a request
132 | _succeeds_ or _fails_. Therefore, `catch` won't work on failed requests. Your
133 | reducer should manage how your state reacts to failed requests. This allows you
134 | to gracefully handle state changes from errors for React applications for
135 | example.
136 |
137 | ```js
138 | // Successful request
139 | store.dispatch(fetchTodo({ id: 42 })).then(() => {
140 | const todo = store.getState().todo;
141 | });
142 |
143 | // Failing request
144 | store.dispatch(addTodo(null, { title: 'Finish tests' })).then(() => {
145 | const error = store.getState().error;
146 | });
147 | ```
148 |
149 | ## API
150 |
151 | ### `createResourceAction`
152 |
153 | ```js
154 | createResourceAction(
155 | url: string | {
156 | url: string,
157 | [method: string],
158 | [headers: Object]
159 | },
160 | sendType: string,
161 | successType: string,
162 | errorType: string
163 | )
164 | ```
165 |
166 | `url` - URL for resource. Allows path parameter placeholders like `/users/:id`.
167 |
168 | `sendType` - The action type when dispatching the request.
169 |
170 | `successType` - The action type when successfully receiving back the resource
171 | from the server.
172 |
173 | `errorType` - The action type when a server request fails.
174 |
175 | ## Example
176 |
177 | ```js
178 | import { applyMiddleware, createStore } from 'redux';
179 | import { resourceMiddleware, createResourceAction } from 'redux-resource';
180 |
181 | const createStoreWithMiddleWare = applyMiddleware(resourceMiddleware)(createStore);
182 |
183 | const INITIAL_STATE = {
184 | fetching: false,
185 | user: null,
186 | error: null
187 | };
188 |
189 | function reducer(state = INITIAL_STATE, action) {
190 | switch(action.type) {
191 | case 'FETCH_USER':
192 | return { ...state, fetching: true };
193 |
194 | case 'RECEIVE_USER':
195 | return { ...state, fetching: false, user: action.payload };
196 |
197 | case 'ERR_RECEIVE_USER':
198 | return { ...state, fetching: false, error: action.payload };
199 |
200 | default:
201 | return state;
202 | }
203 | }
204 |
205 | const store = createStoreWithMiddleWare(reducer);
206 |
207 | const fetchUser = createResourceAction(
208 | '/users/:id', 'FETCH_USER', 'RECEIVE_USER', 'ERR_RECEIVE_USER'
209 | );
210 |
211 | store.dispatch(fetchUser({ id: 1 }))
212 | .then(() => console.log(store.getState()));
213 |
214 | // {
215 | // fetching: false,
216 | // user: { id: 1, name: 'Jeremy' },
217 | // error: null
218 | // }
219 | ```
220 |
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | machine:
2 | node:
3 | version: 5.1.0
4 |
--------------------------------------------------------------------------------
/example/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/example/src/actions.js:
--------------------------------------------------------------------------------
1 | import * as users from './resources';
2 | import { createResourceAction } from '../../src';
3 |
4 | export const updateFetchId = (id) => ({
5 | type: 'UPDATE_FETCH_ID',
6 | payload: id
7 | });
8 |
9 | export const updateCreateName = (name) => ({
10 | type: 'UPDATE_CREATE_NAME',
11 | payload: name
12 | });
13 |
14 | export const fetchUser = createResourceAction(
15 | users.user, 'FETCH_USER', 'RECEIVE_USER', 'ERR_RECEIVE_USER'
16 | );
17 |
18 | export const createUser = createResourceAction(
19 | users.create, 'ADD_USER', 'ADDED_USER', 'ERR_ADDING_USER'
20 | );
21 |
22 | export const createUserFromForm = createResourceAction(
23 | users.createFromForm, 'ADD_USER', 'ADDED_USER', 'ERR_ADDING_USER'
24 | );
25 |
--------------------------------------------------------------------------------
/example/src/client.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill';
2 | import React from 'react';
3 | import { render } from 'react-dom';
4 | import { Provider } from 'react-redux';
5 | import App from './containers/App';
6 | import reducer from './reducer';
7 | import configureStore from './store/configureStore';
8 |
9 | const store = configureStore(reducer);
10 |
11 | render(
12 |
13 |
14 | ,
15 | document.getElementById('main')
16 | );
17 |
--------------------------------------------------------------------------------
/example/src/components/Content.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import User from '../components/User';
3 |
4 | const Content = ({ fetching, user, error }) => {
5 | if (fetching) {
6 | return Sending request...
;
7 | }
8 |
9 | if (error) {
10 | return Error: {error.message}
;
11 | }
12 |
13 | if (user) {
14 | return ;
15 | }
16 |
17 | return ;
18 | };
19 |
20 | export default Content;
21 |
--------------------------------------------------------------------------------
/example/src/components/Input.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Input = ({ placeholder, children, onSubmit }) => {
4 | let input;
5 |
6 | return (
7 |
8 | input = i}
10 | placeholder={placeholder}/>
11 | {' '}
12 |
16 |
17 | );
18 | };
19 |
20 | export default Input;
21 |
--------------------------------------------------------------------------------
/example/src/components/User.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const User = ({ id, name, email }) => (
4 |
5 |
{id}: {name}
6 | {email &&
7 | {email}
8 | }
9 |
10 | );
11 |
12 | export default User;
13 |
--------------------------------------------------------------------------------
/example/src/containers/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { bindActionCreators } from 'redux';
3 | import { connect } from 'react-redux';
4 | import Content from '../components/Content';
5 | import Input from '../components/Input';
6 | import * as actions from '../actions';
7 |
8 | export const App = ({ actions, fetching, error, user }) => (
9 |
10 |
11 |
12 | actions.fetchUser({ id })}
15 | >
16 | Fetch User
17 |
18 |
19 | actions.createUser(null, { name })}
22 | >
23 | Create User
24 |
25 |
26 | actions.createUserFromForm(null, { name })}
29 | >
30 | Create User From Form
31 |
32 |
33 | );
34 |
35 | export default connect(
36 | state => state,
37 | dispatch => ({ actions: bindActionCreators(actions, dispatch) })
38 | )(App);
39 |
--------------------------------------------------------------------------------
/example/src/reducer.js:
--------------------------------------------------------------------------------
1 | const match = (expression, matchers, defaultState) => {
2 | for (const key of Object.keys(matchers)) {
3 | if (expression === key) {
4 | return matchers[key]();
5 | }
6 | }
7 |
8 | return defaultState;
9 | };
10 |
11 | export const INITIAL_STATE = {
12 | fetching: false,
13 | user: null,
14 | error: null
15 | };
16 |
17 | export default (state = INITIAL_STATE, action) => match(action.type, {
18 | FETCH_USER: () => ({ ...state, fetching: true, error: null }),
19 | RECEIVE_USER: () => ({ ...state, fetching: false, user: action.payload }),
20 | ERR_RECEIVE_USER: () => ({ ...state, fetching: false, error: action.payload }),
21 | ADD_USER: () => ({ ...state, adding: true, error: null }),
22 | ADDED_USER: () => ({ ...state, adding: false, user: action.payload }),
23 | ERR_ADDING_USER: () => ({ ...state, adding: false, error: action.payload })
24 | }, state);
25 |
--------------------------------------------------------------------------------
/example/src/resources.js:
--------------------------------------------------------------------------------
1 | export const user = '/users/:id';
2 |
3 | export const create = {
4 | url: '/users/create',
5 | method: 'POST'
6 | };
7 |
8 | export const createFromForm = {
9 | url: '/users/create',
10 | method: 'POST',
11 | headers: {
12 | 'Content-Type': 'application/x-www-form-urlencoded'
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/example/src/server.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import express from 'express';
3 | import bodyParser from 'body-parser';
4 |
5 | const HOST = '127.0.0.1';
6 | const PORT = 3000;
7 | const URL = `http://${HOST}:${PORT}`;
8 |
9 | const createUser = (() => {
10 | let id = 1;
11 |
12 | return (name, email) => ({ name, email, id: id++ });
13 | })();
14 |
15 | const users = _([
16 | createUser('Bob', 'bob@example.com'),
17 | createUser('Alice', 'alice@example.com'),
18 | createUser('Jeremy', 'jeremy@example.com')
19 | ])
20 | .map(user => [user.id, user])
21 | .zipObject()
22 | .value();
23 |
24 | const app = express();
25 |
26 | app.use(express.static('public'));
27 | app.use(bodyParser.urlencoded({ extended: false }));
28 | app.use(bodyParser.json());
29 |
30 | app.get('/users', (req, res) => {
31 | res.send(_.values(users));
32 | });
33 |
34 | app.post('/users/create', (req, res) => {
35 | res.send({
36 | ...req.body,
37 | id: 4
38 | });
39 | });
40 |
41 | app.get('/users/:id', (req, res) => {
42 | const user = users[req.params.id];
43 |
44 | if (!user) {
45 | res
46 | .status(404)
47 | .type('application/json')
48 | .send({ message: 'User not found' });
49 | } else {
50 | res
51 | .type('application/json')
52 | .send(user);
53 | }
54 | });
55 |
56 | app.listen(PORT, HOST, (err) => {
57 | if (err) {
58 | console.error('Error starting server');
59 | } else {
60 | console.log(`Server listening at ${URL}`);
61 | }
62 | });
63 |
--------------------------------------------------------------------------------
/example/src/store/configureStore.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill';
2 | import { applyMiddleware, createStore } from 'redux';
3 | import { resourceMiddleware } from '../../../src';
4 |
5 | const createStoreWithMiddleWare = applyMiddleware(resourceMiddleware)(createStore);
6 |
7 | export default function configureStore(reducer, initialState) {
8 | return createStoreWithMiddleWare(reducer, initialState);
9 | }
10 |
--------------------------------------------------------------------------------
/example/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 |
3 | module.exports = {
4 | devtool: '#inline-source-map',
5 |
6 | entry: path.resolve(__dirname, 'src/client'),
7 |
8 | output: {
9 | path: path.resolve(__dirname, 'public'),
10 | filename: 'bundle.js'
11 | },
12 |
13 | module: {
14 | loaders: [
15 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel' }
16 | ]
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-resource",
3 | "version": "1.1.0",
4 | "description": "Easily create redux actions for managing server resources",
5 | "main": "lib/index.js",
6 | "files": [
7 | "src",
8 | "lib"
9 | ],
10 | "scripts": {
11 | "build": "babel src --out-dir lib",
12 | "clean": "rm -rf lib",
13 | "prepublish": "npm run clean && npm run build",
14 | "test": "NODE_PATH=NODE_PATH:. babel-tape-runner 'test/tests/**/*.test.js'",
15 | "dev:test": "NODE_PATH=NODE_PATH:. babel-tape-runner 'test/tests/**/*.test.js' | tnyan",
16 | "dev:test:watch": "nodemon -w src -w test --exec 'npm run dev:test'"
17 | },
18 | "repository": {
19 | "type": "git",
20 | "url": "https://github.com/jfairbank/redux-resource.git"
21 | },
22 | "keywords": [
23 | "redux",
24 | "rest",
25 | "resource"
26 | ],
27 | "author": "Jeremy Fairbank (http://jeremyfairbank.com)",
28 | "license": "MIT",
29 | "dependencies": {
30 | "pinkie-promise": "^2.0.0",
31 | "redux-thunk": "^1.0.0"
32 | },
33 | "devDependencies": {
34 | "babel-core": "^6.3.15",
35 | "babel-loader": "^6.2.0",
36 | "babel-plugin-syntax-async-functions": "^6.3.13",
37 | "babel-plugin-transform-export-extensions": "^6.3.13",
38 | "babel-plugin-transform-object-rest-spread": "^6.3.13",
39 | "babel-plugin-transform-regenerator": "^6.3.18",
40 | "babel-plugin-transform-runtime": "^6.3.13",
41 | "babel-polyfill": "^6.3.14",
42 | "babel-preset-es2015": "^6.3.13",
43 | "babel-preset-react": "^6.3.13",
44 | "babel-runtime": "^6.3.13",
45 | "babel-tape-runner": "^2.0.0",
46 | "body-parser": "^1.14.1",
47 | "express": "^4.13.3",
48 | "jsdom": "^7.2.1",
49 | "lodash": "^3.10.1",
50 | "nodemon": "^1.8.1",
51 | "react": "^0.14.3",
52 | "react-dom": "^0.14.3",
53 | "react-redux": "^4.0.6",
54 | "redux": "^3.0.4",
55 | "sinon": "^1.17.2",
56 | "tap-nyan": "0.0.2",
57 | "tape": "^4.2.2",
58 | "webpack": "^1.12.9",
59 | "webpack-dev-server": "^1.14.0"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/createResourceAction.js:
--------------------------------------------------------------------------------
1 | import fetchResource from './helpers/fetchResource';
2 | import omit from './helpers/omit';
3 | import parseUrl from './helpers/parseUrl';
4 | import assign from './helpers/assign';
5 |
6 | export default function createResourceAction(
7 | options, sendType, successType, errorType
8 | ) {
9 | let rawUrl, urlCompiler;
10 |
11 | if (typeof options === 'string') {
12 | rawUrl = options;
13 | options = {};
14 | } else {
15 | rawUrl = options.url;
16 | options = omit(options, ['url']);
17 | }
18 |
19 | if (!rawUrl) {
20 | throw new Error('Please provide a url for the resource');
21 | }
22 |
23 | urlCompiler = parseUrl(rawUrl);
24 |
25 | const resourceSendAction = () => ({ type: sendType });
26 |
27 | const resourceSuccessAction = (resource) => ({
28 | type: successType,
29 | payload: resource
30 | });
31 |
32 | const resourceErrorAction = (error) => ({
33 | type: errorType,
34 | payload: error
35 | });
36 |
37 | return (params, data) => (dispatch) => {
38 | const url = urlCompiler(params);
39 |
40 | dispatch(resourceSendAction());
41 |
42 | return fetchResource(url, assign({}, options, { data })).then(
43 | resource => dispatch(resourceSuccessAction(resource)),
44 | error => dispatch(resourceErrorAction(error))
45 | );
46 | };
47 | }
48 |
--------------------------------------------------------------------------------
/src/helpers/assign.js:
--------------------------------------------------------------------------------
1 | if (Object.assign) {
2 | module.exports = Object.assign;
3 | } else {
4 | module.exports = require('./assignPolyfill');
5 | }
6 |
--------------------------------------------------------------------------------
/src/helpers/assignPolyfill.js:
--------------------------------------------------------------------------------
1 | export default function assign(dest, ...sources) {
2 | sources.forEach((source) => {
3 | Object.keys(source).forEach((key) => {
4 | dest[key] = source[key];
5 | });
6 | });
7 |
8 | return dest;
9 | }
10 |
--------------------------------------------------------------------------------
/src/helpers/fetchResource/index.js:
--------------------------------------------------------------------------------
1 | import Promise from 'pinkie-promise';
2 | import tryResult from '../tryResult';
3 | import assign from '../assign';
4 | import setHeaders from './setHeaders';
5 | import normalizeHeaders from './normalizeHeaders';
6 | import serializeDataForContentType from './serializeDataForContentType';
7 |
8 | const DEFAULT_HEADERS = {
9 | 'content-type': 'application/json'
10 | };
11 |
12 | const DEFAULT_OPTIONS = {
13 | method: 'GET'
14 | };
15 |
16 | export default function fetchResource(url, options = {}) {
17 | return new Promise((resolve, reject) => {
18 | options = assign({}, DEFAULT_OPTIONS, options);
19 |
20 | options.headers = assign(
21 | {},
22 | DEFAULT_HEADERS,
23 | normalizeHeaders(options.headers)
24 | );
25 |
26 | const data = serializeDataForContentType(
27 | options.data,
28 | options.headers['content-type']
29 | );
30 |
31 | const xhr = new XMLHttpRequest();
32 |
33 | xhr.open(options.method, url);
34 |
35 | setHeaders(xhr, options.headers);
36 |
37 | xhr.onload = () => {
38 | if (xhr.statusText !== 'OK') {
39 | reject(tryResult(
40 | () => JSON.parse(xhr.responseText),
41 | () => xhr.responseText
42 | ));
43 | } else {
44 | resolve(JSON.parse(xhr.responseText));
45 | }
46 | };
47 |
48 | xhr.send(data);
49 | });
50 | }
51 |
--------------------------------------------------------------------------------
/src/helpers/fetchResource/normalizeHeaders.js:
--------------------------------------------------------------------------------
1 | import mapKeys from '../mapKeys';
2 |
3 | export default function normalizeHeaders(headers = {}) {
4 | return mapKeys(headers, key => key.toLowerCase().trim());
5 | }
6 |
--------------------------------------------------------------------------------
/src/helpers/fetchResource/serializeDataForContentType.js:
--------------------------------------------------------------------------------
1 | import serializeFormData from './serializeFormData';
2 |
3 | export default function serializeDataForContentType(data, contentType) {
4 | if (!data) {
5 | return;
6 | }
7 |
8 | if (contentType === 'application/x-www-form-urlencoded') {
9 | return serializeFormData(data);
10 | }
11 |
12 | return JSON.stringify(data);
13 | }
14 |
--------------------------------------------------------------------------------
/src/helpers/fetchResource/serializeFormData.js:
--------------------------------------------------------------------------------
1 | export default function serializeFormData(data) {
2 | return Object.keys(data).map((key) => (
3 | `${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`
4 | )).join('&');
5 | }
6 |
--------------------------------------------------------------------------------
/src/helpers/fetchResource/setHeaders.js:
--------------------------------------------------------------------------------
1 | export default function setHeaders(xhr, headers) {
2 | Object.keys(headers).forEach((key) => {
3 | xhr.setRequestHeader(key, headers[key]);
4 | });
5 | }
6 |
--------------------------------------------------------------------------------
/src/helpers/includes.js:
--------------------------------------------------------------------------------
1 | export default function includes(array, value) {
2 | return array.indexOf(value) > -1;
3 | }
4 |
--------------------------------------------------------------------------------
/src/helpers/mapKeys.js:
--------------------------------------------------------------------------------
1 | export default function mapKeys(object, fn) {
2 | return Object.keys(object).reduce((memo, key) => {
3 | memo[fn(key)] = object[key];
4 | return memo;
5 | }, {});
6 | }
7 |
--------------------------------------------------------------------------------
/src/helpers/omit.js:
--------------------------------------------------------------------------------
1 | import includes from './includes';
2 |
3 | export default function omit(object, omittedKeys) {
4 | return Object.keys(object).reduce((memo, key) => {
5 | if (!includes(omittedKeys, key)) {
6 | memo[key] = object[key];
7 | }
8 |
9 | return memo;
10 | }, {});
11 | }
12 |
--------------------------------------------------------------------------------
/src/helpers/parseUrl.js:
--------------------------------------------------------------------------------
1 | import splitParams from './splitParams';
2 |
3 | export default function parseUrl(url) {
4 | const regex = /:\w+/g;
5 | const statics = url.split(regex);
6 | const paramKeys = (url.match(regex) || [])
7 | .map((paramId) => paramId.replace(':', ''));
8 |
9 | return (params) => {
10 | const split = splitParams(params || {}, paramKeys);
11 | const pathParams = split[0];
12 | const queryParams = split[1];
13 |
14 | const finalUrl = statics.reduce((memo, piece, i) => {
15 | const paramKey = paramKeys[i] || '';
16 | const paramId = paramKey ? ':' + paramKey : '';
17 |
18 | return memo + piece + (pathParams[paramKey] || paramId);
19 | }, '');
20 |
21 | if (queryParams) {
22 | return finalUrl + '?' + queryParams;
23 | }
24 |
25 | return finalUrl;
26 | };
27 | }
28 |
--------------------------------------------------------------------------------
/src/helpers/splitParams.js:
--------------------------------------------------------------------------------
1 | import includes from './includes';
2 |
3 | export default function splitParams(params, pathParamKeys) {
4 | const pathParams = {};
5 | const queryParams = [];
6 |
7 | Object.keys(params).forEach((paramKey) => {
8 | if (includes(pathParamKeys, paramKey)) {
9 | pathParams[paramKey] = params[paramKey];
10 | } else {
11 | queryParams.push(`${paramKey}=${encodeURIComponent(params[paramKey])}`);
12 | }
13 | });
14 |
15 | return [pathParams, queryParams.join('&')];
16 | }
17 |
--------------------------------------------------------------------------------
/src/helpers/tryResult.js:
--------------------------------------------------------------------------------
1 | export default function tryResult(expressionFn, catchExpression) {
2 | try {
3 | return expressionFn();
4 | } catch (e) {
5 | return catchExpression(e);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import createResourceAction from './createResourceAction';
2 | import resourceMiddleware from './resourceMiddleware';
3 |
4 | export {
5 | createResourceAction,
6 | resourceMiddleware
7 | };
8 |
--------------------------------------------------------------------------------
/src/resourceMiddleware.js:
--------------------------------------------------------------------------------
1 | export default from 'redux-thunk';
2 |
--------------------------------------------------------------------------------
/test/support/httpRequestTest.js:
--------------------------------------------------------------------------------
1 | import '../support/jsdom';
2 | import test from 'tape';
3 | import sinon from 'sinon';
4 |
5 | export default function httpRequestTest(name, fn) {
6 | test(name, t => {
7 | const dispatch = sinon.spy();
8 | const xhr = sinon.useFakeXMLHttpRequest();
9 | const requests = [];
10 |
11 | xhr.onCreate = (xhr) => requests.push(xhr);
12 |
13 | fn(t, () => requests.slice(0), dispatch);
14 |
15 | xhr.restore();
16 | });
17 | }
18 |
--------------------------------------------------------------------------------
/test/support/jsdom.js:
--------------------------------------------------------------------------------
1 | import { jsdom } from 'jsdom';
2 |
3 | global.document = jsdom('');
4 | global.window = document.defaultView;
5 | global.navigator = global.window.navigator;
6 | global.XMLHttpRequest = global.window.XMLHttpRequest;
7 |
--------------------------------------------------------------------------------
/test/tests/createResourceAction.test.js:
--------------------------------------------------------------------------------
1 | import test from '../support/httpRequestTest';
2 | import createResourceAction from '../../src/createResourceAction';
3 |
4 | const users = {
5 | user: '/users/:id',
6 | create: {
7 | url: '/users/create',
8 | method: 'POST'
9 | }
10 | };
11 |
12 | users.createFromForm = {
13 | ...users.create,
14 | headers: {
15 | 'Content-Type': 'application/x-www-form-urlencoded',
16 | 'X-Custom': 'hello world'
17 | }
18 | };
19 |
20 | const fetchUser = createResourceAction(
21 | users.user, 'FETCH_USER', 'RECEIVE_USER', 'ERR_RECEIVE_USER'
22 | );
23 |
24 | const createUser = createResourceAction(
25 | users.create, 'CREATE_USER', 'CREATED_USER', 'ERR_CREATING_USER'
26 | );
27 |
28 | const createUserFromForm = createResourceAction(
29 | users.createFromForm, 'CREATE_USER', 'CREATED_USER', 'ERR_CREATING_USER'
30 | );
31 |
32 | const params = { id: 1 };
33 |
34 | test('making a request with the default verb', (t, getRequests, dispatch) => {
35 | fetchUser(params)(dispatch);
36 |
37 | const requests = getRequests();
38 | const [req] = requests;
39 |
40 | t.equal(requests.length, 1, 'made the request');
41 | t.equal(req.url, `/users/${params.id}`, 'hit correct url');
42 | t.equal(req.method, 'GET', 'made a GET request');
43 |
44 | t.end();
45 | });
46 |
47 | test('making a request with an explicit verb', (t, getRequests, dispatch) => {
48 | createUser()(dispatch);
49 |
50 | const requests = getRequests();
51 | const [req] = requests;
52 |
53 | t.equal(requests.length, 1, 'made the request');
54 | t.equal(req.url, '/users/create', 'hit correct url');
55 | t.equal(req.method, 'POST', 'made a POST request');
56 |
57 | t.end();
58 | });
59 |
60 | test('making a request with an explicit verb and headers',
61 | (t, getRequests, dispatch) => {
62 | createUserFromForm()(dispatch);
63 |
64 | const requests = getRequests();
65 | const [req] = requests;
66 |
67 | t.equal(requests.length, 1, 'made the request');
68 | t.equal(req.url, '/users/create', 'hit correct url');
69 | t.equal(req.method, 'POST', 'made a POST request');
70 |
71 | t.deepEqual(
72 | Object.keys(req.requestHeaders),
73 | ['content-type', 'x-custom'],
74 | 'uses the supplied headers'
75 | );
76 |
77 | t.ok(
78 | /application\/x-www-form-urlencoded/.test(
79 | req.requestHeaders['content-type']
80 | ),
81 | 'uses the form content type header'
82 | );
83 |
84 | t.equal(
85 | req.requestHeaders['x-custom'],
86 | 'hello world',
87 | 'uses the custom header'
88 | );
89 |
90 | t.end();
91 | }
92 | );
93 |
94 | test('dispatching the send action', (t, getRequests, dispatch) => {
95 | fetchUser(params)(dispatch);
96 |
97 | t.ok(dispatch.calledOnce, 'called once');
98 |
99 | t.ok(
100 | dispatch.calledWith({ type: 'FETCH_USER' }),
101 | 'dispatches send action'
102 | );
103 |
104 | t.end();
105 | });
106 |
107 | test('dispatching the resource upon success', async (t, getRequests, dispatch) => {
108 | const promise = fetchUser(params)(dispatch);
109 | const [req] = getRequests();
110 | const user = { id: params.id, name: 'Jeremy' };
111 |
112 | req.respond(
113 | 200,
114 | { 'Content-Type': 'application/json' },
115 | JSON.stringify(user)
116 | );
117 |
118 | await promise;
119 |
120 | t.ok(dispatch.calledTwice, 'called twice');
121 |
122 | t.ok(
123 | dispatch.calledWith({ type: 'RECEIVE_USER', payload: user }),
124 | 'receives resource'
125 | );
126 |
127 | t.end();
128 | });
129 |
130 | test('dispatching an error upon failure', async (t, getRequests, dispatch) => {
131 | const promise = fetchUser(params)(dispatch);
132 | const [req] = getRequests();
133 | const error = { message: 'Internal Server Error' };
134 |
135 | req.respond(
136 | 500,
137 | { 'Content-Type': 'application/json' },
138 | JSON.stringify(error)
139 | );
140 |
141 | await promise;
142 |
143 | t.ok(dispatch.calledTwice, 'called twice');
144 |
145 | t.ok(
146 | dispatch.calledWith({ type: 'ERR_RECEIVE_USER', payload: error }),
147 | 'receives error'
148 | );
149 |
150 | t.end();
151 | });
152 |
153 | test('sending request body data', (t, getRequests, dispatch) => {
154 | const data = { name: 'Jeremy' };
155 |
156 | createUser(null, data)(dispatch);
157 |
158 | const [req] = getRequests();
159 |
160 | t.deepEqual(JSON.parse(req.requestBody), data, 'called with request body');
161 |
162 | t.end();
163 | });
164 |
165 | test('sending request body form data', (t, getRequests, dispatch) => {
166 | const data = { name: 'Jeremy' };
167 |
168 | createUserFromForm(null, data)(dispatch);
169 |
170 | const [req] = getRequests();
171 |
172 | t.equal(
173 | req.requestBody,
174 | 'name=Jeremy',
175 | 'called with request body'
176 | );
177 |
178 | t.end();
179 | });
180 |
--------------------------------------------------------------------------------
/test/tests/helpers/assignPolyfill.test.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import assign from '../../../src/helpers/assignPolyfill';
3 |
4 | test('assigning from one object', t => {
5 | const source = { hello: 'world' };
6 |
7 | t.deepEqual(assign({}, source), source);
8 |
9 | t.end();
10 | });
11 |
12 | test('assigning from two objects', t => {
13 | const source1 = { hello: 'world' };
14 | const source2 = { hola: 'mundo' };
15 |
16 | t.deepEqual(assign({}, source1, source2), {
17 | hello: 'world',
18 | hola: 'mundo'
19 | });
20 |
21 | t.end();
22 | });
23 |
24 | test('mutation', t => {
25 | const object = {};
26 | const source = { hello: 'world' };
27 |
28 | assign(object, source);
29 |
30 | t.deepEqual(object, source);
31 |
32 | t.end();
33 | });
34 |
--------------------------------------------------------------------------------
/test/tests/helpers/fetchResource/fetchResource.test.js:
--------------------------------------------------------------------------------
1 | import test from '../../../support/httpRequestTest.js';
2 | import fetchResource from 'src/helpers/fetchResource';
3 |
4 | const URL = '/user';
5 |
6 | test('making a request with the default options', (t, getRequests) => {
7 | fetchResource(URL);
8 |
9 | const requests = getRequests();
10 | const [req] = requests;
11 |
12 | t.equal(requests.length, 1, 'make the request');
13 | t.equal(req.url, URL, 'make the request to correct url');
14 | t.equal(req.method, 'GET', 'make a GET request');
15 |
16 | t.deepEqual(
17 | Object.keys(req.requestHeaders),
18 | ['content-type'],
19 | 'uses the defaults headers'
20 | );
21 |
22 | t.ok(
23 | /application\/json/.test(
24 | req.requestHeaders['content-type']
25 | ),
26 | 'defaults to json content type'
27 | );
28 |
29 | t.end();
30 | });
31 |
32 | test('making a request with an explicit verb', (t, getRequests) => {
33 | fetchResource(URL, { method: 'POST' });
34 |
35 | const requests = getRequests();
36 | const [req] = requests;
37 |
38 | t.equal(requests.length, 1, 'make the request');
39 | t.equal(req.url, URL, 'make the request to correct url');
40 | t.equal(req.method, 'POST', 'make a POST request');
41 |
42 | t.deepEqual(
43 | Object.keys(req.requestHeaders),
44 | ['content-type'],
45 | 'uses the defaults headers'
46 | );
47 |
48 | t.ok(
49 | /application\/json/.test(
50 | req.requestHeaders['content-type']
51 | ),
52 | 'defaults to json content type'
53 | );
54 |
55 | t.end();
56 | });
57 |
58 | test('making request with headers', (t, getRequests) => {
59 | fetchResource(URL, {
60 | headers: {
61 | 'Content-Type': 'text/plain',
62 | 'X-Custom': 'hello'
63 | }
64 | });
65 |
66 | const [req] = getRequests();
67 |
68 | t.deepEqual(
69 | req.requestHeaders,
70 | { 'content-type': 'text/plain', 'x-custom': 'hello' },
71 | 'uses the supplied headers'
72 | );
73 |
74 | t.end();
75 | });
76 |
77 | test('sending request body data', (t, getRequests) => {
78 | const data = { name: 'Jeremy' };
79 |
80 | fetchResource(URL, { method: 'POST', data });
81 |
82 | const [req] = getRequests();
83 |
84 | t.deepEqual(JSON.parse(req.requestBody), data, 'call with request body');
85 |
86 | t.end();
87 | });
88 |
89 | test('sending request body form data', (t, getRequests) => {
90 | const data = { name: 'Jeremy' };
91 |
92 | fetchResource(URL, {
93 | data,
94 | method: 'POST',
95 | headers: {
96 | 'Content-Type': 'application/x-www-form-urlencoded'
97 | }
98 | });
99 |
100 | const [req] = getRequests();
101 |
102 | t.equal(
103 | req.requestBody,
104 | 'name=Jeremy',
105 | 'called with request body'
106 | );
107 |
108 | t.end();
109 | });
110 |
--------------------------------------------------------------------------------
/test/tests/helpers/fetchResource/normalizeHeaders.test.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import normalizeHeaders from 'src/helpers/fetchResource/normalizeHeaders';
3 |
4 | test('keys become lower case', t => {
5 | const headers = {
6 | 'Content-Type': 'application/json',
7 | 'ACCEPT': '*/*',
8 | 'host': 'localhost'
9 | };
10 |
11 | t.deepEqual(
12 | Object.keys(normalizeHeaders(headers)),
13 | ['content-type', 'accept', 'host']
14 | );
15 |
16 | t.end();
17 | });
18 |
--------------------------------------------------------------------------------
/test/tests/helpers/fetchResource/serializeDataForContentType.test.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 |
3 | import serializeDataForContentType
4 | from 'src/helpers/fetchResource/serializeDataForContentType';
5 |
6 | test('serializing form data', t => {
7 | const contentType = 'application/x-www-form-urlencoded';
8 | const data = { hello: 'world' };
9 |
10 | t.equal(
11 | serializeDataForContentType(data, contentType),
12 | 'hello=world'
13 | );
14 |
15 | t.end();
16 | });
17 |
18 | test('serializing json data', t => {
19 | const contentType = 'application/json';
20 | const data = { hello: 'world' };
21 |
22 | t.equal(
23 | serializeDataForContentType(data, contentType),
24 | JSON.stringify(data)
25 | );
26 |
27 | t.end();
28 | });
29 |
--------------------------------------------------------------------------------
/test/tests/helpers/fetchResource/serializeFormData.test.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import serializeFormData from 'src/helpers/fetchResource/serializeFormData';
3 |
4 | test('serializing form data', t => {
5 | const data = { hello: 'world', hola: 'mundo' };
6 |
7 | t.equal(
8 | serializeFormData(data),
9 | 'hello=world&hola=mundo'
10 | );
11 |
12 | t.end();
13 | });
14 |
15 | test('encoding special chars', t => {
16 | const data = { '@hello': '#world' };
17 |
18 | t.equal(
19 | serializeFormData(data),
20 | '%40hello=%23world'
21 | );
22 |
23 | t.end();
24 | });
25 |
--------------------------------------------------------------------------------
/test/tests/helpers/fetchResource/setHeaders.test.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import sinon from 'sinon';
3 | import setHeaders from 'src/helpers/fetchResource/setHeaders';
4 |
5 | test('setting headers', t => {
6 | const xhr = {
7 | setRequestHeader() {}
8 | };
9 |
10 | const spy = sinon.spy(xhr, 'setRequestHeader');
11 |
12 | const headers = {
13 | hello: 'world',
14 | hola: 'mundo'
15 | };
16 |
17 | setHeaders(xhr, headers);
18 |
19 | t.deepEqual(
20 | spy.args,
21 | [
22 | ['hello', 'world'],
23 | ['hola', 'mundo']
24 | ]
25 | );
26 |
27 | t.end();
28 | });
29 |
--------------------------------------------------------------------------------
/test/tests/helpers/includes.test.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import includes from '../../../src/helpers/includes';
3 |
4 | test('returns true if the value exists in the array', t => {
5 | t.ok(includes([1, 2, 3], 1));
6 | t.end();
7 | });
8 |
9 | test('checks based on strict equality', t => {
10 | t.notOk(includes([1, 2, 3], '1'));
11 | t.end();
12 | });
13 |
14 | test('returns false if the value is not in the array', t => {
15 | t.notOk(includes([1, 2, 3], 4));
16 | t.end();
17 | });
18 |
--------------------------------------------------------------------------------
/test/tests/helpers/mapKeys.test.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import mapKeys from 'src/helpers/mapKeys';
3 |
4 | test('transforming object with keys', t => {
5 | const before = { FOO: 'BAR', BAZ: 42 };
6 | const after = { foo: 'BAR', baz: 42 };
7 |
8 | t.deepEqual(
9 | mapKeys(before, key => key.toLowerCase()),
10 | after
11 | );
12 |
13 | t.end();
14 | });
15 |
16 | test('transforming empty object', t => {
17 | t.deepEqual(
18 | mapKeys({}, key => key.toUpperCase()),
19 | {}
20 | );
21 |
22 | t.end();
23 | });
24 |
--------------------------------------------------------------------------------
/test/tests/helpers/omit.test.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import omit from '../../../src/helpers/omit';
3 |
4 | test('removing excluded keys', t => {
5 | t.deepEqual(
6 | omit({ hello: 'world', hola: 'mundo' }, ['hello']),
7 | { hola: 'mundo' }
8 | );
9 |
10 | t.end();
11 | });
12 |
13 | test('removing nothing for no keys', t => {
14 | const object = { hello: 'world', hola: 'mundo' };
15 |
16 | t.deepEqual(omit(object, []), object);
17 |
18 | t.end();
19 | });
20 |
--------------------------------------------------------------------------------
/test/tests/helpers/parseUrl.test.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import parseUrl from '../../../src/helpers/parseUrl';
3 |
4 | test('returns a compiled function for generating a url', t => {
5 | t.ok(parseUrl('/users') instanceof Function);
6 | t.end();
7 | });
8 |
9 | test('parsing without placeholders', t => {
10 | const urlCompiler = parseUrl('/users');
11 |
12 | t.equal(urlCompiler(), '/users');
13 |
14 | t.end();
15 | });
16 |
17 | test('parsing with placeholder', t => {
18 | const urlCompiler = parseUrl('/users/:id');
19 |
20 | t.equal(urlCompiler({ id: 1 }), '/users/1', 'Uses id');
21 |
22 | t.end();
23 | });
24 |
25 |
26 | test('parsing with multiple placeholders', t => {
27 | const urlCompiler = parseUrl('/users/:userId/tasks/:taskId');
28 | const params = { userId: 1, taskId: 2 };
29 |
30 | t.equal(urlCompiler(params), '/users/1/tasks/2', 'Uses userId and taskId');
31 |
32 | t.end();
33 | });
34 |
35 | test('passing non-named params', t => {
36 | t.equal(
37 | parseUrl('/users')({ id: 1 }), '/users?id=1', 'Without named parameters'
38 | );
39 |
40 | t.equal(
41 | parseUrl('/users/:id')({ id: 1, name: 'Jeremy' }),
42 | '/users/1?name=Jeremy',
43 | 'With named parameter'
44 | );
45 |
46 | t.end();
47 | });
48 |
--------------------------------------------------------------------------------
/test/tests/helpers/splitParams.test.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import splitParams from '../../../src/helpers/splitParams';
3 |
4 | test('splitting into named and query parameters', t => {
5 | const params = { id: 1, firstName: 'Jeremy', lastName: 'Fairbank' };
6 | const namedParamKeys = ['id'];
7 | const [namedParams, queryParams] = splitParams(params, namedParamKeys);
8 |
9 | t.deepEqual(namedParams, { id: 1 });
10 | t.equal(queryParams, 'firstName=Jeremy&lastName=Fairbank');
11 |
12 | t.end();
13 | });
14 |
15 | test('with no named parameters', t => {
16 | const params = { id: 1, name: 'Jeremy' };
17 | const [namedParams, queryParams] = splitParams(params, []);
18 |
19 | t.deepEqual(namedParams, {});
20 | t.equal(queryParams, 'id=1&name=Jeremy');
21 |
22 | t.end();
23 | });
24 |
25 | test('with no query params', t => {
26 | const params = { id: 1, name: 'Jeremy' };
27 | const namedParamKeys = ['id', 'name'];
28 | const [namedParams, queryParams] = splitParams(params, namedParamKeys);
29 |
30 | t.deepEqual(namedParams, { ...params });
31 | t.equal(queryParams, '');
32 |
33 | t.end();
34 | });
35 |
--------------------------------------------------------------------------------
/test/tests/helpers/tryResult.test.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import tryResult from '../../../src/helpers/tryResult';
3 |
4 | test('returning the value when no error is thrown', t => {
5 | t.equal(tryResult(() => 42), 42);
6 | t.end();
7 | });
8 |
9 | test('returning the catch value when an error is thrown', t => {
10 | const expressionFn = () => { throw new Error('error'); };
11 | const catchFn = (e) => e.message;
12 |
13 | t.equal(tryResult(expressionFn, catchFn), 'error');
14 |
15 | t.end();
16 | });
17 |
--------------------------------------------------------------------------------