├── .eslintrc ├── .gitignore ├── .size-limit ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── babel.config.js ├── jest.config.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── RSAA.js ├── __snapshots__ │ ├── errors.test.js.snap │ ├── middleware.test.js.snap │ ├── util.test.js.snap │ └── validation.test.js.snap ├── errors.js ├── errors.test.js ├── index.js ├── middleware.js ├── middleware.test.js ├── util.js ├── util.test.js ├── validation.js └── validation.test.js └── test └── setupJest.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "prettier", 4 | "jest" 5 | ], 6 | "extends": ["plugin:jest/recommended"], 7 | "env": { 8 | "browser": true, 9 | "es6": true, 10 | "jest/globals": true 11 | }, 12 | "parserOptions": { 13 | "ecmaVersion": 2018, 14 | "sourceType": "module" 15 | }, 16 | "root": true, 17 | "rules": { 18 | "no-underscore-dangle": 0, 19 | "strict": [2, "global"], 20 | "eqeqeq": [2, "smart"], 21 | "no-undef": 2, 22 | "no-console": 1, 23 | "no-nested-ternary": 2, 24 | "jest/expect-expect": 0, 25 | "prettier/prettier": [ 26 | "error", 27 | { 28 | "singleQuote": true 29 | } 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | lib 4 | es 5 | coverage 6 | .nyc_output 7 | .idea 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /.size-limit: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "lib/index.cjs.js (min)", 4 | "path": "lib/index.cjs.js", 5 | "limit": "10 KB", 6 | "gzip": false 7 | }, 8 | { 9 | "name": "lib/index.cjs.js (min + gzip)", 10 | "path": "lib/index.cjs.js", 11 | "limit": "4 KB" 12 | }, 13 | { 14 | "name": "lib/index.umd.js (min)", 15 | "path": "lib/index.umd.js", 16 | "limit": "31 KB", 17 | "gzip": false 18 | }, 19 | { 20 | "name": "lib/index.umd.js (min + gzip)", 21 | "path": "lib/index.umd.js", 22 | "limit": "12 KB" 23 | } 24 | ] -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | - "lts/*" 5 | cache: 6 | directories: 7 | - "node_modules" 8 | script: 9 | - npm run cover 10 | after_success: 11 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 12 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at michael@nason.us. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | 3 | [Node.js](http://nodejs.org/) >= v8 must be installed. 4 | 5 | ## Installation 6 | 7 | - Running `npm install` in the module's root directory will install everything you need for development. 8 | 9 | ## Running Tests 10 | 11 | - `npm test` will run the tests once. 12 | 13 | ## Building 14 | 15 | - `npm run build` will build the module. 16 | 17 | - `npm run clean` will delete built resources. 18 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Alberto Garcia-Raboso 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | redux-api-middleware 2 | ==================== 3 | [![npm version](https://badge.fury.io/js/redux-api-middleware.svg)](https://npm.im/redux-api-middleware) [![npm downloads](https://img.shields.io/npm/dm/redux-api-middleware.svg)](https://npm.im/redux-api-middleware) [![Build Status](https://travis-ci.org/agraboso/redux-api-middleware.svg?branch=master)](https://travis-ci.org/agraboso/redux-api-middleware) [![Coverage Status](https://coveralls.io/repos/agraboso/redux-api-middleware/badge.svg?branch=master&service=github)](https://coveralls.io/github/agraboso/redux-api-middleware?branch=master) [![Package Size](https://badgen.net/bundlephobia/minzip/redux-api-middleware)](https://bundlephobia.com/result?p=redux-api-middleware) 4 | 5 | [Redux middleware](https://redux.js.org/docs/advanced/Middleware.html) for calling an API. 6 | 7 | This middleware receives [*Redux Standard API-calling Actions*](#redux-standard-api-calling-actions) (RSAAs) and dispatches [*Flux Standard Actions*](#flux-standard-actions) (FSAs) to the next middleware. 8 | 9 | RSAAs are identified by the presence of an `[RSAA]` property, where [`RSAA`](#rsaa) is a `String` constant defined in, and exported by `redux-api-middleware`. They contain information describing an API call and three different types of FSAs, known as the *request*, *success* and *failure* FSAs. 10 | 11 | ------------------- 12 | 13 | ## Table of contents 14 | 15 | 16 | 17 | 18 | 19 | - [Introduction](#introduction) 20 | - [Breaking Changes in 2.0 Release](#breaking-changes-in-20-release) 21 | - [Breaking Changes in 3.0 Release](#breaking-changes-in-30-release) 22 | - [Installation](#installation) 23 | - [configureStore.js](#configurestorejs) 24 | - [app.js](#appjs) 25 | - [Usage](#usage) 26 | - [Defining the API call](#defining-the-api-call) 27 | - [`endpoint`](#endpoint-required) 28 | - [`method`](#method-required) 29 | - [`body`](#body) 30 | - [`headers`](#headers) 31 | - [`options`](#options) 32 | - [`credentials`](#credentials) 33 | - [`fetch`](#fetch) 34 | - [Bailing out](#bailing-out) 35 | - [Lifecycle](#lifecycle) 36 | - [Customizing the dispatched FSAs](#customizing-the-dispatched-fsas) 37 | - [Dispatching Thunks](#dispatching-thunks) 38 | - [Testing](#testing) 39 | - [Reference](#reference) 40 | - [*Request* type descriptors](#request-type-descriptors) 41 | - [*Success* type descriptors](#success-type-descriptors) 42 | - [*Failure* type descriptors](#failure-type-descriptors) 43 | - [Exports](#exports) 44 | - [`createAction`](#createactionapicall) 45 | - [`RSAA`](#rsaa) 46 | - [`apiMiddleware`](#apimiddleware) 47 | - [`createMiddleware(options)`](#createmiddlewareoptions) 48 | - [`isRSAA(action)`](#isrsaaaction) 49 | - [`validateRSAA(action)`](#validatersaaaction) 50 | - [`isValidRSAA(action)`](#isvalidrsaaaction) 51 | - [`InvalidRSAA`](#invalidrsaa) 52 | - [`InternalError`](#internalerror) 53 | - [`RequestError`](#requesterror) 54 | - [`ApiError`](#apierror) 55 | - [`getJSON(res)`](#getjsonres) 56 | - [Flux Standard Actions](#flux-standard-actions) 57 | - [`type`](#type) 58 | - [`payload`](#payload) 59 | - [`error`](#error) 60 | - [`meta`](#meta) 61 | - [Redux Standard API-calling Actions](#redux-standard-api-calling-actions) 62 | - [`[RSAA]`](#rsaa) 63 | - [`endpoint`](#endpoint-1) 64 | - [`method`](#method-1) 65 | - [`body`](#body-1) 66 | - [`headers`](#headers-1) 67 | - [`options`](#options-1) 68 | - [`credentials`](#credentials-1) 69 | - [`bailout`](#bailout) 70 | - [`fetch`](#fetch-1) 71 | - [`ok`](#ok) 72 | - [`types`](#types) 73 | - [Type descriptors](#type-descriptors) 74 | - [History](#history) 75 | - [Tests](#tests) 76 | - [Upgrading from v1.0.x](#upgrading-from-v10x) 77 | - [Upgrading from v2.0.x](#upgrading-from-v20x) 78 | - [License](#license) 79 | - [Projects using redux-api-middleware](#projects-using-redux-api-middleware) 80 | - [Acknowledgements](#acknowledgements) 81 | 82 | 83 | 84 | ## Introduction 85 | 86 | The following is a minimal RSAA action: 87 | 88 | ```js 89 | import { createAction } from `redux-api-middleware`; 90 | 91 | createAction({ 92 | endpoint: 'http://www.example.com/api/users', 93 | method: 'GET', 94 | types: ['REQUEST', 'SUCCESS', 'FAILURE'] 95 | }) 96 | ``` 97 | 98 | Upon receiving this action, `redux-api-middleware` will 99 | 100 | 1. check that it is indeed a valid RSAA action; 101 | 2. dispatch the following *request* FSA to the next middleware; 102 | 103 | ```js 104 | { 105 | type: 'REQUEST' 106 | } 107 | ``` 108 | 109 | 3. make a GET request to `http://www.example.com/api/users`; 110 | 4. if the request is successful, dispatch the following *success* FSA to the next middleware; 111 | 112 | ```js 113 | { 114 | type: 'SUCCESS', 115 | payload: { 116 | users: [ 117 | { id: 1, name: 'John Doe' }, 118 | { id: 2, name: 'Jane Doe' }, 119 | ] 120 | } 121 | } 122 | ``` 123 | 124 | 5. if the request is unsuccessful, dispatch the following *failure* FSA to the next middleware. 125 | 126 | ```js 127 | { 128 | type: 'FAILURE', 129 | payload: error // An ApiError object 130 | error: true 131 | } 132 | ``` 133 | 134 | We have tiptoed around error-handling issues here. For a thorough walkthrough of the `redux-api-middleware` lifecycle, see [Lifecycle](#lifecycle) below. 135 | 136 | ### Breaking Changes in 2.0 Release 137 | 138 | See the [2.0 Release Notes](https://github.com/agraboso/redux-api-middleware/releases/tag/v2.0.0), and [Upgrading from v1.0.x](#upgrading-from-v10x) for details on upgrading. 139 | 140 | ### Breaking Changes in 3.0 Release 141 | 142 | See the [3.0 Release Notes](https://github.com/agraboso/redux-api-middleware/releases/tag/v3.0.0), and [Upgrading from v2.0.x](#upgrading-from-v20x) for details on upgrading. 143 | 144 | ## Installation 145 | 146 | `redux-api-middleware` is available on [npm](https://www.npmjs.com/package/redux-api-middleware). 147 | 148 | ``` 149 | $ npm install redux-api-middleware --save 150 | ``` 151 | 152 | To use it, wrap the standard Redux store with it. Here is an example setup. For more information (for example, on how to add several middlewares), consult the [Redux documentation](http://redux.js.org). 153 | 154 | Note: `redux-api-middleware` depends on a [global Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) being available, and may require a polyfill for your runtime environment(s). 155 | 156 | #### configureStore.js 157 | 158 | ```js 159 | import { createStore, applyMiddleware, combineReducers } from 'redux'; 160 | import { apiMiddleware } from 'redux-api-middleware'; 161 | import reducers from './reducers'; 162 | 163 | const reducer = combineReducers(reducers); 164 | const createStoreWithMiddleware = applyMiddleware(apiMiddleware)(createStore); 165 | 166 | export default function configureStore(initialState) { 167 | return createStoreWithMiddleware(reducer, initialState); 168 | } 169 | ``` 170 | 171 | #### app.js 172 | 173 | ```js 174 | const store = configureStore(initialState); 175 | ``` 176 | 177 | ## Usage 178 | 179 | ### Defining the API call 180 | 181 | You can create an API call by creating an action using `createAction` and passing the following options to it. 182 | 183 | #### `endpoint` (Required) 184 | 185 | The URL endpoint for the API call. 186 | 187 | It is usually a string, be it a plain old one or an ES2015 template string. It may also be a function taking the state of your Redux store as its argument, and returning such a string. 188 | 189 | #### `method` (Required) 190 | 191 | The HTTP method for the API call. 192 | 193 | It must be one of the strings `GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE` or `OPTIONS`, in any mixture of lowercase and uppercase letters. 194 | 195 | #### `body` 196 | 197 | The body of the API call. 198 | 199 | `redux-api-middleware` uses the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to make the API call. `body` should hence be a valid body according to the [fetch specification](https://fetch.spec.whatwg.org). In most cases, this will be a JSON-encoded string or a [`FormData`](https://developer.mozilla.org/en/docs/Web/API/FormData) object. 200 | 201 | It may also be a function taking the state of your Redux store as its argument, and returning a body as described above. 202 | 203 | #### `headers` 204 | 205 | The HTTP headers for the API call. 206 | 207 | It is usually an object, with the keys specifying the header names and the values containing their content. For example, you can let the server know your call contains a JSON-encoded string body in the following way. 208 | 209 | ```js 210 | createAction({ 211 | // ... 212 | headers: { 'Content-Type': 'application/json' } 213 | // ... 214 | }) 215 | ``` 216 | 217 | It may also be a function taking the state of your Redux store as its argument, and returning an object of headers as above. 218 | 219 | #### `options` 220 | 221 | The fetch options for the API call. What options are available depends on what fetch implementation is in use. See [MDN fetch](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch) or [node-fetch](https://github.com/bitinn/node-fetch#options) for more information. 222 | 223 | It is usually an object with the options keys/values. For example, you can specify a network timeout for node.js code 224 | in the following way. 225 | 226 | ```js 227 | createAction({ 228 | // ... 229 | options: { timeout: 3000 } 230 | // ... 231 | }) 232 | ``` 233 | 234 | It may also be a function taking the state of your Redux store as its argument, and returning an object of options as above. 235 | 236 | #### `credentials` 237 | 238 | Whether or not to send cookies with the API call. 239 | 240 | It must be one of the following strings: 241 | 242 | - `omit` is the default, and does not send any cookies; 243 | - `same-origin` only sends cookies for the current domain; 244 | - `include` always send cookies, even for cross-origin calls. 245 | 246 | #### `fetch` 247 | 248 | A custom [Fetch](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch) implementation, useful for intercepting the fetch request to customize the response status, modify the response payload or skip the request altogether and provide a cached response instead. 249 | 250 | If provided, the fetch option must be a function that conforms to the Fetch API. Otherwise, the global fetch will be used. 251 | 252 | **Examples:** 253 | 254 |
255 | Modify a response payload and status 256 | 257 | ```js 258 | createAction({ 259 | // ... 260 | fetch: async (...args) => { 261 | // `fetch` args may be just a Request instance or [URI, options] (see Fetch API docs above) 262 | const res = await fetch(...args); 263 | const json = await res.json(); 264 | 265 | return new Response( 266 | JSON.stringify({ 267 | ...json, 268 | // Adding to the JSON response 269 | foo: 'bar' 270 | }), 271 | { 272 | // Custom success/error status based on an `error` key in the API response 273 | status: json.error ? 500 : 200, 274 | headers: { 275 | 'Content-Type': 'application/json' 276 | } 277 | } 278 | ); 279 | } 280 | // ... 281 | }) 282 | ``` 283 |
284 | 285 |
286 | Modify a response status based on response json 287 | 288 | ```js 289 | createAction({ 290 | // ... 291 | fetch: async (...args) => { 292 | const res = await fetch(...args); 293 | const returnRes = res.clone(); // faster then above example with JSON.stringify 294 | const json = await res.json(); // we need json just to check status 295 | 296 | returnRes.status = json.error ? 500 : 200; 297 | 298 | return returnRes; 299 | } 300 | // ... 301 | }) 302 | ``` 303 |
304 | 305 |
306 | Skip the request in favor of a cached response 307 | 308 | ```js 309 | createAction({ 310 | // ... 311 | fetch: async (...args) => { 312 | const cached = await getCache('someKey'); 313 | 314 | if (cached) { 315 | // where `cached` is a JSON string: '{"foo": "bar"}' 316 | return new Response(cached, 317 | { 318 | status: 200, 319 | headers: { 320 | 'Content-Type': 'application/json' 321 | } 322 | } 323 | ); 324 | } 325 | 326 | // Fetch as usual if not cached 327 | return fetch(...args); 328 | } 329 | // ... 330 | }) 331 | ``` 332 |
333 | 334 | ### Bailing out 335 | 336 | In some cases, the data you would like to fetch from the server may already be cached in your Redux store. Or you may decide that the current user does not have the necessary permissions to make some request. 337 | 338 | You can tell `redux-api-middleware` to not make the API call through `bailout` property. If the value is `true`, the RSAA will die here, and no FSA will be passed on to the next middleware. 339 | 340 | A more useful possibility is to give `bailout` a function. At runtime, it will be passed the state of your Redux store as its only argument, if the return value of the function is `true`, the API call will not be made. 341 | 342 | ### Lifecycle 343 | 344 | The `types` property controls the output of `redux-api-middleware`. The simplest form it can take is an array of length 3 consisting of string constants (or symbols), as in our [example](#a-simple-example) above. This results in the default behavior we now describe. 345 | 346 | 1. When `redux-api-middleware` receives an action, it first checks whether it has an `[RSAA]` property. If it does not, it was clearly not intended for processing with `redux-api-middleware`, and so it is unceremoniously passed on to the next middleware. 347 | 348 | 2. It is now time to validate the action against the [RSAA definition](#redux-standard-api-calling-actions). If there are any validation errors, a *request* FSA will be dispatched (if at all possible) with the following properties: 349 | - `type`: the string constant in the first position of the `types` array; 350 | - `payload`: an [`InvalidRSAA`](#invalidrsaa) object containing a list of said validation errors; 351 | - `error: true`. 352 | 353 | `redux-api-middleware` will perform no further operations. In particular, no API call will be made, and the incoming RSAA will die here. 354 | 355 | 3. Now that `redux-api-middleware` is sure it has received a valid RSAA, it will try making the API call. If everything is alright, a *request* FSA will be dispatched with the following property: 356 | - `type`: the string constant in the first position of the `types` array. 357 | 358 | But errors may pop up at this stage, for several reasons: 359 | - `redux-api-middleware` has to call those of `bailout`, `endpoint`, `body`, `options` and `headers` that happen to be a function, which may throw an error; 360 | - `fetch` may throw an error: the RSAA definition is not strong enough to preclude that from happening (you may, for example, send in a `body` that is not valid according to the fetch specification — mind the SHOULDs in the [RSAA definition](#redux-standard-api-calling-actions)); 361 | - a network failure occurs (the network is unreachable, the server responds with an error,...). 362 | 363 | If such an error occurs, a *failure* FSA will be dispatched containing the following properties: 364 | - `type`: the string constant in the last position of the `types` array; 365 | - `payload`: a [`RequestError`](#requesterror) object containing an error message; 366 | - `error: true`. 367 | 368 | 4. If `redux-api-middleware` receives a response from the server with a status code in the 200 range, a *success* FSA will be dispatched with the following properties: 369 | - `type`: the string constant in the second position of the `types` array; 370 | - `payload`: if the `Content-Type` header of the response is set to something JSONy (see [*Success* type descriptors](#success-type-descriptors) below), the parsed JSON response of the server, or undefined otherwise. 371 | 372 | If the status code of the response falls outside that 200 range, a *failure* FSA will dispatched instead, with the following properties: 373 | - `type`: the string constant in the third position of the `types` array; 374 | - `payload`: an [`ApiError`](#apierror) object containing the message `` `${status} - ${statusText}` ``; 375 | - `error: true`. 376 | 377 | ### Customizing the dispatched FSAs 378 | 379 | It is possible to customize the output of `redux-api-middleware` by replacing one or more of the string constants (or symbols) in `types` by a type descriptor. 380 | 381 | A *type descriptor* is a plain JavaScript object that will be used as a blueprint for the dispatched FSAs. As such, type descriptors must have a `type` property, intended to house the string constant or symbol specifying the `type` of the resulting FSAs. 382 | 383 | They may also have `payload` and `meta` properties, which may be of any type. Functions passed as `payload` and `meta` properties of type descriptors will be evaluated at runtime. The signature of these functions should be different depending on whether the type descriptor refers to *request*, *success* or *failure* FSAs — keep reading. 384 | 385 | If a custom `payload` and `meta` function throws an error, `redux-api-middleware` will dispatch an FSA with its `error` property set to `true`, and an `InternalError` object as its `payload`. 386 | 387 | A noteworthy feature of `redux-api-middleware` is that it accepts Promises (or function that return them) in `payload` and `meta` properties of type descriptors, and it will wait for them to resolve before dispatching the FSA — so no need to use anything like `redux-promise`. 388 | 389 | ### Dispatching Thunks 390 | 391 | You can use `redux-thunk` to compose effects, dispatch custom actions on success/error, and implement other types of complex behavior. 392 | 393 | See [the Redux docs on composition](https://github.com/reduxjs/redux-thunk#composition) for more in-depth information, or expand the example below. 394 | 395 |
396 | Example 397 | 398 | ```js 399 | export function patchAsyncExampleThunkChainedActionCreator(values) { 400 | return async (dispatch, getState) => { 401 | const actionResponse = await dispatch(createAction({ 402 | endpoint: "...", 403 | method: "PATCH", 404 | body: JSON.stringify(values), 405 | headers: { 406 | "Accept": "application/json", 407 | "Content-Type": "application/json", 408 | }, 409 | types: [PATCH, PATCH_SUCCESS, PATCH_FAILED] 410 | })); 411 | 412 | if (actionResponse.error) { 413 | // the last dispatched action has errored, break out of the promise chain. 414 | throw new Error("Promise flow received action error", actionResponse); 415 | } 416 | 417 | // you can EITHER return the above resolved promise (actionResponse) here... 418 | return actionResponse; 419 | 420 | // OR resolve another asyncAction here directly and pass the previous received payload value as argument... 421 | return await yourOtherAsyncAction(actionResponse.payload.foo); 422 | }; 423 | } 424 | ``` 425 |
426 | 427 | ### Testing 428 | 429 | To test `redux-api-middleware` calls inside our application, we can create a fetch mock in order to simulate the response of the call. The `fetch-mock` and `redux-mock-store`packages can be used for this purpose as shown in the following example: 430 | 431 | **actions/user.js** 432 | 433 | ```javascript 434 | export const USER_REQUEST = '@@user/USER_REQUEST' 435 | export const USER_SUCCESS = '@@user/USER_SUCCESS' 436 | export const USER_FAILURE = '@@user/USER_FAILURE' 437 | 438 | export const getUser = () => createAction({ 439 | endpoint: 'https://hostname/api/users/', 440 | method: 'GET', 441 | headers: { 'Content-Type': 'application/json' }, 442 | types: [ 443 | USER_REQUEST, 444 | USER_SUCCESS, 445 | USER_FAILURE 446 | ] 447 | }) 448 | ``` 449 | 450 | **actions/user.test.js** 451 | 452 | ```javascript 453 | // This is a Jest test, fyi 454 | 455 | import configureMockStore from 'redux-mock-store' 456 | import { apiMiddleware } from 'redux-api-middleware' 457 | import thunk from 'redux-thunk' 458 | import fetchMock from 'fetch-mock' 459 | 460 | import {getUser} from './user' 461 | 462 | const middlewares = [ thunk, apiMiddleware ] 463 | const mockStore = configureMockStore(middlewares) 464 | 465 | describe('async user actions', () => { 466 | // If we have several tests in our test suit, we might want to 467 | // reset and restore the mocks after each test to avoid unexpected behaviors 468 | afterEach(() => { 469 | fetchMock.reset() 470 | fetchMock.restore() 471 | }) 472 | 473 | it('should dispatch USER_SUCCESS when getUser is called', () => { 474 | // We create a mock store for our test data. 475 | const store = mockStore({}) 476 | 477 | const body = { 478 | email: 'EMAIL', 479 | username: 'USERNAME' 480 | } 481 | // We build the mock for the fetch request. 482 | // beware that the url must match the action endpoint. 483 | fetchMock.getOnce(`https://hostname/api/users/`, {body: body, headers: {'content-type': 'application/json'}}) 484 | // We are going to verify the response with the following actions 485 | const expectedActions = [ 486 | {type: actions.USER_REQUEST}, 487 | {type: actions.USER_SUCCESS, payload: body} 488 | ] 489 | return store.dispatch(actions.getUser()).then(() => { 490 | // Verify that all the actions in the store are the expected ones 491 | expect(store.getActions()).toEqual(expectedActions) 492 | }) 493 | }) 494 | }) 495 | ``` 496 | 497 | ## Reference 498 | 499 | ### *Request* type descriptors 500 | 501 | `payload` and `meta` functions will be passed the RSAA action itself and the state of your Redux store. 502 | 503 | For example, if you want your *request* FSA to have the URL endpoint of the API call in its `payload` property, you can model your RSAA on the following. 504 | 505 | ```js 506 | // Input RSAA 507 | createAction({ 508 | endpoint: 'http://www.example.com/api/users', 509 | method: 'GET', 510 | types: [ 511 | { 512 | type: 'REQUEST', 513 | payload: (action, state) => ({ endpoint: action.endpoint }) 514 | }, 515 | 'SUCCESS', 516 | 'FAILURE' 517 | ] 518 | }) 519 | 520 | // Output request FSA 521 | { 522 | type: 'REQUEST', 523 | payload: { endpoint: 'http://www.example.com/api/users' } 524 | } 525 | ``` 526 | 527 | If you do not need access to the action itself or the state of your Redux store, you may as well just use a static object. For example, if you want the `meta` property to contain a fixed message saying where in your application you're making the request, you can do this. 528 | 529 | ```js 530 | // Input RSAA 531 | createAction({ 532 | endpoint: 'http://www.example.com/api/users', 533 | method: 'GET', 534 | types: [ 535 | { 536 | type: 'REQUEST', 537 | meta: { source: 'userList' } 538 | }, 539 | 'SUCCESS', 540 | 'FAILURE' 541 | ] 542 | }) 543 | 544 | // Output request FSA 545 | { 546 | type: 'REQUEST', 547 | meta: { source: 'userList' } 548 | } 549 | ``` 550 | 551 | By default, *request* FSAs will not contain `payload` and `meta` properties. 552 | 553 | Error *request* FSAs might need to obviate these custom settings though. 554 | - *Request* FSAs resulting from invalid RSAAs (step 2 in [Lifecycle](#lifecycle) above) cannot be customized. `redux-api-middleware` will try to dispatch an error *request* FSA, but it might not be able to (it may happen that the invalid RSAA does not contain a value that can be used as the *request* FSA `type` property, in which case `redux-api-middleware` will let the RSAA die silently). 555 | - *Request* FSAs resulting in request errors (step 3 in [Lifecycle](#lifecycle) above) will honor the user-provided `meta`, but will ignore the user-provided `payload`, which is reserved for the default error object. 556 | 557 | ### *Success* type descriptors 558 | 559 | `payload` and `meta` functions will be passed the RSAA action itself, the state of your Redux store, and the raw server response. 560 | 561 | For example, if you want to process the JSON response of the server using [`normalizr`](https://github.com/gaearon/normalizr), you can do it as follows. 562 | 563 | ```js 564 | import { Schema, arrayOf, normalize } from 'normalizr'; 565 | const userSchema = new Schema('users'); 566 | 567 | // Input RSAA 568 | createAction({ 569 | endpoint: 'http://www.example.com/api/users', 570 | method: 'GET', 571 | types: [ 572 | 'REQUEST', 573 | { 574 | type: 'SUCCESS', 575 | payload: (action, state, res) => { 576 | const contentType = res.headers.get('Content-Type'); 577 | if (contentType && ~contentType.indexOf('json')) { 578 | // Just making sure res.json() does not raise an error 579 | return res.json().then(json => normalize(json, { users: arrayOf(userSchema) })); 580 | } 581 | } 582 | }, 583 | 'FAILURE' 584 | ] 585 | }) 586 | 587 | // Output success FSA 588 | { 589 | type: 'SUCCESS', 590 | payload: { 591 | result: [1, 2], 592 | entities: { 593 | users: { 594 | 1: { 595 | id: 1, 596 | name: 'John Doe' 597 | }, 598 | 2: { 599 | id: 2, 600 | name: 'Jane Doe' 601 | } 602 | } 603 | } 604 | } 605 | } 606 | ``` 607 | 608 | The above pattern of parsing the JSON body of the server response is probably quite common, so `redux-api-middleware` exports a utility function `getJSON` which allows for the above `payload` function to be written as 609 | ```js 610 | (action, state, res) => 611 | getJSON(res) 612 | .then(json => normalize(json, { users: arrayOf(userSchema) })); 613 | ``` 614 | 615 | By default, *success* FSAs will not contain a `meta` property, while their `payload` property will be evaluated from 616 | ```js 617 | (action, state, res) => getJSON(res) 618 | ``` 619 | 620 | ### *Failure* type descriptors 621 | 622 | `payload` and `meta` functions will be passed the RSAA action itself, the state of your Redux store, and the raw server response — exactly as for *success* type descriptors. The `error` property of dispatched *failure* FSAs will always be set to `true`. 623 | 624 | For example, if you want the status code and status message of a unsuccessful API call in the `meta` property of your *failure* FSA, do the following. 625 | 626 | ```js 627 | createAction({ 628 | endpoint: 'http://www.example.com/api/users/1', 629 | method: 'GET', 630 | types: [ 631 | 'REQUEST', 632 | 'SUCCESS', 633 | { 634 | type: 'FAILURE', 635 | meta: (action, state, res) => { 636 | if (res) { 637 | return { 638 | status: res.status, 639 | statusText: res.statusText 640 | }; 641 | } else { 642 | return { 643 | status: 'Network request failed' 644 | } 645 | } 646 | } 647 | } 648 | ] 649 | }) 650 | ``` 651 | 652 | By default, *failure* FSAs will not contain a `meta` property, while their `payload` property will be evaluated from 653 | ```js 654 | (action, state, res) => 655 | getJSON(res) 656 | .then(json => new ApiError(res.status, res.statusText, json)) 657 | ``` 658 | 659 | 660 | Note that *failure* FSAs dispatched due to fetch errors will not have a `res` argument into `meta` or `payload`. The `res` parameter will exist for completed requests that have resulted in errors, but not for failed requests. 661 | 662 | ### Exports 663 | 664 | The following objects are exported by `redux-api-middleware`. 665 | 666 | #### `createAction(apiCall)` 667 | 668 | Function used to create RSAA action. This is the preferred way to create a RSAA action. 669 | 670 | #### `RSAA` 671 | 672 | A JavaScript `String` whose presence as a key in an action signals that `redux-api-middleware` should process said action. 673 | 674 | #### `apiMiddleware` 675 | 676 | The Redux middleware itself. 677 | 678 | #### `createMiddleware(options)` 679 | 680 | A function that creates an `apiMiddleware` with custom options. 681 | 682 | The following `options` properties are used: 683 | 684 | - `fetch` - provide a `fetch` API compatible function here to use instead of the default `window.fetch` 685 | - `ok` - provide a function here to use as a status check in the RSAA flow instead of `(res) => res.ok` 686 | 687 | #### `isRSAA(action)` 688 | 689 | A function that returns `true` if `action` has an `[RSAA]` property, and `false` otherwise. 690 | 691 | #### `validateRSAA(action)` 692 | 693 | A function that validates `action` against the RSAA definition, returning an array of validation errors. 694 | 695 | #### `isValidRSAA(action)` 696 | 697 | A function that returns `true` if `action` conforms to the RSAA definition, and `false` otherwise. Internally, it simply checks the length of the array of validation errors returned by `validateRSAA(action)`. 698 | 699 | #### `InvalidRSAA` 700 | 701 | An error class extending the native `Error` object. Its constructor takes an array of validation errors as its only argument. 702 | 703 | `InvalidRSAA` objects have three properties: 704 | 705 | - `name: 'InvalidRSAA'`; 706 | - `validationErrors`: the argument of the call to its constructor; and 707 | - `message: 'Invalid RSAA'`. 708 | 709 | #### `InternalError` 710 | 711 | An error class extending the native `Error` object. Its constructor takes a string, intended to contain an error message. 712 | 713 | `InternalError` objects have two properties: 714 | 715 | - `name: 'InternalError'`; 716 | - `message`: the argument of the call to its constructor. 717 | 718 | #### `RequestError` 719 | 720 | An error class extending the native `Error` object. Its constructor takes a string, intended to contain an error message. 721 | 722 | `RequestError` objects have two properties: 723 | 724 | - `name: 'RequestError'`; 725 | - `message`: the argument of the call to its constructor. 726 | 727 | #### `ApiError` 728 | 729 | An error class extending the native `Error` object. Its constructor takes three arguments: 730 | 731 | - a status code, 732 | - a status text, and 733 | - a further object, intended for a possible JSON response from the server. 734 | 735 | `ApiError` objects have five properties: 736 | 737 | - `name: 'ApiError'`; 738 | - `status`: the first argument of the call to its constructor; 739 | - `statusText`: the second argument of the call to its constructor; 740 | - `response`: to the third argument of the call to its constructor; and 741 | - `` message : `${status} - ${statusText}` ``. 742 | 743 | #### `getJSON(res)` 744 | 745 | A function taking a response object as its only argument. If the response object contains a JSONy `Content-Type`, it returns a promise resolving to its JSON body. Otherwise, it returns a promise resolving to undefined. 746 | 747 | ### Flux Standard Actions 748 | 749 | For convenience, we recall here the definition of a [*Flux Standard Action*](https://github.com/acdlite/flux-standard-action). 750 | 751 | An action MUST 752 | 753 | - be a plain JavaScript object, 754 | - have a `type` property. 755 | 756 | An action MAY 757 | 758 | - have an `error` property, 759 | - have a `payload` property, 760 | - have a `meta` property. 761 | 762 | An action MUST NOT 763 | 764 | - include properties other than `type`, `payload`, `error` and `meta`. 765 | 766 | #### `type` 767 | 768 | The `type` of an action identifies to the consumer the nature of the action that has occurred. Two actions with the same `type` MUST be strictly equivalent (using `===`). By convention, `type` is usually a string constant or a `Symbol`. 769 | 770 | #### `payload` 771 | 772 | The optional `payload` property MAY be any type of value. It represents the payload of the action. Any information about the action that is not the `type` or status of the action should be part of the `payload` field. 773 | 774 | By convention, if `error` is true, the `payload` SHOULD be an error object. This is akin to rejecting a Promise with an error object. 775 | 776 | #### `error` 777 | 778 | The optional `error` property MAY be set to `true` if the action represents an error. 779 | 780 | An action whose `error` is true is analogous to a rejected Promise. By convention, the `payload` SHOULD be an error object. 781 | 782 | If `error` has any other value besides `true`, including `undefined` and `null`, the action MUST NOT be interpreted as an error. 783 | 784 | #### `meta` 785 | 786 | The optional `meta` property MAY be any type of value. It is intended for any extra information that is not part of the payload. 787 | 788 | ### Redux Standard API-calling Actions 789 | 790 | The definition of a *Redux Standard API-calling Action* below is the one used to validate RSAA actions. As explained in [Lifecycle](#lifecycle), 791 | - actions without an `[RSAA]` property will be passed to the next middleware without any modifications; 792 | - actions with an `[RSAA]` property that fail validation will result in an error *request* FSA. 793 | 794 | A *Redux Standard API-calling Action* MUST 795 | 796 | - be a plain JavaScript object, 797 | - have an `[RSAA]` property. 798 | 799 | A *Redux Standard API-calling Action* MAY 800 | 801 | - include properties other than `[RSAA]` (but will be ignored by redux-api-middleware). 802 | 803 | #### Action object 804 | 805 | The `[RSAA]` property MUST 806 | 807 | - be a plain JavaScript Object, 808 | - have an `endpoint` property, 809 | - have a `method` property, 810 | - have a `types` property. 811 | 812 | The `[RSAA]` property MAY 813 | 814 | - have a `body` property, 815 | - have a `headers` property, 816 | - have an `options` property, 817 | - have a `credentials` property, 818 | - have a `bailout` property, 819 | - have a `fetch` property, 820 | - have an `ok` property. 821 | 822 | The `[RSAA]` property MUST NOT 823 | 824 | - include properties other than `endpoint`, `method`, `types`, `body`, `headers`, `options`, `credentials`, `bailout`, `fetch` and `ok`. 825 | 826 | #### `endpoint` 827 | 828 | The `endpoint` property MUST be a string or a function. In the second case, the function SHOULD return a string. 829 | 830 | #### `method` 831 | 832 | The `method` property MUST be one of the strings `GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE` or `OPTIONS`, in any mixture of lowercase and uppercase letters. 833 | 834 | #### `body` 835 | 836 | The optional `body` property SHOULD be a valid body according to the [fetch specification](https://fetch.spec.whatwg.org), or a function. In the second case, the function SHOULD return a valid body. 837 | 838 | #### `headers` 839 | 840 | The optional `headers` property MUST be a plain JavaScript object or a function. In the second case, the function SHOULD return a plain JavaScript object. 841 | 842 | #### `options` 843 | 844 | The optional `options` property MUST be a plain JavaScript object or a function. In the second case, the function SHOULD return a plain JavaScript object. 845 | The options object can contain any options supported by the effective fetch implementation. 846 | See [MDN fetch](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch) or [node-fetch](https://github.com/bitinn/node-fetch#options). 847 | 848 | #### `credentials` 849 | 850 | The optional `credentials` property MUST be one of the strings `omit`, `same-origin` or `include`. 851 | 852 | #### `bailout` 853 | 854 | The optional `bailout` property MUST be a boolean or a function. 855 | 856 | #### `fetch` 857 | 858 | The optional `fetch` property MUST be a function that conforms to the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). 859 | 860 | #### `ok` 861 | 862 | The optional `ok` property MUST be a function that accepts a response object and returns a boolean indicating if the request is a success or failure 863 | 864 | #### `types` 865 | 866 | The `types` property MUST be an array of length 3. Each element of the array MUST be a string, a `Symbol`, or a type descriptor. 867 | 868 | #### Type descriptors 869 | 870 | A type descriptor MUST 871 | 872 | - be a plain JavaScript object, 873 | - have a `type` property, which MUST be a string or a `Symbol`. 874 | 875 | A type descriptor MAY 876 | 877 | - have a `payload` property, which MAY be of any type, 878 | - have a `meta` property, which MAY be of any type. 879 | 880 | A type descriptor MUST NOT 881 | 882 | - have properties other than `type`, `payload` and `meta`. 883 | 884 | ## History 885 | 886 | TODO 887 | 888 | ## Tests 889 | 890 | ``` 891 | $ npm install && npm test 892 | ``` 893 | 894 | ## Upgrading from v1.0.x 895 | 896 | - The `CALL_API` symbol is replaced with the `RSAA` string as the top-level RSAA action key. `CALL_API` is aliased to the new value as of 2.0, but this will ultimately be deprecated. 897 | - `redux-api-middleware` no longer brings its own `fetch` implementation and depends on a global `fetch` to be provided in the runtime 898 | - A new `options` config is added to pass your `fetch` implementation extra options other than `method`, `headers`, `body` and `credentials` 899 | - `apiMiddleware` no longer returns a promise on actions without [RSAA] 900 | 901 | ## Upgrading from v2.0.x 902 | 903 | - The `CALL_API` alias has been removed 904 | - Error handling around failed fetches has been updated ([#175](https://github.com/agraboso/redux-api-middleware/pull/175)) 905 | - Previously, a failed `fetch` would dispatch a `REQUEST` FSA followed by another `REQUEST` FSA with an error flag 906 | - Now, a failed `fetch` will dispatch a `REQUEST` FSA followed by a `FAILURE` FSA 907 | 908 | ## License 909 | 910 | MIT 911 | 912 | ## Projects using redux-api-middleware 913 | 914 | - [react-trebuchet](https://github.com/barrystaes/react-trebuchet/tree/test-bottledapi-apireduxmiddleware) (experimental/opinionated fork of react-slingshot for SPA frontends using REST JSON API backends) 915 | 916 | If your opensource project uses (or works with) `redux-api-middleware` we would be happy to list it here! 917 | 918 | ## Acknowledgements 919 | 920 | The code in this module was originally extracted from the [real-world](https://github.com/reduxjs/redux/blob/master/examples/real-world/src/middleware/api.js) example in the [redux](https://github.com/rackt/redux) repository, due to [Dan Abramov](https://github.com/gaearon). It has evolved thanks to issues filed by, and pull requests contributed by, other developers. 921 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | const env = api.cache(() => process.env.NODE_ENV); 3 | 4 | const nodeTarget = env === 'test' ? 'current' : '8'; 5 | const envModules = env === 'test' ? 'commonjs' : false; 6 | 7 | const presets = [ 8 | [ 9 | "@babel/preset-env", { 10 | modules: envModules, 11 | "useBuiltIns": "usage", 12 | "targets": { 13 | "node": nodeTarget 14 | }, 15 | } 16 | ] 17 | ]; 18 | 19 | const plugins = []; 20 | 21 | return { 22 | presets, 23 | plugins 24 | }; 25 | } -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verbose: !!process.env.CI, 3 | automock: false, 4 | resetMocks: true, 5 | restoreMocks: true, 6 | resetModules: true, 7 | setupFiles: [ 8 | "./test/setupJest.js" 9 | ], 10 | moduleNameMapper: { 11 | "^redux-api-middleware$": process.env.TEST_LIB ? '..' : './index' 12 | } 13 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-api-middleware", 3 | "version": "3.2.1", 4 | "description": "Redux middleware for calling an API.", 5 | "main": "lib/index.cjs.js", 6 | "browser": "lib/index.umd.js", 7 | "module": "es/index.js", 8 | "sideEffects": false, 9 | "scripts": { 10 | "build": "babel src --out-dir es && rollup -c", 11 | "postbuild": "npm run size", 12 | "clean": "rimraf es lib coverage", 13 | "test": "cross-env NODE_ENV=test jest src es", 14 | "test:build": "cross-env TEST_LIB=true NODE_ENV=test jest src", 15 | "cover": "npm test -- --verbose --coverage --collectCoverageFrom \"src/**/*.js\"", 16 | "lint": "eslint src", 17 | "prepublishOnly": "npm run lint && npm test && npm run clean && npm run build && npm run test:build", 18 | "size": "size-limit" 19 | }, 20 | "repository": "agraboso/redux-api-middleware", 21 | "homepage": "https://github.com/agraboso/redux-api-middleware", 22 | "keywords": [ 23 | "redux", 24 | "api", 25 | "middleware", 26 | "redux-middleware", 27 | "flux" 28 | ], 29 | "author": { 30 | "name": "Alberto Garcia-Raboso", 31 | "email": "agraboso@gmail.com" 32 | }, 33 | "license": "MIT", 34 | "dependencies": {}, 35 | "devDependencies": { 36 | "@babel/cli": "^7.8.4", 37 | "@babel/core": "^7.8.7", 38 | "@babel/preset-env": "^7.8.7", 39 | "@rollup/plugin-commonjs": "^11.0.2", 40 | "@rollup/plugin-node-resolve": "^7.1.1", 41 | "@size-limit/preset-small-lib": "^4.4.0", 42 | "babel-core": "^7.0.0-bridge.0", 43 | "coveralls": "^3.0.9", 44 | "cross-env": "^7.0.2", 45 | "eslint": "^6.8.0", 46 | "eslint-plugin-jest": "^23.8.2", 47 | "eslint-plugin-prettier": "^3.1.2", 48 | "jest": "^23.6.0", 49 | "jest-fetch-mock": "^1.7.5", 50 | "prettier": "^1.19.1", 51 | "rimraf": "^2.7.1", 52 | "rollup": "^1.32.1", 53 | "rollup-plugin-babel": "^4.4.0", 54 | "size-limit": "^4.4.0" 55 | }, 56 | "files": [ 57 | "README.md", 58 | "LICENSE.md", 59 | "es", 60 | "lib" 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import babel from 'rollup-plugin-babel'; 4 | import pkg from './package.json'; 5 | 6 | const pkgDeps = Object.keys(pkg.dependencies) 7 | 8 | export default [ 9 | // browser-friendly UMD build 10 | { 11 | input: 'src/index.js', 12 | output: { 13 | file: pkg.browser, 14 | format: 'umd', 15 | name: 'ReduxApiMiddleware', 16 | }, 17 | plugins: [ 18 | resolve(), 19 | commonjs(), 20 | babel({ 21 | exclude: ['node_modules/**'], 22 | presets: [ 23 | [ 24 | "@babel/preset-env", { 25 | modules: false, 26 | useBuiltIns: "usage" 27 | } 28 | ] 29 | ] 30 | }) 31 | ] 32 | }, 33 | 34 | // CommonJS (for Node) 35 | { 36 | input: 'src/index.js', 37 | output: { 38 | file: pkg.main, 39 | format: 'cjs', 40 | }, 41 | external: pkgDeps, 42 | plugins: [ 43 | babel({ 44 | exclude: ['node_modules/**'] 45 | }) 46 | ] 47 | } 48 | ]; -------------------------------------------------------------------------------- /src/RSAA.js: -------------------------------------------------------------------------------- 1 | /** 2 | * String key that carries API call info interpreted by this Redux middleware. 3 | * 4 | * @constant {string} 5 | * @access public 6 | * @deprecated To be made private (implementation detail). Use `createAction` instead. 7 | * @default 8 | */ 9 | const RSAA = '@@redux-api-middleware/RSAA'; 10 | 11 | export default RSAA; 12 | -------------------------------------------------------------------------------- /src/__snapshots__/errors.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ApiError matches snapshot 1`] = `[ApiError: 404 - Not Found]`; 4 | 5 | exports[`ApiError matches snapshot: object.entries 1`] = ` 6 | Array [ 7 | Array [ 8 | "name", 9 | "ApiError", 10 | ], 11 | Array [ 12 | "status", 13 | 404, 14 | ], 15 | Array [ 16 | "statusText", 17 | "Not Found", 18 | ], 19 | Array [ 20 | "response", 21 | Object { 22 | "error": "Resource not found", 23 | }, 24 | ], 25 | Array [ 26 | "message", 27 | "404 - Not Found", 28 | ], 29 | ] 30 | `; 31 | 32 | exports[`InternalError matches snapshot 1`] = `[InternalError: error thrown in payload function]`; 33 | 34 | exports[`InternalError matches snapshot: object.entries 1`] = ` 35 | Array [ 36 | Array [ 37 | "name", 38 | "InternalError", 39 | ], 40 | Array [ 41 | "message", 42 | "error thrown in payload function", 43 | ], 44 | ] 45 | `; 46 | 47 | exports[`InvalidRSAA matches snapshot 1`] = `[InvalidRSAA: Invalid RSAA]`; 48 | 49 | exports[`InvalidRSAA matches snapshot: object.entries 1`] = ` 50 | Array [ 51 | Array [ 52 | "name", 53 | "InvalidRSAA", 54 | ], 55 | Array [ 56 | "message", 57 | "Invalid RSAA", 58 | ], 59 | Array [ 60 | "validationErrors", 61 | Array [ 62 | "validation error 1", 63 | "validation error 2", 64 | ], 65 | ], 66 | ] 67 | `; 68 | 69 | exports[`RequestError matches snapshot 1`] = `[RequestError: Network request failed]`; 70 | 71 | exports[`RequestError matches snapshot: object.entries 1`] = ` 72 | Array [ 73 | Array [ 74 | "name", 75 | "RequestError", 76 | ], 77 | Array [ 78 | "message", 79 | "Network request failed", 80 | ], 81 | ] 82 | `; 83 | -------------------------------------------------------------------------------- /src/__snapshots__/middleware.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`#apiMiddleware must dispatch a failure FSA on an unsuccessful API call with a non-JSON response: fetch mock 1`] = ` 4 | Object { 5 | "calls": Array [ 6 | Array [ 7 | "http://127.0.0.1/api/users/1", 8 | Object { 9 | "body": undefined, 10 | "credentials": undefined, 11 | "headers": Object {}, 12 | "method": "GET", 13 | }, 14 | ], 15 | ], 16 | "instances": Array [ 17 | undefined, 18 | ], 19 | "invocationCallOrder": Any, 20 | "results": Array [ 21 | Object { 22 | "isThrow": false, 23 | "value": Promise {}, 24 | }, 25 | ], 26 | } 27 | `; 28 | 29 | exports[`#apiMiddleware must dispatch a failure FSA on an unsuccessful API call with a non-JSON response: final result 1`] = ` 30 | Object { 31 | "error": true, 32 | "meta": "failureMeta", 33 | "payload": [ApiError: 404 - Not Found], 34 | "type": "FAILURE", 35 | } 36 | `; 37 | 38 | exports[`#apiMiddleware must dispatch a failure FSA on an unsuccessful API call with a non-JSON response: next mock 1`] = ` 39 | [MockFunction] { 40 | "calls": Array [ 41 | Array [ 42 | Object { 43 | "meta": "requestMeta", 44 | "payload": "requestPayload", 45 | "type": "REQUEST", 46 | }, 47 | ], 48 | Array [ 49 | Object { 50 | "error": true, 51 | "meta": "failureMeta", 52 | "payload": [ApiError: 404 - Not Found], 53 | "type": "FAILURE", 54 | }, 55 | ], 56 | ], 57 | "results": Array [ 58 | Object { 59 | "isThrow": false, 60 | "value": Object { 61 | "meta": "requestMeta", 62 | "payload": "requestPayload", 63 | "type": "REQUEST", 64 | }, 65 | }, 66 | Object { 67 | "isThrow": false, 68 | "value": Object { 69 | "error": true, 70 | "meta": "failureMeta", 71 | "payload": [ApiError: 404 - Not Found], 72 | "type": "FAILURE", 73 | }, 74 | }, 75 | ], 76 | } 77 | `; 78 | 79 | exports[`#apiMiddleware must dispatch a failure FSA on an unsuccessful API call with a non-empty JSON response: fetch mock 1`] = ` 80 | Object { 81 | "calls": Array [ 82 | Array [ 83 | "http://127.0.0.1/api/users/1", 84 | Object { 85 | "body": undefined, 86 | "credentials": undefined, 87 | "headers": Object {}, 88 | "method": "GET", 89 | }, 90 | ], 91 | ], 92 | "instances": Array [ 93 | undefined, 94 | ], 95 | "invocationCallOrder": Any, 96 | "results": Array [ 97 | Object { 98 | "isThrow": false, 99 | "value": Promise {}, 100 | }, 101 | ], 102 | } 103 | `; 104 | 105 | exports[`#apiMiddleware must dispatch a failure FSA on an unsuccessful API call with a non-empty JSON response: final result 1`] = ` 106 | Object { 107 | "error": true, 108 | "meta": "failureMeta", 109 | "payload": [ApiError: 404 - Not Found], 110 | "type": "FAILURE", 111 | } 112 | `; 113 | 114 | exports[`#apiMiddleware must dispatch a failure FSA on an unsuccessful API call with a non-empty JSON response: next mock 1`] = ` 115 | [MockFunction] { 116 | "calls": Array [ 117 | Array [ 118 | Object { 119 | "meta": "requestMeta", 120 | "payload": "requestPayload", 121 | "type": "REQUEST", 122 | }, 123 | ], 124 | Array [ 125 | Object { 126 | "error": true, 127 | "meta": "failureMeta", 128 | "payload": [ApiError: 404 - Not Found], 129 | "type": "FAILURE", 130 | }, 131 | ], 132 | ], 133 | "results": Array [ 134 | Object { 135 | "isThrow": false, 136 | "value": Object { 137 | "meta": "requestMeta", 138 | "payload": "requestPayload", 139 | "type": "REQUEST", 140 | }, 141 | }, 142 | Object { 143 | "isThrow": false, 144 | "value": Object { 145 | "error": true, 146 | "meta": "failureMeta", 147 | "payload": [ApiError: 404 - Not Found], 148 | "type": "FAILURE", 149 | }, 150 | }, 151 | ], 152 | } 153 | `; 154 | 155 | exports[`#apiMiddleware must dispatch a failure FSA on an unsuccessful API call with an empty JSON response: fetch mock 1`] = ` 156 | Object { 157 | "calls": Array [ 158 | Array [ 159 | "http://127.0.0.1/api/users/1", 160 | Object { 161 | "body": undefined, 162 | "credentials": undefined, 163 | "headers": Object {}, 164 | "method": "GET", 165 | }, 166 | ], 167 | ], 168 | "instances": Array [ 169 | undefined, 170 | ], 171 | "invocationCallOrder": Any, 172 | "results": Array [ 173 | Object { 174 | "isThrow": false, 175 | "value": Promise {}, 176 | }, 177 | ], 178 | } 179 | `; 180 | 181 | exports[`#apiMiddleware must dispatch a failure FSA on an unsuccessful API call with an empty JSON response: final result 1`] = ` 182 | Object { 183 | "error": true, 184 | "meta": "failureMeta", 185 | "payload": [ApiError: 404 - Not Found], 186 | "type": "FAILURE", 187 | } 188 | `; 189 | 190 | exports[`#apiMiddleware must dispatch a failure FSA on an unsuccessful API call with an empty JSON response: next mock 1`] = ` 191 | [MockFunction] { 192 | "calls": Array [ 193 | Array [ 194 | Object { 195 | "meta": "requestMeta", 196 | "payload": "requestPayload", 197 | "type": "REQUEST", 198 | }, 199 | ], 200 | Array [ 201 | Object { 202 | "error": true, 203 | "meta": "failureMeta", 204 | "payload": [ApiError: 404 - Not Found], 205 | "type": "FAILURE", 206 | }, 207 | ], 208 | ], 209 | "results": Array [ 210 | Object { 211 | "isThrow": false, 212 | "value": Object { 213 | "meta": "requestMeta", 214 | "payload": "requestPayload", 215 | "type": "REQUEST", 216 | }, 217 | }, 218 | Object { 219 | "isThrow": false, 220 | "value": Object { 221 | "error": true, 222 | "meta": "failureMeta", 223 | "payload": [ApiError: 404 - Not Found], 224 | "type": "FAILURE", 225 | }, 226 | }, 227 | ], 228 | } 229 | `; 230 | 231 | exports[`#apiMiddleware must dispatch a failure FSA when [RSAA].ok returns false on a successful request: fetch mock 1`] = ` 232 | Object { 233 | "calls": Array [ 234 | Array [ 235 | "http://127.0.0.1/api/users/1", 236 | Object { 237 | "body": undefined, 238 | "credentials": undefined, 239 | "headers": Object {}, 240 | "method": "GET", 241 | }, 242 | ], 243 | ], 244 | "instances": Array [ 245 | undefined, 246 | ], 247 | "invocationCallOrder": Any, 248 | "results": Array [ 249 | Object { 250 | "isThrow": false, 251 | "value": Promise {}, 252 | }, 253 | ], 254 | } 255 | `; 256 | 257 | exports[`#apiMiddleware must dispatch a failure FSA when [RSAA].ok returns false on a successful request: final result 1`] = ` 258 | Object { 259 | "error": true, 260 | "meta": undefined, 261 | "payload": [ApiError: 200 - OK], 262 | "type": "FAILURE", 263 | } 264 | `; 265 | 266 | exports[`#apiMiddleware must dispatch a failure FSA when [RSAA].ok returns false on a successful request: next mock 1`] = ` 267 | [MockFunction] { 268 | "calls": Array [ 269 | Array [ 270 | Object { 271 | "type": "REQUEST", 272 | }, 273 | ], 274 | Array [ 275 | Object { 276 | "error": true, 277 | "meta": undefined, 278 | "payload": [ApiError: 200 - OK], 279 | "type": "FAILURE", 280 | }, 281 | ], 282 | ], 283 | "results": Array [ 284 | Object { 285 | "isThrow": false, 286 | "value": Object { 287 | "type": "REQUEST", 288 | }, 289 | }, 290 | Object { 291 | "isThrow": false, 292 | "value": Object { 293 | "error": true, 294 | "meta": undefined, 295 | "payload": [ApiError: 200 - OK], 296 | "type": "FAILURE", 297 | }, 298 | }, 299 | ], 300 | } 301 | `; 302 | 303 | exports[`#apiMiddleware must dispatch a failure FSA when [RSAA].ok returns false on a successful request: ok() 1`] = ` 304 | [MockFunction] { 305 | "calls": Array [ 306 | Array [ 307 | Response { 308 | "size": 0, 309 | "timeout": 0, 310 | Symbol(Body internals): Object { 311 | "body": "{\\"data\\":\\"12345\\"}", 312 | "disturbed": true, 313 | "error": null, 314 | }, 315 | Symbol(Response internals): Object { 316 | "headers": Headers { 317 | Symbol(map): Object { 318 | "Content-Type": Array [ 319 | "application/json", 320 | ], 321 | }, 322 | }, 323 | "status": 200, 324 | "statusText": "OK", 325 | "url": undefined, 326 | }, 327 | }, 328 | ], 329 | ], 330 | "results": Array [ 331 | Object { 332 | "isThrow": false, 333 | "value": false, 334 | }, 335 | ], 336 | } 337 | `; 338 | 339 | exports[`#apiMiddleware must dispatch a failure FSA with an error on a request error: fetch mock 1`] = ` 340 | Object { 341 | "calls": Array [ 342 | Array [ 343 | "http://127.0.0.1/api/users/1", 344 | Object { 345 | "body": undefined, 346 | "credentials": undefined, 347 | "headers": Object {}, 348 | "method": "GET", 349 | }, 350 | ], 351 | ], 352 | "instances": Array [ 353 | undefined, 354 | ], 355 | "invocationCallOrder": Any, 356 | "results": Array [ 357 | Object { 358 | "isThrow": false, 359 | "value": Promise {}, 360 | }, 361 | ], 362 | } 363 | `; 364 | 365 | exports[`#apiMiddleware must dispatch a failure FSA with an error on a request error: final result 1`] = ` 366 | Object { 367 | "error": true, 368 | "meta": undefined, 369 | "payload": [RequestError: Test request error], 370 | "type": "FAILURE", 371 | } 372 | `; 373 | 374 | exports[`#apiMiddleware must dispatch a failure FSA with an error on a request error: next mock 1`] = ` 375 | [MockFunction] { 376 | "calls": Array [ 377 | Array [ 378 | Object { 379 | "meta": "someMeta", 380 | "payload": "ignoredPayload", 381 | "type": "REQUEST", 382 | }, 383 | ], 384 | Array [ 385 | Object { 386 | "error": true, 387 | "meta": undefined, 388 | "payload": [RequestError: Test request error], 389 | "type": "FAILURE", 390 | }, 391 | ], 392 | ], 393 | "results": Array [ 394 | Object { 395 | "isThrow": false, 396 | "value": Object { 397 | "meta": "someMeta", 398 | "payload": "ignoredPayload", 399 | "type": "REQUEST", 400 | }, 401 | }, 402 | Object { 403 | "isThrow": false, 404 | "value": Object { 405 | "error": true, 406 | "meta": undefined, 407 | "payload": [RequestError: Test request error], 408 | "type": "FAILURE", 409 | }, 410 | }, 411 | ], 412 | } 413 | `; 414 | 415 | exports[`#apiMiddleware must dispatch a success FSA on a successful API call with a non-JSON response: fetch mock 1`] = ` 416 | Object { 417 | "calls": Array [ 418 | Array [ 419 | "http://127.0.0.1/api/users/1", 420 | Object { 421 | "body": undefined, 422 | "credentials": undefined, 423 | "headers": Object {}, 424 | "method": "GET", 425 | }, 426 | ], 427 | ], 428 | "instances": Array [ 429 | undefined, 430 | ], 431 | "invocationCallOrder": Any, 432 | "results": Array [ 433 | Object { 434 | "isThrow": false, 435 | "value": Promise {}, 436 | }, 437 | ], 438 | } 439 | `; 440 | 441 | exports[`#apiMiddleware must dispatch a success FSA on a successful API call with a non-JSON response: final result 1`] = ` 442 | Object { 443 | "meta": "successMeta", 444 | "payload": undefined, 445 | "type": "SUCCESS", 446 | } 447 | `; 448 | 449 | exports[`#apiMiddleware must dispatch a success FSA on a successful API call with a non-JSON response: next mock 1`] = ` 450 | [MockFunction] { 451 | "calls": Array [ 452 | Array [ 453 | Object { 454 | "meta": "requestMeta", 455 | "payload": "requestPayload", 456 | "type": "REQUEST", 457 | }, 458 | ], 459 | Array [ 460 | Object { 461 | "meta": "successMeta", 462 | "payload": undefined, 463 | "type": "SUCCESS", 464 | }, 465 | ], 466 | ], 467 | "results": Array [ 468 | Object { 469 | "isThrow": false, 470 | "value": Object { 471 | "meta": "requestMeta", 472 | "payload": "requestPayload", 473 | "type": "REQUEST", 474 | }, 475 | }, 476 | Object { 477 | "isThrow": false, 478 | "value": Object { 479 | "meta": "successMeta", 480 | "payload": undefined, 481 | "type": "SUCCESS", 482 | }, 483 | }, 484 | ], 485 | } 486 | `; 487 | 488 | exports[`#apiMiddleware must dispatch a success FSA on a successful API call with a non-empty JSON response: fetch mock 1`] = ` 489 | Object { 490 | "calls": Array [ 491 | Array [ 492 | "http://127.0.0.1/api/users/1", 493 | Object { 494 | "body": undefined, 495 | "credentials": undefined, 496 | "headers": Object {}, 497 | "method": "GET", 498 | }, 499 | ], 500 | ], 501 | "instances": Array [ 502 | undefined, 503 | ], 504 | "invocationCallOrder": Any, 505 | "results": Array [ 506 | Object { 507 | "isThrow": false, 508 | "value": Promise {}, 509 | }, 510 | ], 511 | } 512 | `; 513 | 514 | exports[`#apiMiddleware must dispatch a success FSA on a successful API call with a non-empty JSON response: final result 1`] = ` 515 | Object { 516 | "meta": "successMeta", 517 | "payload": Object { 518 | "username": "Alice", 519 | }, 520 | "type": "SUCCESS", 521 | } 522 | `; 523 | 524 | exports[`#apiMiddleware must dispatch a success FSA on a successful API call with a non-empty JSON response: next mock 1`] = ` 525 | [MockFunction] { 526 | "calls": Array [ 527 | Array [ 528 | Object { 529 | "meta": "requestMeta", 530 | "payload": "requestPayload", 531 | "type": "REQUEST", 532 | }, 533 | ], 534 | Array [ 535 | Object { 536 | "meta": "successMeta", 537 | "payload": Object { 538 | "username": "Alice", 539 | }, 540 | "type": "SUCCESS", 541 | }, 542 | ], 543 | ], 544 | "results": Array [ 545 | Object { 546 | "isThrow": false, 547 | "value": Object { 548 | "meta": "requestMeta", 549 | "payload": "requestPayload", 550 | "type": "REQUEST", 551 | }, 552 | }, 553 | Object { 554 | "isThrow": false, 555 | "value": Object { 556 | "meta": "successMeta", 557 | "payload": Object { 558 | "username": "Alice", 559 | }, 560 | "type": "SUCCESS", 561 | }, 562 | }, 563 | ], 564 | } 565 | `; 566 | 567 | exports[`#apiMiddleware must dispatch a success FSA on a successful API call with an empty JSON response: fetch mock 1`] = ` 568 | Object { 569 | "calls": Array [ 570 | Array [ 571 | "http://127.0.0.1/api/users/1", 572 | Object { 573 | "body": undefined, 574 | "credentials": undefined, 575 | "headers": Object {}, 576 | "method": "GET", 577 | }, 578 | ], 579 | ], 580 | "instances": Array [ 581 | undefined, 582 | ], 583 | "invocationCallOrder": Any, 584 | "results": Array [ 585 | Object { 586 | "isThrow": false, 587 | "value": Promise {}, 588 | }, 589 | ], 590 | } 591 | `; 592 | 593 | exports[`#apiMiddleware must dispatch a success FSA on a successful API call with an empty JSON response: final result 1`] = ` 594 | Object { 595 | "meta": "successMeta", 596 | "payload": Object {}, 597 | "type": "SUCCESS", 598 | } 599 | `; 600 | 601 | exports[`#apiMiddleware must dispatch a success FSA on a successful API call with an empty JSON response: next mock 1`] = ` 602 | [MockFunction] { 603 | "calls": Array [ 604 | Array [ 605 | Object { 606 | "meta": "requestMeta", 607 | "payload": "requestPayload", 608 | "type": "REQUEST", 609 | }, 610 | ], 611 | Array [ 612 | Object { 613 | "meta": "successMeta", 614 | "payload": Object {}, 615 | "type": "SUCCESS", 616 | }, 617 | ], 618 | ], 619 | "results": Array [ 620 | Object { 621 | "isThrow": false, 622 | "value": Object { 623 | "meta": "requestMeta", 624 | "payload": "requestPayload", 625 | "type": "REQUEST", 626 | }, 627 | }, 628 | Object { 629 | "isThrow": false, 630 | "value": Object { 631 | "meta": "successMeta", 632 | "payload": Object {}, 633 | "type": "SUCCESS", 634 | }, 635 | }, 636 | ], 637 | } 638 | `; 639 | 640 | exports[`#apiMiddleware must dispatch a success FSA with an error state on a successful API call with an invalid JSON response: fetch mock 1`] = ` 641 | Object { 642 | "calls": Array [ 643 | Array [ 644 | "http://127.0.0.1/api/users/1", 645 | Object { 646 | "body": undefined, 647 | "credentials": undefined, 648 | "headers": Object {}, 649 | "method": "GET", 650 | }, 651 | ], 652 | ], 653 | "instances": Array [ 654 | undefined, 655 | ], 656 | "invocationCallOrder": Any, 657 | "results": Array [ 658 | Object { 659 | "isThrow": false, 660 | "value": Promise {}, 661 | }, 662 | ], 663 | } 664 | `; 665 | 666 | exports[`#apiMiddleware must dispatch a success FSA with an error state on a successful API call with an invalid JSON response: final result 1`] = ` 667 | Object { 668 | "error": true, 669 | "meta": "successMeta", 670 | "payload": [InternalError: Expected error - simulating invalid JSON], 671 | "type": "SUCCESS", 672 | } 673 | `; 674 | 675 | exports[`#apiMiddleware must dispatch a success FSA with an error state on a successful API call with an invalid JSON response: next mock 1`] = ` 676 | [MockFunction] { 677 | "calls": Array [ 678 | Array [ 679 | Object { 680 | "meta": "requestMeta", 681 | "payload": "requestPayload", 682 | "type": "REQUEST", 683 | }, 684 | ], 685 | Array [ 686 | Object { 687 | "error": true, 688 | "meta": "successMeta", 689 | "payload": [InternalError: Expected error - simulating invalid JSON], 690 | "type": "SUCCESS", 691 | }, 692 | ], 693 | ], 694 | "results": Array [ 695 | Object { 696 | "isThrow": false, 697 | "value": Object { 698 | "meta": "requestMeta", 699 | "payload": "requestPayload", 700 | "type": "REQUEST", 701 | }, 702 | }, 703 | Object { 704 | "isThrow": false, 705 | "value": Object { 706 | "error": true, 707 | "meta": "successMeta", 708 | "payload": [InternalError: Expected error - simulating invalid JSON], 709 | "type": "SUCCESS", 710 | }, 711 | }, 712 | ], 713 | } 714 | `; 715 | 716 | exports[`#apiMiddleware must dispatch an error request FSA for an invalid RSAA with a descriptor request type: next mock 1`] = ` 717 | [MockFunction] { 718 | "calls": Array [ 719 | Array [ 720 | Object { 721 | "error": true, 722 | "payload": [InvalidRSAA: Invalid RSAA], 723 | "type": "REQUEST", 724 | }, 725 | ], 726 | ], 727 | "results": Array [ 728 | Object { 729 | "isThrow": false, 730 | "value": Object { 731 | "error": true, 732 | "payload": [InvalidRSAA: Invalid RSAA], 733 | "type": "REQUEST", 734 | }, 735 | }, 736 | ], 737 | } 738 | `; 739 | 740 | exports[`#apiMiddleware must dispatch an error request FSA for an invalid RSAA with a string request type: next mock 1`] = ` 741 | [MockFunction] { 742 | "calls": Array [ 743 | Array [ 744 | Object { 745 | "error": true, 746 | "payload": [InvalidRSAA: Invalid RSAA], 747 | "type": "REQUEST", 748 | }, 749 | ], 750 | ], 751 | "results": Array [ 752 | Object { 753 | "isThrow": false, 754 | "value": Object { 755 | "error": true, 756 | "payload": [InvalidRSAA: Invalid RSAA], 757 | "type": "REQUEST", 758 | }, 759 | }, 760 | ], 761 | } 762 | `; 763 | 764 | exports[`#apiMiddleware must dispatch an error request FSA when [RSAA].bailout fails: final result 1`] = ` 765 | Object { 766 | "error": true, 767 | "meta": undefined, 768 | "payload": [RequestError: [RSAA].bailout function failed], 769 | "type": "FAILURE", 770 | } 771 | `; 772 | 773 | exports[`#apiMiddleware must dispatch an error request FSA when [RSAA].bailout fails: next mock 1`] = ` 774 | [MockFunction] { 775 | "calls": Array [ 776 | Array [ 777 | Object { 778 | "error": true, 779 | "meta": undefined, 780 | "payload": [RequestError: [RSAA].bailout function failed], 781 | "type": "FAILURE", 782 | }, 783 | ], 784 | ], 785 | "results": Array [ 786 | Object { 787 | "isThrow": false, 788 | "value": Object { 789 | "error": true, 790 | "meta": undefined, 791 | "payload": [RequestError: [RSAA].bailout function failed], 792 | "type": "FAILURE", 793 | }, 794 | }, 795 | ], 796 | } 797 | `; 798 | 799 | exports[`#apiMiddleware must dispatch an error request FSA when [RSAA].body fails: final result 1`] = ` 800 | Object { 801 | "error": true, 802 | "meta": undefined, 803 | "payload": [RequestError: [RSAA].body function failed], 804 | "type": "FAILURE", 805 | } 806 | `; 807 | 808 | exports[`#apiMiddleware must dispatch an error request FSA when [RSAA].body fails: next mock 1`] = ` 809 | [MockFunction] { 810 | "calls": Array [ 811 | Array [ 812 | Object { 813 | "error": true, 814 | "meta": undefined, 815 | "payload": [RequestError: [RSAA].body function failed], 816 | "type": "FAILURE", 817 | }, 818 | ], 819 | ], 820 | "results": Array [ 821 | Object { 822 | "isThrow": false, 823 | "value": Object { 824 | "error": true, 825 | "meta": undefined, 826 | "payload": [RequestError: [RSAA].body function failed], 827 | "type": "FAILURE", 828 | }, 829 | }, 830 | ], 831 | } 832 | `; 833 | 834 | exports[`#apiMiddleware must dispatch an error request FSA when [RSAA].endpoint fails: final result 1`] = ` 835 | Object { 836 | "error": true, 837 | "meta": undefined, 838 | "payload": [RequestError: [RSAA].endpoint function failed], 839 | "type": "FAILURE", 840 | } 841 | `; 842 | 843 | exports[`#apiMiddleware must dispatch an error request FSA when [RSAA].endpoint fails: next mock 1`] = ` 844 | [MockFunction] { 845 | "calls": Array [ 846 | Array [ 847 | Object { 848 | "error": true, 849 | "meta": undefined, 850 | "payload": [RequestError: [RSAA].endpoint function failed], 851 | "type": "FAILURE", 852 | }, 853 | ], 854 | ], 855 | "results": Array [ 856 | Object { 857 | "isThrow": false, 858 | "value": Object { 859 | "error": true, 860 | "meta": undefined, 861 | "payload": [RequestError: [RSAA].endpoint function failed], 862 | "type": "FAILURE", 863 | }, 864 | }, 865 | ], 866 | } 867 | `; 868 | 869 | exports[`#apiMiddleware must dispatch an error request FSA when [RSAA].headers fails: final result 1`] = ` 870 | Object { 871 | "error": true, 872 | "meta": undefined, 873 | "payload": [RequestError: [RSAA].headers function failed], 874 | "type": "FAILURE", 875 | } 876 | `; 877 | 878 | exports[`#apiMiddleware must dispatch an error request FSA when [RSAA].headers fails: next mock 1`] = ` 879 | [MockFunction] { 880 | "calls": Array [ 881 | Array [ 882 | Object { 883 | "error": true, 884 | "meta": undefined, 885 | "payload": [RequestError: [RSAA].headers function failed], 886 | "type": "FAILURE", 887 | }, 888 | ], 889 | ], 890 | "results": Array [ 891 | Object { 892 | "isThrow": false, 893 | "value": Object { 894 | "error": true, 895 | "meta": undefined, 896 | "payload": [RequestError: [RSAA].headers function failed], 897 | "type": "FAILURE", 898 | }, 899 | }, 900 | ], 901 | } 902 | `; 903 | 904 | exports[`#apiMiddleware must dispatch an error request FSA when [RSAA].ok fails: fetch mock 1`] = ` 905 | Object { 906 | "calls": Array [ 907 | Array [ 908 | "http://127.0.0.1/api/users/1", 909 | Object { 910 | "body": undefined, 911 | "credentials": undefined, 912 | "headers": Object {}, 913 | "method": "GET", 914 | }, 915 | ], 916 | ], 917 | "instances": Array [ 918 | undefined, 919 | ], 920 | "invocationCallOrder": Any, 921 | "results": Array [ 922 | Object { 923 | "isThrow": false, 924 | "value": Promise {}, 925 | }, 926 | ], 927 | } 928 | `; 929 | 930 | exports[`#apiMiddleware must dispatch an error request FSA when [RSAA].ok fails: final result 1`] = ` 931 | Object { 932 | "error": true, 933 | "meta": undefined, 934 | "payload": [InternalError: [RSAA].ok function failed], 935 | "type": "FAILURE", 936 | } 937 | `; 938 | 939 | exports[`#apiMiddleware must dispatch an error request FSA when [RSAA].ok fails: next mock 1`] = ` 940 | [MockFunction] { 941 | "calls": Array [ 942 | Array [ 943 | Object { 944 | "type": "REQUEST", 945 | }, 946 | ], 947 | Array [ 948 | Object { 949 | "error": true, 950 | "meta": undefined, 951 | "payload": [InternalError: [RSAA].ok function failed], 952 | "type": "FAILURE", 953 | }, 954 | ], 955 | ], 956 | "results": Array [ 957 | Object { 958 | "isThrow": false, 959 | "value": Object { 960 | "type": "REQUEST", 961 | }, 962 | }, 963 | Object { 964 | "isThrow": false, 965 | "value": Object { 966 | "error": true, 967 | "meta": undefined, 968 | "payload": [InternalError: [RSAA].ok function failed], 969 | "type": "FAILURE", 970 | }, 971 | }, 972 | ], 973 | } 974 | `; 975 | 976 | exports[`#apiMiddleware must dispatch an error request FSA when [RSAA].options fails: final result 1`] = ` 977 | Object { 978 | "error": true, 979 | "meta": undefined, 980 | "payload": [RequestError: [RSAA].options function failed], 981 | "type": "FAILURE", 982 | } 983 | `; 984 | 985 | exports[`#apiMiddleware must dispatch an error request FSA when [RSAA].options fails: next mock 1`] = ` 986 | [MockFunction] { 987 | "calls": Array [ 988 | Array [ 989 | Object { 990 | "error": true, 991 | "meta": undefined, 992 | "payload": [RequestError: [RSAA].options function failed], 993 | "type": "FAILURE", 994 | }, 995 | ], 996 | ], 997 | "results": Array [ 998 | Object { 999 | "isThrow": false, 1000 | "value": Object { 1001 | "error": true, 1002 | "meta": undefined, 1003 | "payload": [RequestError: [RSAA].options function failed], 1004 | "type": "FAILURE", 1005 | }, 1006 | }, 1007 | ], 1008 | } 1009 | `; 1010 | 1011 | exports[`#apiMiddleware must dispatch correct error payload when [RSAA].fetch wrapper returns an error response: final result 1`] = ` 1012 | Object { 1013 | "error": true, 1014 | "meta": undefined, 1015 | "payload": [ApiError: 500 - Internal Server Error], 1016 | "type": "FAILURE", 1017 | } 1018 | `; 1019 | 1020 | exports[`#apiMiddleware must dispatch correct error payload when [RSAA].fetch wrapper returns an error response: next mock 1`] = ` 1021 | [MockFunction] { 1022 | "calls": Array [ 1023 | Array [ 1024 | Object { 1025 | "type": "REQUEST", 1026 | }, 1027 | ], 1028 | Array [ 1029 | Object { 1030 | "error": true, 1031 | "meta": undefined, 1032 | "payload": [ApiError: 500 - Internal Server Error], 1033 | "type": "FAILURE", 1034 | }, 1035 | ], 1036 | ], 1037 | "results": Array [ 1038 | Object { 1039 | "isThrow": false, 1040 | "value": Object { 1041 | "type": "REQUEST", 1042 | }, 1043 | }, 1044 | Object { 1045 | "isThrow": false, 1046 | "value": Object { 1047 | "error": true, 1048 | "meta": undefined, 1049 | "payload": [ApiError: 500 - Internal Server Error], 1050 | "type": "FAILURE", 1051 | }, 1052 | }, 1053 | ], 1054 | } 1055 | `; 1056 | 1057 | exports[`#apiMiddleware must pass actions without an [RSAA] property to the next handler: final result 1`] = `Object {}`; 1058 | 1059 | exports[`#apiMiddleware must pass actions without an [RSAA] property to the next handler: next mock 1`] = ` 1060 | [MockFunction] { 1061 | "calls": Array [ 1062 | Array [ 1063 | Object {}, 1064 | ], 1065 | ], 1066 | "results": Array [ 1067 | Object { 1068 | "isThrow": false, 1069 | "value": Object {}, 1070 | }, 1071 | ], 1072 | } 1073 | `; 1074 | 1075 | exports[`#apiMiddleware must use a [RSAA].fetch custom fetch wrapper when present: fetch mock 1`] = ` 1076 | Object { 1077 | "calls": Array [ 1078 | Array [ 1079 | "http://127.0.0.1/api/users/1", 1080 | Object { 1081 | "body": undefined, 1082 | "credentials": undefined, 1083 | "headers": Object {}, 1084 | "method": "GET", 1085 | }, 1086 | ], 1087 | ], 1088 | "instances": Array [ 1089 | undefined, 1090 | ], 1091 | "invocationCallOrder": Any, 1092 | "results": Array [ 1093 | Object { 1094 | "isThrow": false, 1095 | "value": Promise {}, 1096 | }, 1097 | ], 1098 | } 1099 | `; 1100 | 1101 | exports[`#apiMiddleware must use a [RSAA].fetch custom fetch wrapper when present: final result 1`] = ` 1102 | Object { 1103 | "meta": undefined, 1104 | "payload": Object { 1105 | "error": false, 1106 | "foo": "bar", 1107 | "id": 1, 1108 | "name": "Alan", 1109 | }, 1110 | "type": "SUCCESS", 1111 | } 1112 | `; 1113 | 1114 | exports[`#apiMiddleware must use a [RSAA].fetch custom fetch wrapper when present: next mock 1`] = ` 1115 | [MockFunction] { 1116 | "calls": Array [ 1117 | Array [ 1118 | Object { 1119 | "type": "REQUEST", 1120 | }, 1121 | ], 1122 | Array [ 1123 | Object { 1124 | "meta": undefined, 1125 | "payload": Object { 1126 | "error": false, 1127 | "foo": "bar", 1128 | "id": 1, 1129 | "name": "Alan", 1130 | }, 1131 | "type": "SUCCESS", 1132 | }, 1133 | ], 1134 | ], 1135 | "results": Array [ 1136 | Object { 1137 | "isThrow": false, 1138 | "value": Object { 1139 | "type": "REQUEST", 1140 | }, 1141 | }, 1142 | Object { 1143 | "isThrow": false, 1144 | "value": Object { 1145 | "meta": undefined, 1146 | "payload": Object { 1147 | "error": false, 1148 | "foo": "bar", 1149 | "id": 1, 1150 | "name": "Alan", 1151 | }, 1152 | "type": "SUCCESS", 1153 | }, 1154 | }, 1155 | ], 1156 | } 1157 | `; 1158 | 1159 | exports[`#apiMiddleware must use an [RSAA].bailout function when present: bailout() 1`] = ` 1160 | [MockFunction] { 1161 | "calls": Array [ 1162 | Array [ 1163 | undefined, 1164 | ], 1165 | ], 1166 | "results": Array [ 1167 | Object { 1168 | "isThrow": false, 1169 | "value": true, 1170 | }, 1171 | ], 1172 | } 1173 | `; 1174 | 1175 | exports[`#apiMiddleware must use an [RSAA].body function when present: body() 1`] = ` 1176 | [MockFunction] { 1177 | "calls": Array [ 1178 | Array [ 1179 | undefined, 1180 | ], 1181 | ], 1182 | "results": Array [ 1183 | Object { 1184 | "isThrow": false, 1185 | "value": "mockBody", 1186 | }, 1187 | ], 1188 | } 1189 | `; 1190 | 1191 | exports[`#apiMiddleware must use an [RSAA].body function when present: fetch mock 1`] = ` 1192 | Object { 1193 | "calls": Array [ 1194 | Array [ 1195 | "http://127.0.0.1/api/users/1", 1196 | Object { 1197 | "body": "mockBody", 1198 | "credentials": undefined, 1199 | "headers": Object {}, 1200 | "method": "GET", 1201 | }, 1202 | ], 1203 | ], 1204 | "instances": Array [ 1205 | undefined, 1206 | ], 1207 | "invocationCallOrder": Any, 1208 | "results": Array [ 1209 | Object { 1210 | "isThrow": false, 1211 | "value": Promise {}, 1212 | }, 1213 | ], 1214 | } 1215 | `; 1216 | 1217 | exports[`#apiMiddleware must use an [RSAA].body function when present: final result 1`] = ` 1218 | Object { 1219 | "meta": undefined, 1220 | "payload": Object { 1221 | "data": "12345", 1222 | }, 1223 | "type": "SUCCESS", 1224 | } 1225 | `; 1226 | 1227 | exports[`#apiMiddleware must use an [RSAA].body function when present: next mock 1`] = ` 1228 | [MockFunction] { 1229 | "calls": Array [ 1230 | Array [ 1231 | Object { 1232 | "type": "REQUEST", 1233 | }, 1234 | ], 1235 | Array [ 1236 | Object { 1237 | "meta": undefined, 1238 | "payload": Object { 1239 | "data": "12345", 1240 | }, 1241 | "type": "SUCCESS", 1242 | }, 1243 | ], 1244 | ], 1245 | "results": Array [ 1246 | Object { 1247 | "isThrow": false, 1248 | "value": Object { 1249 | "type": "REQUEST", 1250 | }, 1251 | }, 1252 | Object { 1253 | "isThrow": false, 1254 | "value": Object { 1255 | "meta": undefined, 1256 | "payload": Object { 1257 | "data": "12345", 1258 | }, 1259 | "type": "SUCCESS", 1260 | }, 1261 | }, 1262 | ], 1263 | } 1264 | `; 1265 | 1266 | exports[`#apiMiddleware must use an [RSAA].endpoint function when present: endpoint() 1`] = ` 1267 | [MockFunction] { 1268 | "calls": Array [ 1269 | Array [ 1270 | undefined, 1271 | ], 1272 | ], 1273 | "results": Array [ 1274 | Object { 1275 | "isThrow": false, 1276 | "value": "http://127.0.0.1/api/users/1", 1277 | }, 1278 | ], 1279 | } 1280 | `; 1281 | 1282 | exports[`#apiMiddleware must use an [RSAA].endpoint function when present: fetch mock 1`] = ` 1283 | Object { 1284 | "calls": Array [ 1285 | Array [ 1286 | "http://127.0.0.1/api/users/1", 1287 | Object { 1288 | "body": undefined, 1289 | "credentials": undefined, 1290 | "headers": Object {}, 1291 | "method": "GET", 1292 | }, 1293 | ], 1294 | ], 1295 | "instances": Array [ 1296 | undefined, 1297 | ], 1298 | "invocationCallOrder": Any, 1299 | "results": Array [ 1300 | Object { 1301 | "isThrow": false, 1302 | "value": Promise {}, 1303 | }, 1304 | ], 1305 | } 1306 | `; 1307 | 1308 | exports[`#apiMiddleware must use an [RSAA].endpoint function when present: final result 1`] = ` 1309 | Object { 1310 | "meta": undefined, 1311 | "payload": Object { 1312 | "data": "12345", 1313 | }, 1314 | "type": "SUCCESS", 1315 | } 1316 | `; 1317 | 1318 | exports[`#apiMiddleware must use an [RSAA].endpoint function when present: next mock 1`] = ` 1319 | [MockFunction] { 1320 | "calls": Array [ 1321 | Array [ 1322 | Object { 1323 | "type": "REQUEST", 1324 | }, 1325 | ], 1326 | Array [ 1327 | Object { 1328 | "meta": undefined, 1329 | "payload": Object { 1330 | "data": "12345", 1331 | }, 1332 | "type": "SUCCESS", 1333 | }, 1334 | ], 1335 | ], 1336 | "results": Array [ 1337 | Object { 1338 | "isThrow": false, 1339 | "value": Object { 1340 | "type": "REQUEST", 1341 | }, 1342 | }, 1343 | Object { 1344 | "isThrow": false, 1345 | "value": Object { 1346 | "meta": undefined, 1347 | "payload": Object { 1348 | "data": "12345", 1349 | }, 1350 | "type": "SUCCESS", 1351 | }, 1352 | }, 1353 | ], 1354 | } 1355 | `; 1356 | 1357 | exports[`#apiMiddleware must use an [RSAA].headers function when present: fetch mock 1`] = ` 1358 | Object { 1359 | "calls": Array [ 1360 | Array [ 1361 | "http://127.0.0.1/api/users/1", 1362 | Object { 1363 | "body": undefined, 1364 | "credentials": undefined, 1365 | "headers": Object { 1366 | "Test-Header": "test", 1367 | }, 1368 | "method": "GET", 1369 | }, 1370 | ], 1371 | ], 1372 | "instances": Array [ 1373 | undefined, 1374 | ], 1375 | "invocationCallOrder": Any, 1376 | "results": Array [ 1377 | Object { 1378 | "isThrow": false, 1379 | "value": Promise {}, 1380 | }, 1381 | ], 1382 | } 1383 | `; 1384 | 1385 | exports[`#apiMiddleware must use an [RSAA].headers function when present: final result 1`] = ` 1386 | Object { 1387 | "meta": undefined, 1388 | "payload": Object { 1389 | "data": "12345", 1390 | }, 1391 | "type": "SUCCESS", 1392 | } 1393 | `; 1394 | 1395 | exports[`#apiMiddleware must use an [RSAA].headers function when present: headers() 1`] = ` 1396 | [MockFunction] { 1397 | "calls": Array [ 1398 | Array [ 1399 | undefined, 1400 | ], 1401 | ], 1402 | "results": Array [ 1403 | Object { 1404 | "isThrow": false, 1405 | "value": Object { 1406 | "Test-Header": "test", 1407 | }, 1408 | }, 1409 | ], 1410 | } 1411 | `; 1412 | 1413 | exports[`#apiMiddleware must use an [RSAA].headers function when present: next mock 1`] = ` 1414 | [MockFunction] { 1415 | "calls": Array [ 1416 | Array [ 1417 | Object { 1418 | "type": "REQUEST", 1419 | }, 1420 | ], 1421 | Array [ 1422 | Object { 1423 | "meta": undefined, 1424 | "payload": Object { 1425 | "data": "12345", 1426 | }, 1427 | "type": "SUCCESS", 1428 | }, 1429 | ], 1430 | ], 1431 | "results": Array [ 1432 | Object { 1433 | "isThrow": false, 1434 | "value": Object { 1435 | "type": "REQUEST", 1436 | }, 1437 | }, 1438 | Object { 1439 | "isThrow": false, 1440 | "value": Object { 1441 | "meta": undefined, 1442 | "payload": Object { 1443 | "data": "12345", 1444 | }, 1445 | "type": "SUCCESS", 1446 | }, 1447 | }, 1448 | ], 1449 | } 1450 | `; 1451 | 1452 | exports[`#apiMiddleware must use an [RSAA].ok function when present: fetch mock 1`] = ` 1453 | Object { 1454 | "calls": Array [ 1455 | Array [ 1456 | "http://127.0.0.1/api/users/1", 1457 | Object { 1458 | "body": undefined, 1459 | "credentials": undefined, 1460 | "headers": Object {}, 1461 | "method": "GET", 1462 | }, 1463 | ], 1464 | ], 1465 | "instances": Array [ 1466 | undefined, 1467 | ], 1468 | "invocationCallOrder": Any, 1469 | "results": Array [ 1470 | Object { 1471 | "isThrow": false, 1472 | "value": Promise {}, 1473 | }, 1474 | ], 1475 | } 1476 | `; 1477 | 1478 | exports[`#apiMiddleware must use an [RSAA].ok function when present: final result 1`] = ` 1479 | Object { 1480 | "meta": undefined, 1481 | "payload": Object { 1482 | "data": "12345", 1483 | }, 1484 | "type": "SUCCESS", 1485 | } 1486 | `; 1487 | 1488 | exports[`#apiMiddleware must use an [RSAA].ok function when present: next mock 1`] = ` 1489 | [MockFunction] { 1490 | "calls": Array [ 1491 | Array [ 1492 | Object { 1493 | "type": "REQUEST", 1494 | }, 1495 | ], 1496 | Array [ 1497 | Object { 1498 | "meta": undefined, 1499 | "payload": Object { 1500 | "data": "12345", 1501 | }, 1502 | "type": "SUCCESS", 1503 | }, 1504 | ], 1505 | ], 1506 | "results": Array [ 1507 | Object { 1508 | "isThrow": false, 1509 | "value": Object { 1510 | "type": "REQUEST", 1511 | }, 1512 | }, 1513 | Object { 1514 | "isThrow": false, 1515 | "value": Object { 1516 | "meta": undefined, 1517 | "payload": Object { 1518 | "data": "12345", 1519 | }, 1520 | "type": "SUCCESS", 1521 | }, 1522 | }, 1523 | ], 1524 | } 1525 | `; 1526 | 1527 | exports[`#apiMiddleware must use an [RSAA].ok function when present: ok() 1`] = ` 1528 | [MockFunction] { 1529 | "calls": Array [ 1530 | Array [ 1531 | Response { 1532 | "size": 0, 1533 | "timeout": 0, 1534 | Symbol(Body internals): Object { 1535 | "body": "{\\"data\\":\\"12345\\"}", 1536 | "disturbed": true, 1537 | "error": null, 1538 | }, 1539 | Symbol(Response internals): Object { 1540 | "headers": Headers { 1541 | Symbol(map): Object { 1542 | "Content-Type": Array [ 1543 | "application/json", 1544 | ], 1545 | }, 1546 | }, 1547 | "status": 200, 1548 | "statusText": "OK", 1549 | "url": undefined, 1550 | }, 1551 | }, 1552 | ], 1553 | ], 1554 | "results": Array [ 1555 | Object { 1556 | "isThrow": false, 1557 | "value": true, 1558 | }, 1559 | ], 1560 | } 1561 | `; 1562 | 1563 | exports[`#apiMiddleware must use an [RSAA].options function when present: fetch mock 1`] = ` 1564 | Object { 1565 | "calls": Array [ 1566 | Array [ 1567 | "http://127.0.0.1/api/users/1", 1568 | Object { 1569 | "body": undefined, 1570 | "credentials": undefined, 1571 | "headers": Object {}, 1572 | "method": "GET", 1573 | }, 1574 | ], 1575 | ], 1576 | "instances": Array [ 1577 | undefined, 1578 | ], 1579 | "invocationCallOrder": Any, 1580 | "results": Array [ 1581 | Object { 1582 | "isThrow": false, 1583 | "value": Promise {}, 1584 | }, 1585 | ], 1586 | } 1587 | `; 1588 | 1589 | exports[`#apiMiddleware must use an [RSAA].options function when present: final result 1`] = ` 1590 | Object { 1591 | "meta": undefined, 1592 | "payload": Object { 1593 | "data": "12345", 1594 | }, 1595 | "type": "SUCCESS", 1596 | } 1597 | `; 1598 | 1599 | exports[`#apiMiddleware must use an [RSAA].options function when present: next mock 1`] = ` 1600 | [MockFunction] { 1601 | "calls": Array [ 1602 | Array [ 1603 | Object { 1604 | "type": "REQUEST", 1605 | }, 1606 | ], 1607 | Array [ 1608 | Object { 1609 | "meta": undefined, 1610 | "payload": Object { 1611 | "data": "12345", 1612 | }, 1613 | "type": "SUCCESS", 1614 | }, 1615 | ], 1616 | ], 1617 | "results": Array [ 1618 | Object { 1619 | "isThrow": false, 1620 | "value": Object { 1621 | "type": "REQUEST", 1622 | }, 1623 | }, 1624 | Object { 1625 | "isThrow": false, 1626 | "value": Object { 1627 | "meta": undefined, 1628 | "payload": Object { 1629 | "data": "12345", 1630 | }, 1631 | "type": "SUCCESS", 1632 | }, 1633 | }, 1634 | ], 1635 | } 1636 | `; 1637 | 1638 | exports[`#apiMiddleware must use an [RSAA].options function when present: options() 1`] = ` 1639 | [MockFunction] { 1640 | "calls": Array [ 1641 | Array [ 1642 | undefined, 1643 | ], 1644 | ], 1645 | "results": Array [ 1646 | Object { 1647 | "isThrow": false, 1648 | "value": Object {}, 1649 | }, 1650 | ], 1651 | } 1652 | `; 1653 | 1654 | exports[`#apiMiddleware must use an async [RSAA].body function when present: body() 1`] = ` 1655 | [MockFunction] { 1656 | "calls": Array [ 1657 | Array [ 1658 | undefined, 1659 | ], 1660 | ], 1661 | "results": Array [ 1662 | Object { 1663 | "isThrow": false, 1664 | "value": Promise {}, 1665 | }, 1666 | ], 1667 | } 1668 | `; 1669 | 1670 | exports[`#apiMiddleware must use an async [RSAA].body function when present: fetch mock 1`] = ` 1671 | Object { 1672 | "calls": Array [ 1673 | Array [ 1674 | "http://127.0.0.1/api/users/1", 1675 | Object { 1676 | "body": "mockBody", 1677 | "credentials": undefined, 1678 | "headers": Object {}, 1679 | "method": "GET", 1680 | }, 1681 | ], 1682 | ], 1683 | "instances": Array [ 1684 | undefined, 1685 | ], 1686 | "invocationCallOrder": Any, 1687 | "results": Array [ 1688 | Object { 1689 | "isThrow": false, 1690 | "value": Promise {}, 1691 | }, 1692 | ], 1693 | } 1694 | `; 1695 | 1696 | exports[`#apiMiddleware must use an async [RSAA].body function when present: final result 1`] = ` 1697 | Object { 1698 | "meta": undefined, 1699 | "payload": Object { 1700 | "data": "12345", 1701 | }, 1702 | "type": "SUCCESS", 1703 | } 1704 | `; 1705 | 1706 | exports[`#apiMiddleware must use an async [RSAA].body function when present: next mock 1`] = ` 1707 | [MockFunction] { 1708 | "calls": Array [ 1709 | Array [ 1710 | Object { 1711 | "type": "REQUEST", 1712 | }, 1713 | ], 1714 | Array [ 1715 | Object { 1716 | "meta": undefined, 1717 | "payload": Object { 1718 | "data": "12345", 1719 | }, 1720 | "type": "SUCCESS", 1721 | }, 1722 | ], 1723 | ], 1724 | "results": Array [ 1725 | Object { 1726 | "isThrow": false, 1727 | "value": Object { 1728 | "type": "REQUEST", 1729 | }, 1730 | }, 1731 | Object { 1732 | "isThrow": false, 1733 | "value": Object { 1734 | "meta": undefined, 1735 | "payload": Object { 1736 | "data": "12345", 1737 | }, 1738 | "type": "SUCCESS", 1739 | }, 1740 | }, 1741 | ], 1742 | } 1743 | `; 1744 | 1745 | exports[`#apiMiddleware must use an async [RSAA].endpoint function when present: endpoint() 1`] = ` 1746 | [MockFunction] { 1747 | "calls": Array [ 1748 | Array [ 1749 | undefined, 1750 | ], 1751 | ], 1752 | "results": Array [ 1753 | Object { 1754 | "isThrow": false, 1755 | "value": Promise {}, 1756 | }, 1757 | ], 1758 | } 1759 | `; 1760 | 1761 | exports[`#apiMiddleware must use an async [RSAA].endpoint function when present: fetch mock 1`] = ` 1762 | Object { 1763 | "calls": Array [ 1764 | Array [ 1765 | "http://127.0.0.1/api/users/1", 1766 | Object { 1767 | "body": undefined, 1768 | "credentials": undefined, 1769 | "headers": Object {}, 1770 | "method": "GET", 1771 | }, 1772 | ], 1773 | ], 1774 | "instances": Array [ 1775 | undefined, 1776 | ], 1777 | "invocationCallOrder": Any, 1778 | "results": Array [ 1779 | Object { 1780 | "isThrow": false, 1781 | "value": Promise {}, 1782 | }, 1783 | ], 1784 | } 1785 | `; 1786 | 1787 | exports[`#apiMiddleware must use an async [RSAA].endpoint function when present: final result 1`] = ` 1788 | Object { 1789 | "meta": undefined, 1790 | "payload": Object { 1791 | "data": "12345", 1792 | }, 1793 | "type": "SUCCESS", 1794 | } 1795 | `; 1796 | 1797 | exports[`#apiMiddleware must use an async [RSAA].endpoint function when present: next mock 1`] = ` 1798 | [MockFunction] { 1799 | "calls": Array [ 1800 | Array [ 1801 | Object { 1802 | "type": "REQUEST", 1803 | }, 1804 | ], 1805 | Array [ 1806 | Object { 1807 | "meta": undefined, 1808 | "payload": Object { 1809 | "data": "12345", 1810 | }, 1811 | "type": "SUCCESS", 1812 | }, 1813 | ], 1814 | ], 1815 | "results": Array [ 1816 | Object { 1817 | "isThrow": false, 1818 | "value": Object { 1819 | "type": "REQUEST", 1820 | }, 1821 | }, 1822 | Object { 1823 | "isThrow": false, 1824 | "value": Object { 1825 | "meta": undefined, 1826 | "payload": Object { 1827 | "data": "12345", 1828 | }, 1829 | "type": "SUCCESS", 1830 | }, 1831 | }, 1832 | ], 1833 | } 1834 | `; 1835 | 1836 | exports[`#apiMiddleware must use an async [RSAA].headers function when present: fetch mock 1`] = ` 1837 | Object { 1838 | "calls": Array [ 1839 | Array [ 1840 | "http://127.0.0.1/api/users/1", 1841 | Object { 1842 | "body": undefined, 1843 | "credentials": undefined, 1844 | "headers": Object { 1845 | "Test-Header": "test", 1846 | }, 1847 | "method": "GET", 1848 | }, 1849 | ], 1850 | ], 1851 | "instances": Array [ 1852 | undefined, 1853 | ], 1854 | "invocationCallOrder": Any, 1855 | "results": Array [ 1856 | Object { 1857 | "isThrow": false, 1858 | "value": Promise {}, 1859 | }, 1860 | ], 1861 | } 1862 | `; 1863 | 1864 | exports[`#apiMiddleware must use an async [RSAA].headers function when present: final result 1`] = ` 1865 | Object { 1866 | "meta": undefined, 1867 | "payload": Object { 1868 | "data": "12345", 1869 | }, 1870 | "type": "SUCCESS", 1871 | } 1872 | `; 1873 | 1874 | exports[`#apiMiddleware must use an async [RSAA].headers function when present: headers() 1`] = ` 1875 | [MockFunction] { 1876 | "calls": Array [ 1877 | Array [ 1878 | undefined, 1879 | ], 1880 | ], 1881 | "results": Array [ 1882 | Object { 1883 | "isThrow": false, 1884 | "value": Promise {}, 1885 | }, 1886 | ], 1887 | } 1888 | `; 1889 | 1890 | exports[`#apiMiddleware must use an async [RSAA].headers function when present: next mock 1`] = ` 1891 | [MockFunction] { 1892 | "calls": Array [ 1893 | Array [ 1894 | Object { 1895 | "type": "REQUEST", 1896 | }, 1897 | ], 1898 | Array [ 1899 | Object { 1900 | "meta": undefined, 1901 | "payload": Object { 1902 | "data": "12345", 1903 | }, 1904 | "type": "SUCCESS", 1905 | }, 1906 | ], 1907 | ], 1908 | "results": Array [ 1909 | Object { 1910 | "isThrow": false, 1911 | "value": Object { 1912 | "type": "REQUEST", 1913 | }, 1914 | }, 1915 | Object { 1916 | "isThrow": false, 1917 | "value": Object { 1918 | "meta": undefined, 1919 | "payload": Object { 1920 | "data": "12345", 1921 | }, 1922 | "type": "SUCCESS", 1923 | }, 1924 | }, 1925 | ], 1926 | } 1927 | `; 1928 | 1929 | exports[`#apiMiddleware must use an async [RSAA].options function when present: fetch mock 1`] = ` 1930 | Object { 1931 | "calls": Array [ 1932 | Array [ 1933 | "http://127.0.0.1/api/users/1", 1934 | Object { 1935 | "body": undefined, 1936 | "credentials": undefined, 1937 | "headers": Object {}, 1938 | "method": "GET", 1939 | }, 1940 | ], 1941 | ], 1942 | "instances": Array [ 1943 | undefined, 1944 | ], 1945 | "invocationCallOrder": Any, 1946 | "results": Array [ 1947 | Object { 1948 | "isThrow": false, 1949 | "value": Promise {}, 1950 | }, 1951 | ], 1952 | } 1953 | `; 1954 | 1955 | exports[`#apiMiddleware must use an async [RSAA].options function when present: final result 1`] = ` 1956 | Object { 1957 | "meta": undefined, 1958 | "payload": Object { 1959 | "data": "12345", 1960 | }, 1961 | "type": "SUCCESS", 1962 | } 1963 | `; 1964 | 1965 | exports[`#apiMiddleware must use an async [RSAA].options function when present: next mock 1`] = ` 1966 | [MockFunction] { 1967 | "calls": Array [ 1968 | Array [ 1969 | Object { 1970 | "type": "REQUEST", 1971 | }, 1972 | ], 1973 | Array [ 1974 | Object { 1975 | "meta": undefined, 1976 | "payload": Object { 1977 | "data": "12345", 1978 | }, 1979 | "type": "SUCCESS", 1980 | }, 1981 | ], 1982 | ], 1983 | "results": Array [ 1984 | Object { 1985 | "isThrow": false, 1986 | "value": Object { 1987 | "type": "REQUEST", 1988 | }, 1989 | }, 1990 | Object { 1991 | "isThrow": false, 1992 | "value": Object { 1993 | "meta": undefined, 1994 | "payload": Object { 1995 | "data": "12345", 1996 | }, 1997 | "type": "SUCCESS", 1998 | }, 1999 | }, 2000 | ], 2001 | } 2002 | `; 2003 | 2004 | exports[`#apiMiddleware must use an async [RSAA].options function when present: options() 1`] = ` 2005 | [MockFunction] { 2006 | "calls": Array [ 2007 | Array [ 2008 | undefined, 2009 | ], 2010 | ], 2011 | "results": Array [ 2012 | Object { 2013 | "isThrow": false, 2014 | "value": Promise {}, 2015 | }, 2016 | ], 2017 | } 2018 | `; 2019 | 2020 | exports[`#apiMiddleware must use meta property of request type descriptor when it is a function: fetch mock 1`] = ` 2021 | Object { 2022 | "calls": Array [ 2023 | Array [ 2024 | "http://127.0.0.1/api/users/1", 2025 | Object { 2026 | "body": undefined, 2027 | "credentials": undefined, 2028 | "headers": Object {}, 2029 | "method": "GET", 2030 | }, 2031 | ], 2032 | ], 2033 | "instances": Array [ 2034 | undefined, 2035 | ], 2036 | "invocationCallOrder": Any, 2037 | "results": Array [ 2038 | Object { 2039 | "isThrow": false, 2040 | "value": Promise {}, 2041 | }, 2042 | ], 2043 | } 2044 | `; 2045 | 2046 | exports[`#apiMiddleware must use meta property of request type descriptor when it is a function: final result 1`] = ` 2047 | Object { 2048 | "meta": undefined, 2049 | "payload": Object { 2050 | "data": "12345", 2051 | }, 2052 | "type": "SUCCESS", 2053 | } 2054 | `; 2055 | 2056 | exports[`#apiMiddleware must use meta property of request type descriptor when it is a function: meta() 1`] = ` 2057 | [MockFunction] { 2058 | "calls": Array [ 2059 | Array [ 2060 | Object { 2061 | "@@redux-api-middleware/RSAA": Object { 2062 | "endpoint": "http://127.0.0.1/api/users/1", 2063 | "method": "GET", 2064 | "types": Array [ 2065 | Object { 2066 | "meta": "requestMeta", 2067 | "payload": "requestPayload", 2068 | "type": "REQUEST", 2069 | }, 2070 | "SUCCESS", 2071 | "FAILURE", 2072 | ], 2073 | }, 2074 | }, 2075 | undefined, 2076 | ], 2077 | ], 2078 | "results": Array [ 2079 | Object { 2080 | "isThrow": false, 2081 | "value": "requestMeta", 2082 | }, 2083 | ], 2084 | } 2085 | `; 2086 | 2087 | exports[`#apiMiddleware must use meta property of request type descriptor when it is a function: next mock 1`] = ` 2088 | [MockFunction] { 2089 | "calls": Array [ 2090 | Array [ 2091 | Object { 2092 | "meta": "requestMeta", 2093 | "payload": "requestPayload", 2094 | "type": "REQUEST", 2095 | }, 2096 | ], 2097 | Array [ 2098 | Object { 2099 | "meta": undefined, 2100 | "payload": Object { 2101 | "data": "12345", 2102 | }, 2103 | "type": "SUCCESS", 2104 | }, 2105 | ], 2106 | ], 2107 | "results": Array [ 2108 | Object { 2109 | "isThrow": false, 2110 | "value": Object { 2111 | "meta": "requestMeta", 2112 | "payload": "requestPayload", 2113 | "type": "REQUEST", 2114 | }, 2115 | }, 2116 | Object { 2117 | "isThrow": false, 2118 | "value": Object { 2119 | "meta": undefined, 2120 | "payload": Object { 2121 | "data": "12345", 2122 | }, 2123 | "type": "SUCCESS", 2124 | }, 2125 | }, 2126 | ], 2127 | } 2128 | `; 2129 | 2130 | exports[`#apiMiddleware must use payload property of request type descriptor when it is a function: fetch mock 1`] = ` 2131 | Object { 2132 | "calls": Array [ 2133 | Array [ 2134 | "http://127.0.0.1/api/users/1", 2135 | Object { 2136 | "body": undefined, 2137 | "credentials": undefined, 2138 | "headers": Object {}, 2139 | "method": "GET", 2140 | }, 2141 | ], 2142 | ], 2143 | "instances": Array [ 2144 | undefined, 2145 | ], 2146 | "invocationCallOrder": Any, 2147 | "results": Array [ 2148 | Object { 2149 | "isThrow": false, 2150 | "value": Promise {}, 2151 | }, 2152 | ], 2153 | } 2154 | `; 2155 | 2156 | exports[`#apiMiddleware must use payload property of request type descriptor when it is a function: final result 1`] = ` 2157 | Object { 2158 | "meta": undefined, 2159 | "payload": Object { 2160 | "data": "12345", 2161 | }, 2162 | "type": "SUCCESS", 2163 | } 2164 | `; 2165 | 2166 | exports[`#apiMiddleware must use payload property of request type descriptor when it is a function: next mock 1`] = ` 2167 | [MockFunction] { 2168 | "calls": Array [ 2169 | Array [ 2170 | Object { 2171 | "meta": "requestMeta", 2172 | "payload": "requestPayload", 2173 | "type": "REQUEST", 2174 | }, 2175 | ], 2176 | Array [ 2177 | Object { 2178 | "meta": undefined, 2179 | "payload": Object { 2180 | "data": "12345", 2181 | }, 2182 | "type": "SUCCESS", 2183 | }, 2184 | ], 2185 | ], 2186 | "results": Array [ 2187 | Object { 2188 | "isThrow": false, 2189 | "value": Object { 2190 | "meta": "requestMeta", 2191 | "payload": "requestPayload", 2192 | "type": "REQUEST", 2193 | }, 2194 | }, 2195 | Object { 2196 | "isThrow": false, 2197 | "value": Object { 2198 | "meta": undefined, 2199 | "payload": Object { 2200 | "data": "12345", 2201 | }, 2202 | "type": "SUCCESS", 2203 | }, 2204 | }, 2205 | ], 2206 | } 2207 | `; 2208 | 2209 | exports[`#apiMiddleware must use payload property of request type descriptor when it is a function: payload() 1`] = ` 2210 | [MockFunction] { 2211 | "calls": Array [ 2212 | Array [ 2213 | Object { 2214 | "@@redux-api-middleware/RSAA": Object { 2215 | "endpoint": "http://127.0.0.1/api/users/1", 2216 | "method": "GET", 2217 | "types": Array [ 2218 | Object { 2219 | "meta": "requestMeta", 2220 | "payload": "requestPayload", 2221 | "type": "REQUEST", 2222 | }, 2223 | "SUCCESS", 2224 | "FAILURE", 2225 | ], 2226 | }, 2227 | }, 2228 | undefined, 2229 | ], 2230 | ], 2231 | "results": Array [ 2232 | Object { 2233 | "isThrow": false, 2234 | "value": "requestPayload", 2235 | }, 2236 | ], 2237 | } 2238 | `; 2239 | 2240 | exports[`#apiMiddleware mustn't return a promise on actions without a [RSAA] property: final result 1`] = `Object {}`; 2241 | 2242 | exports[`#apiMiddleware mustn't return a promise on actions without a [RSAA] property: next mock 1`] = ` 2243 | [MockFunction] { 2244 | "calls": Array [ 2245 | Array [ 2246 | Object {}, 2247 | ], 2248 | ], 2249 | "results": Array [ 2250 | Object { 2251 | "isThrow": false, 2252 | "value": Object {}, 2253 | }, 2254 | ], 2255 | } 2256 | `; 2257 | -------------------------------------------------------------------------------- /src/__snapshots__/util.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`#actionWith handles a synchronous meta function 1`] = ` 4 | Object { 5 | "meta": "someMeta", 6 | "payload": undefined, 7 | "type": "REQUEST", 8 | } 9 | `; 10 | 11 | exports[`#actionWith handles a synchronous payload function 1`] = ` 12 | Object { 13 | "meta": undefined, 14 | "payload": "somePayload", 15 | "type": "REQUEST", 16 | } 17 | `; 18 | 19 | exports[`#actionWith handles an asynchronous meta function 1`] = ` 20 | Object { 21 | "meta": Promise {}, 22 | "payload": undefined, 23 | "type": "REQUEST", 24 | } 25 | `; 26 | 27 | exports[`#actionWith handles an asynchronous payload function 1`] = ` 28 | Object { 29 | "meta": undefined, 30 | "payload": Promise {}, 31 | "type": "REQUEST", 32 | } 33 | `; 34 | 35 | exports[`#actionWith handles an error in the meta function 1`] = ` 36 | Object { 37 | "error": true, 38 | "payload": [InternalError: test error in meta function], 39 | "type": "REQUEST", 40 | } 41 | `; 42 | 43 | exports[`#actionWith handles an error in the payload function 1`] = ` 44 | Object { 45 | "error": true, 46 | "meta": undefined, 47 | "payload": [InternalError: test error in payload function], 48 | "type": "REQUEST", 49 | } 50 | `; 51 | 52 | exports[`#actionWith handles function payload and meta descriptor properties 1`] = ` 53 | Object { 54 | "meta": "someMetaFromFn", 55 | "payload": "somePayloadFromFn", 56 | "type": "REQUEST", 57 | } 58 | `; 59 | 60 | exports[`#actionWith handles string payload and meta descriptor properties 1`] = ` 61 | Object { 62 | "error": true, 63 | "meta": "someMeta", 64 | "payload": "somePayload", 65 | "type": "REQUEST", 66 | } 67 | `; 68 | 69 | exports[`#getJSON returns the JSON body of a response with a JSONy 'Content-Type' header 1`] = ` 70 | Object { 71 | "message": "ok", 72 | } 73 | `; 74 | 75 | exports[`#normalizeTypeDescriptors handles object types 1`] = ` 76 | Array [ 77 | Object { 78 | "meta": "requestMeta", 79 | "payload": "requestPayload", 80 | "type": "REQUEST", 81 | }, 82 | Object { 83 | "meta": "successMeta", 84 | "payload": "successPayload", 85 | "type": "SUCCESS", 86 | }, 87 | Object { 88 | "meta": "failureMeta", 89 | "payload": "failurePayload", 90 | "type": "FAILURE", 91 | }, 92 | ] 93 | `; 94 | 95 | exports[`#normalizeTypeDescriptors handles string types 1`] = ` 96 | Array [ 97 | Object { 98 | "type": "REQUEST", 99 | }, 100 | Object { 101 | "payload": [Function], 102 | "type": "SUCCESS", 103 | }, 104 | Object { 105 | "payload": [Function], 106 | "type": "FAILURE", 107 | }, 108 | ] 109 | `; 110 | -------------------------------------------------------------------------------- /src/__snapshots__/validation.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`#validateRSAA / #isValidRSAA handles invalid [RSAA].bailout property 1`] = ` 4 | Array [ 5 | "[RSAA].bailout property must be undefined, a boolean, or a function", 6 | ] 7 | `; 8 | 9 | exports[`#validateRSAA / #isValidRSAA handles invalid [RSAA].credentials property (invalid string) 1`] = ` 10 | Array [ 11 | "Invalid [RSAA].credentials: InvalidCredentials", 12 | ] 13 | `; 14 | 15 | exports[`#validateRSAA / #isValidRSAA handles invalid [RSAA].credentials property (object) 1`] = ` 16 | Array [ 17 | "[RSAA].credentials property must be undefined, or a string", 18 | ] 19 | `; 20 | 21 | exports[`#validateRSAA / #isValidRSAA handles invalid [RSAA].endpoint property 1`] = ` 22 | Array [ 23 | "[RSAA].endpoint property must be a string or a function", 24 | ] 25 | `; 26 | 27 | exports[`#validateRSAA / #isValidRSAA handles invalid [RSAA].fetch property 1`] = ` 28 | Array [ 29 | "[RSAA].fetch property must be a function", 30 | ] 31 | `; 32 | 33 | exports[`#validateRSAA / #isValidRSAA handles invalid [RSAA].headers property 1`] = ` 34 | Array [ 35 | "[RSAA].headers property must be undefined, a plain JavaScript object, or a function", 36 | ] 37 | `; 38 | 39 | exports[`#validateRSAA / #isValidRSAA handles invalid [RSAA].method property (invalid string) 1`] = ` 40 | Array [ 41 | "Invalid [RSAA].method: INVALID_METHOD", 42 | ] 43 | `; 44 | 45 | exports[`#validateRSAA / #isValidRSAA handles invalid [RSAA].method property (object) 1`] = ` 46 | Array [ 47 | "[RSAA].method property must be a string", 48 | ] 49 | `; 50 | 51 | exports[`#validateRSAA / #isValidRSAA handles invalid [RSAA].ok property 1`] = ` 52 | Array [ 53 | "[RSAA].ok property must be a function", 54 | ] 55 | `; 56 | 57 | exports[`#validateRSAA / #isValidRSAA handles invalid [RSAA].options property 1`] = ` 58 | Array [ 59 | "[RSAA].options property must be undefined, a plain JavaScript object, or a function", 60 | ] 61 | `; 62 | 63 | exports[`#validateRSAA / #isValidRSAA handles invalid [RSAA].types property (invalid objects) 1`] = ` 64 | Array [ 65 | "Invalid request type", 66 | "Invalid success type", 67 | "Invalid failure type", 68 | ] 69 | `; 70 | 71 | exports[`#validateRSAA / #isValidRSAA handles invalid [RSAA].types property (object) 1`] = ` 72 | Array [ 73 | "[RSAA].types property must be an array of length 3", 74 | ] 75 | `; 76 | 77 | exports[`#validateRSAA / #isValidRSAA handles invalid [RSAA].types property (wrong length) 1`] = ` 78 | Array [ 79 | "[RSAA].types property must be an array of length 3", 80 | ] 81 | `; 82 | 83 | exports[`#validateRSAA / #isValidRSAA handles invalid RSAA value (invalid object) 1`] = ` 84 | Array [ 85 | "Invalid [RSAA] key: invalidKey", 86 | "[RSAA] must have an endpoint property", 87 | "[RSAA] must have a method property", 88 | "[RSAA] must have a types property", 89 | ] 90 | `; 91 | 92 | exports[`#validateRSAA / #isValidRSAA handles invalid RSAA value (string) 1`] = ` 93 | Array [ 94 | "[RSAA] property must be a plain JavaScript object", 95 | "[RSAA] must have an endpoint property", 96 | "[RSAA] must have a method property", 97 | "[RSAA] must have a types property", 98 | ] 99 | `; 100 | 101 | exports[`#validateRSAA / #isValidRSAA handles invalid actions 1`] = ` 102 | Array [ 103 | "RSAAs must be plain JavaScript objects with an [RSAA] property", 104 | ] 105 | `; 106 | 107 | exports[`#validateRSAA / #isValidRSAA handles missing RSAA properties 1`] = ` 108 | Array [ 109 | "[RSAA] must have an endpoint property", 110 | "[RSAA] must have a method property", 111 | "[RSAA] must have a types property", 112 | ] 113 | `; 114 | 115 | exports[`#validateRSAA / #isValidRSAA handles top-level string properties other than RSAA 1`] = `Array []`; 116 | 117 | exports[`#validateRSAA / #isValidRSAA handles top-level symbol properties other than RSAA 1`] = `Array []`; 118 | 119 | exports[`#validateRSAA / #isValidRSAA handles valid RSAA with bailout boolean 1`] = `Array []`; 120 | 121 | exports[`#validateRSAA / #isValidRSAA handles valid RSAA with bailout function 1`] = `Array []`; 122 | 123 | exports[`#validateRSAA / #isValidRSAA handles valid RSAA with endpoint function 1`] = `Array []`; 124 | 125 | exports[`#validateRSAA / #isValidRSAA handles valid RSAA with endpoint string 1`] = `Array []`; 126 | 127 | exports[`#validateRSAA / #isValidRSAA handles valid RSAA with fetch function 1`] = `Array []`; 128 | 129 | exports[`#validateRSAA / #isValidRSAA handles valid RSAA with headers function 1`] = `Array []`; 130 | 131 | exports[`#validateRSAA / #isValidRSAA handles valid RSAA with headers object 1`] = `Array []`; 132 | 133 | exports[`#validateRSAA / #isValidRSAA handles valid RSAA with options function 1`] = `Array []`; 134 | 135 | exports[`#validateRSAA / #isValidRSAA handles valid RSAA with options object 1`] = `Array []`; 136 | 137 | exports[`#validateRSAA / #isValidRSAA handles valid RSAA with types of symbols 1`] = `Array []`; 138 | 139 | exports[`#validateRSAA / #isValidRSAA handles valid RSAA with types of type descriptors 1`] = `Array []`; 140 | -------------------------------------------------------------------------------- /src/errors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Error class for an RSAA that does not conform to the RSAA definition 3 | * 4 | * @class InvalidRSAA 5 | * @access public 6 | * @param {array} validationErrors - an array of validation errors 7 | */ 8 | class InvalidRSAA extends Error { 9 | constructor(validationErrors) { 10 | super(); 11 | this.name = 'InvalidRSAA'; 12 | this.message = 'Invalid RSAA'; 13 | this.validationErrors = validationErrors; 14 | } 15 | } 16 | 17 | /** 18 | * Error class for a custom `payload` or `meta` function throwing 19 | * 20 | * @class InternalError 21 | * @access public 22 | * @param {string} message - the error message 23 | */ 24 | class InternalError extends Error { 25 | constructor(message) { 26 | super(); 27 | this.name = 'InternalError'; 28 | this.message = message; 29 | } 30 | } 31 | 32 | /** 33 | * Error class for an error raised trying to make an API call 34 | * 35 | * @class RequestError 36 | * @access public 37 | * @param {string} message - the error message 38 | */ 39 | class RequestError extends Error { 40 | constructor(message) { 41 | super(); 42 | this.name = 'RequestError'; 43 | this.message = message; 44 | } 45 | } 46 | 47 | /** 48 | * Error class for an API response outside the 200 range 49 | * 50 | * @class ApiError 51 | * @access public 52 | * @param {number} status - the status code of the API response 53 | * @param {string} statusText - the status text of the API response 54 | * @param {object} response - the parsed JSON response of the API server if the 55 | * 'Content-Type' header signals a JSON response 56 | */ 57 | class ApiError extends Error { 58 | constructor(status, statusText, response) { 59 | super(); 60 | this.name = 'ApiError'; 61 | this.status = status; 62 | this.statusText = statusText; 63 | this.response = response; 64 | this.message = `${status} - ${statusText}`; 65 | } 66 | } 67 | 68 | export { InvalidRSAA, InternalError, RequestError, ApiError }; 69 | -------------------------------------------------------------------------------- /src/errors.test.js: -------------------------------------------------------------------------------- 1 | // Public package exports 2 | import { 3 | InvalidRSAA, 4 | InternalError, 5 | RequestError, 6 | ApiError 7 | } from 'redux-api-middleware'; 8 | 9 | describe('InvalidRSAA', () => { 10 | const validationErrors = ['validation error 1', 'validation error 2']; 11 | const error = new InvalidRSAA(validationErrors); 12 | 13 | it('is an error object', () => { 14 | expect(error).toBeInstanceOf(Error); 15 | }); 16 | 17 | it('matches snapshot', () => { 18 | expect(error).toMatchSnapshot(); 19 | expect(Object.entries(error)).toMatchSnapshot('object.entries'); 20 | }); 21 | }); 22 | 23 | describe('InternalError', () => { 24 | const error = new InternalError('error thrown in payload function'); 25 | 26 | it('is an error object', () => { 27 | expect(error).toBeInstanceOf(Error); 28 | }); 29 | 30 | it('matches snapshot', () => { 31 | expect(error).toMatchSnapshot(); 32 | expect(Object.entries(error)).toMatchSnapshot('object.entries'); 33 | }); 34 | }); 35 | 36 | describe('RequestError', () => { 37 | const error = new RequestError('Network request failed'); 38 | 39 | it('is an error object', () => { 40 | expect(error).toBeInstanceOf(Error); 41 | }); 42 | 43 | it('matches snapshot', () => { 44 | expect(error).toMatchSnapshot(); 45 | expect(Object.entries(error)).toMatchSnapshot('object.entries'); 46 | }); 47 | }); 48 | 49 | describe('ApiError', () => { 50 | const json = { error: 'Resource not found' }; 51 | const error = new ApiError(404, 'Not Found', json); 52 | 53 | it('is an error object', () => { 54 | expect(error).toBeInstanceOf(Error); 55 | }); 56 | 57 | it('matches snapshot', () => { 58 | expect(error).toMatchSnapshot(); 59 | expect(Object.entries(error)).toMatchSnapshot('object.entries'); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Redux middleware for calling an API 3 | * @module redux-api-middleware 4 | * @requires lodash.isplainobject 5 | * @exports {string} RSAA 6 | * @exports {function} isRSAA 7 | * @exports {function} validateRSAA 8 | * @exports {function} isValidRSAA 9 | * @exports {error} InvalidRSAA 10 | * @exports {error} InternalError 11 | * @exports {error} RequestError 12 | * @exports {error} ApiError 13 | * @exports {function} getJSON 14 | * @exports {function} createMiddleware 15 | * @exports {ReduxMiddleWare} apiMiddleware 16 | */ 17 | 18 | /** 19 | * @typedef {function} ReduxMiddleware 20 | * @param {object} store 21 | * @returns {ReduxNextHandler} 22 | * 23 | * @typedef {function} ReduxNextHandler 24 | * @param {function} next 25 | * @returns {ReduxActionHandler} 26 | * 27 | * @typedef {function} ReduxActionHandler 28 | * @param {object} action 29 | * @returns undefined 30 | */ 31 | 32 | import RSAA from './RSAA'; 33 | import { isRSAA, validateRSAA, isValidRSAA } from './validation'; 34 | import { InvalidRSAA, InternalError, RequestError, ApiError } from './errors'; 35 | import { createAction, getJSON } from './util'; 36 | import { apiMiddleware, createMiddleware } from './middleware'; 37 | 38 | export { 39 | RSAA, 40 | isRSAA, 41 | validateRSAA, 42 | isValidRSAA, 43 | InvalidRSAA, 44 | InternalError, 45 | RequestError, 46 | ApiError, 47 | getJSON, 48 | createAction, 49 | createMiddleware, 50 | apiMiddleware 51 | }; 52 | -------------------------------------------------------------------------------- /src/middleware.js: -------------------------------------------------------------------------------- 1 | import RSAA from './RSAA'; 2 | import { isRSAA, validateRSAA } from './validation'; 3 | import { InvalidRSAA, RequestError, InternalError } from './errors'; 4 | import { normalizeTypeDescriptors, actionWith } from './util'; 5 | 6 | /** 7 | * Default options for redux-api-middleware 8 | * These can be customized by passing options into `createMiddleware` 9 | * @type {Object} 10 | */ 11 | const defaults = { 12 | ok: res => res.ok 13 | }; 14 | 15 | /** 16 | * A middleware creator used to create a ReduxApiMiddleware 17 | * with custom defaults 18 | * 19 | * @type {function} 20 | * @returns {ReduxMiddleware} 21 | * @access public 22 | */ 23 | function createMiddleware(options = {}) { 24 | const middlewareOptions = Object.assign({}, defaults, options); 25 | 26 | return ({ getState }) => next => action => { 27 | // Do not process actions without an [RSAA] property 28 | if (!isRSAA(action)) { 29 | return next(action); 30 | } 31 | 32 | return (async () => { 33 | // Try to dispatch an error request FSA for invalid RSAAs 34 | const validationErrors = validateRSAA(action); 35 | if (validationErrors.length) { 36 | const callAPI = action[RSAA]; 37 | if (callAPI.types && Array.isArray(callAPI.types)) { 38 | let requestType = callAPI.types[0]; 39 | if (requestType && requestType.type) { 40 | requestType = requestType.type; 41 | } 42 | next({ 43 | type: requestType, 44 | payload: new InvalidRSAA(validationErrors), 45 | error: true 46 | }); 47 | } 48 | return; 49 | } 50 | 51 | // Parse the validated RSAA action 52 | const callAPI = action[RSAA]; 53 | var { 54 | endpoint, 55 | body, 56 | headers, 57 | options = {}, 58 | fetch: doFetch = middlewareOptions.fetch || fetch, 59 | ok = middlewareOptions.ok 60 | } = callAPI; 61 | const { method, credentials, bailout, types } = callAPI; 62 | const [requestType, successType, failureType] = normalizeTypeDescriptors( 63 | types 64 | ); 65 | 66 | // Should we bail out? 67 | try { 68 | if ( 69 | (typeof bailout === 'boolean' && bailout) || 70 | (typeof bailout === 'function' && bailout(getState())) 71 | ) { 72 | return; 73 | } 74 | } catch (e) { 75 | return next( 76 | await actionWith( 77 | { 78 | ...failureType, 79 | payload: new RequestError('[RSAA].bailout function failed'), 80 | error: true 81 | }, 82 | [action, getState()] 83 | ) 84 | ); 85 | } 86 | 87 | // Process [RSAA].endpoint function 88 | if (typeof endpoint === 'function') { 89 | try { 90 | endpoint = await endpoint(getState()); 91 | } catch (e) { 92 | return next( 93 | await actionWith( 94 | { 95 | ...failureType, 96 | payload: new RequestError('[RSAA].endpoint function failed'), 97 | error: true 98 | }, 99 | [action, getState()] 100 | ) 101 | ); 102 | } 103 | } 104 | 105 | // Process [RSAA].body function 106 | if (typeof body === 'function') { 107 | try { 108 | body = await body(getState()); 109 | } catch (e) { 110 | return next( 111 | await actionWith( 112 | { 113 | ...failureType, 114 | payload: new RequestError('[RSAA].body function failed'), 115 | error: true 116 | }, 117 | [action, getState()] 118 | ) 119 | ); 120 | } 121 | } 122 | 123 | // Process [RSAA].headers function 124 | if (typeof headers === 'function') { 125 | try { 126 | headers = await headers(getState()); 127 | } catch (e) { 128 | return next( 129 | await actionWith( 130 | { 131 | ...failureType, 132 | payload: new RequestError('[RSAA].headers function failed'), 133 | error: true 134 | }, 135 | [action, getState()] 136 | ) 137 | ); 138 | } 139 | } 140 | 141 | // Process [RSAA].options function 142 | if (typeof options === 'function') { 143 | try { 144 | options = await options(getState()); 145 | } catch (e) { 146 | return next( 147 | await actionWith( 148 | { 149 | ...failureType, 150 | payload: new RequestError('[RSAA].options function failed'), 151 | error: true 152 | }, 153 | [action, getState()] 154 | ) 155 | ); 156 | } 157 | } 158 | 159 | // We can now dispatch the request FSA 160 | if ( 161 | typeof requestType.payload === 'function' || 162 | typeof requestType.meta === 'function' 163 | ) { 164 | next(await actionWith(requestType, [action, getState()])); 165 | } else { 166 | next(requestType); 167 | } 168 | 169 | let res; 170 | try { 171 | // Make the API call 172 | res = await doFetch(endpoint, { 173 | ...options, 174 | method, 175 | body: body || undefined, 176 | credentials, 177 | headers: headers || {} 178 | }); 179 | } catch (e) { 180 | // The request was malformed, or there was a network error 181 | return next( 182 | await actionWith( 183 | { 184 | ...failureType, 185 | payload: new RequestError(e.message), 186 | error: true 187 | }, 188 | [action, getState()] 189 | ) 190 | ); 191 | } 192 | 193 | let isOk; 194 | try { 195 | isOk = ok(res); 196 | } catch (e) { 197 | return next( 198 | await actionWith( 199 | { 200 | ...failureType, 201 | payload: new InternalError('[RSAA].ok function failed'), 202 | error: true 203 | }, 204 | [action, getState(), res] 205 | ) 206 | ); 207 | } 208 | 209 | // Process the server response 210 | if (isOk) { 211 | return next(await actionWith(successType, [action, getState(), res])); 212 | } else { 213 | return next( 214 | await actionWith( 215 | { 216 | ...failureType, 217 | error: true 218 | }, 219 | [action, getState(), res] 220 | ) 221 | ); 222 | } 223 | })(); 224 | }; 225 | } 226 | 227 | /** 228 | * A Redux middleware that processes RSAA actions. 229 | * 230 | * @type {ReduxMiddleware} 231 | * @access public 232 | * @deprecated since v3.2.0 use `createMiddleware` 233 | */ 234 | function apiMiddleware({ getState }) { 235 | return createMiddleware()({ getState }); 236 | } 237 | 238 | export { createMiddleware, apiMiddleware }; 239 | -------------------------------------------------------------------------------- /src/middleware.test.js: -------------------------------------------------------------------------------- 1 | // Public package exports 2 | import { 3 | RSAA, 4 | apiMiddleware, 5 | createMiddleware, 6 | InternalError 7 | } from 'redux-api-middleware'; 8 | 9 | const fetchMockSnapshotMatcher = { 10 | invocationCallOrder: expect.any(Object) 11 | }; 12 | // const fetchMockSnapshotMatcher = {}; 13 | 14 | const doTestMiddleware = async ({ response, action }) => { 15 | if (response) { 16 | const { body, ...mockConfig } = response; 17 | fetch.mockResponseOnce(body, mockConfig); 18 | } 19 | 20 | const doGetState = jest.fn(); 21 | doGetState.mockImplementation(() => {}); 22 | const doNext = jest.fn(); 23 | doNext.mockImplementation(it => it); 24 | 25 | const nextHandler = apiMiddleware({ getState: doGetState }); 26 | const actionHandler = nextHandler(doNext); 27 | const result = actionHandler(action); 28 | 29 | if (result) { 30 | const final = await result; 31 | if (final) { 32 | expect(final).toMatchSnapshot({}, 'final result'); 33 | } 34 | } 35 | 36 | if (doNext.mock.calls.length) { 37 | expect(doNext).toMatchSnapshot({}, 'next mock'); 38 | } 39 | 40 | if (fetch.mock.calls.length) { 41 | expect(fetch.mock).toMatchSnapshot( 42 | { 43 | invocationCallOrder: expect.any(Object) 44 | }, 45 | 'fetch mock' 46 | ); 47 | } 48 | 49 | return { 50 | doGetState, 51 | nextHandler, 52 | doNext, 53 | actionHandler, 54 | result 55 | }; 56 | }; 57 | 58 | describe('#createMiddleware', () => { 59 | it('returns a redux middleware', () => { 60 | const doGetState = () => {}; 61 | const middleware = createMiddleware(); 62 | const nextHandler = middleware({ getState: doGetState }); 63 | const doNext = () => {}; 64 | const actionHandler = nextHandler(doNext); 65 | 66 | expect(typeof middleware).toEqual('function'); 67 | expect(middleware).toHaveLength(1); 68 | 69 | expect(typeof nextHandler).toEqual('function'); 70 | expect(nextHandler).toHaveLength(1); 71 | 72 | expect(typeof actionHandler).toEqual('function'); 73 | expect(actionHandler).toHaveLength(1); 74 | }); 75 | }); 76 | 77 | describe('#apiMiddleware', () => { 78 | it('is a redux middleware', () => { 79 | const doGetState = () => {}; 80 | const nextHandler = apiMiddleware({ getState: doGetState }); 81 | const doNext = () => {}; 82 | const actionHandler = nextHandler(doNext); 83 | 84 | expect(typeof apiMiddleware).toEqual('function'); 85 | expect(apiMiddleware).toHaveLength(1); 86 | 87 | expect(typeof nextHandler).toEqual('function'); 88 | expect(nextHandler).toHaveLength(1); 89 | 90 | expect(typeof actionHandler).toEqual('function'); 91 | expect(actionHandler).toHaveLength(1); 92 | }); 93 | 94 | it('must pass actions without an [RSAA] property to the next handler', async () => { 95 | const action = {}; 96 | 97 | const { doNext } = await doTestMiddleware({ 98 | action 99 | }); 100 | expect(doNext).toHaveBeenCalledWith(action); 101 | }); 102 | 103 | it("mustn't return a promise on actions without a [RSAA] property", async () => { 104 | const action = {}; 105 | 106 | const { result } = await doTestMiddleware({ 107 | action 108 | }); 109 | 110 | expect(result.then).toBeUndefined(); 111 | }); 112 | 113 | it('must return a promise on actions without a [RSAA] property', async () => { 114 | const action = { [RSAA]: {} }; 115 | 116 | const { result } = await doTestMiddleware({ 117 | action 118 | }); 119 | 120 | expect(typeof result.then).toEqual('function'); 121 | }); 122 | 123 | it('must dispatch an error request FSA for an invalid RSAA with a string request type', async () => { 124 | const action = { 125 | [RSAA]: { 126 | types: ['REQUEST'] 127 | } 128 | }; 129 | 130 | await doTestMiddleware({ 131 | action 132 | }); 133 | }); 134 | 135 | it('must dispatch an error request FSA for an invalid RSAA with a descriptor request type', async () => { 136 | const action = { 137 | [RSAA]: { 138 | types: [ 139 | { 140 | type: 'REQUEST' 141 | } 142 | ] 143 | } 144 | }; 145 | 146 | await doTestMiddleware({ 147 | action 148 | }); 149 | }); 150 | 151 | it('must do nothing for an invalid RSAA without a request type', async () => { 152 | const action = { 153 | [RSAA]: {} 154 | }; 155 | 156 | const { doNext } = await doTestMiddleware({ 157 | action 158 | }); 159 | 160 | expect(doNext).not.toHaveBeenCalled(); 161 | }); 162 | 163 | it('must dispatch an error request FSA when [RSAA].bailout fails', async () => { 164 | const action = { 165 | [RSAA]: { 166 | endpoint: '', 167 | method: 'GET', 168 | bailout: () => { 169 | throw new Error(); 170 | }, 171 | types: [ 172 | { 173 | type: 'REQUEST', 174 | payload: () => 'ignoredPayload', 175 | meta: () => 'someMeta' 176 | }, 177 | 'SUCCESS', 178 | 'FAILURE' 179 | ] 180 | } 181 | }; 182 | 183 | await doTestMiddleware({ 184 | action 185 | }); 186 | }); 187 | 188 | it('must dispatch an error request FSA when [RSAA].body fails', async () => { 189 | const action = { 190 | [RSAA]: { 191 | endpoint: 'http://127.0.0.1/api/users/1', 192 | body: () => { 193 | throw new Error(); 194 | }, 195 | method: 'GET', 196 | types: [ 197 | { 198 | type: 'REQUEST', 199 | payload: 'ignoredPayload', 200 | meta: 'someMeta' 201 | }, 202 | 'SUCCESS', 203 | 'FAILURE' 204 | ] 205 | } 206 | }; 207 | 208 | await doTestMiddleware({ 209 | action 210 | }); 211 | }); 212 | 213 | it('must dispatch an error request FSA when [RSAA].endpoint fails', async () => { 214 | const action = { 215 | [RSAA]: { 216 | endpoint: () => { 217 | throw new Error(); 218 | }, 219 | method: 'GET', 220 | types: [ 221 | { 222 | type: 'REQUEST', 223 | payload: 'ignoredPayload', 224 | meta: 'someMeta' 225 | }, 226 | 'SUCCESS', 227 | 'FAILURE' 228 | ] 229 | } 230 | }; 231 | 232 | await doTestMiddleware({ 233 | action 234 | }); 235 | }); 236 | 237 | it('must dispatch an error request FSA when [RSAA].headers fails', async () => { 238 | const action = { 239 | [RSAA]: { 240 | endpoint: '', 241 | method: 'GET', 242 | headers: () => { 243 | throw new Error(); 244 | }, 245 | types: [ 246 | { 247 | type: 'REQUEST', 248 | payload: 'ignoredPayload', 249 | meta: 'someMeta' 250 | }, 251 | 'SUCCESS', 252 | 'FAILURE' 253 | ] 254 | } 255 | }; 256 | 257 | await doTestMiddleware({ 258 | action 259 | }); 260 | }); 261 | 262 | it('must dispatch an error request FSA when [RSAA].options fails', async () => { 263 | const action = { 264 | [RSAA]: { 265 | endpoint: '', 266 | method: 'GET', 267 | options: () => { 268 | throw new Error(); 269 | }, 270 | types: [ 271 | { 272 | type: 'REQUEST', 273 | payload: 'ignoredPayload', 274 | meta: 'someMeta' 275 | }, 276 | 'SUCCESS', 277 | 'FAILURE' 278 | ] 279 | } 280 | }; 281 | 282 | await doTestMiddleware({ 283 | action 284 | }); 285 | }); 286 | 287 | it('must dispatch an error request FSA when [RSAA].ok fails', async () => { 288 | const action = { 289 | [RSAA]: { 290 | endpoint: 'http://127.0.0.1/api/users/1', 291 | method: 'GET', 292 | ok: () => { 293 | throw new Error(); 294 | }, 295 | types: ['REQUEST', 'SUCCESS', 'FAILURE'] 296 | } 297 | }; 298 | 299 | await doTestMiddleware({ 300 | action, 301 | response: { 302 | body: JSON.stringify({ data: '12345' }), 303 | status: 200, 304 | headers: { 305 | 'Content-Type': 'application/json' 306 | } 307 | } 308 | }); 309 | }); 310 | 311 | it('must dispatch a failure FSA with an error on a request error', async () => { 312 | fetch.mockRejectOnce(new Error('Test request error')); 313 | 314 | const action = { 315 | [RSAA]: { 316 | endpoint: 'http://127.0.0.1/api/users/1', 317 | method: 'GET', 318 | types: [ 319 | { 320 | type: 'REQUEST', 321 | payload: 'ignoredPayload', 322 | meta: 'someMeta' 323 | }, 324 | 'SUCCESS', 325 | 'FAILURE' 326 | ] 327 | } 328 | }; 329 | 330 | await doTestMiddleware({ 331 | action 332 | }); 333 | }); 334 | 335 | it('must use an [RSAA].bailout boolean when present', async () => { 336 | const action = { 337 | [RSAA]: { 338 | endpoint: 'http://127.0.0.1/api/users/1', 339 | method: 'GET', 340 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 341 | bailout: true 342 | } 343 | }; 344 | 345 | await doTestMiddleware({ 346 | action 347 | }); 348 | }); 349 | 350 | it('must use an [RSAA].bailout function when present', async () => { 351 | const bailout = jest.fn(); 352 | bailout.mockReturnValue(true); 353 | 354 | const action = { 355 | [RSAA]: { 356 | endpoint: 'http://127.0.0.1/api/users/1', 357 | method: 'GET', 358 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 359 | bailout 360 | } 361 | }; 362 | 363 | const { doNext } = await doTestMiddleware({ 364 | action, 365 | response: { 366 | body: JSON.stringify({ data: '12345' }), 367 | status: 200, 368 | headers: { 369 | 'Content-Type': 'application/json' 370 | } 371 | } 372 | }); 373 | 374 | expect(bailout).toMatchSnapshot({}, 'bailout()'); 375 | expect(doNext).not.toHaveBeenCalled(); 376 | }); 377 | 378 | it('must use an [RSAA].body function when present', async () => { 379 | const body = jest.fn(); 380 | body.mockReturnValue('mockBody'); 381 | 382 | const action = { 383 | [RSAA]: { 384 | endpoint: 'http://127.0.0.1/api/users/1', 385 | method: 'GET', 386 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 387 | body 388 | } 389 | }; 390 | 391 | await doTestMiddleware({ 392 | action, 393 | response: { 394 | body: JSON.stringify({ data: '12345' }), 395 | status: 200, 396 | headers: { 397 | 'Content-Type': 'application/json' 398 | } 399 | } 400 | }); 401 | 402 | expect(body).toMatchSnapshot({}, 'body()'); 403 | }); 404 | 405 | it('must use an async [RSAA].body function when present', async () => { 406 | const body = jest.fn(); 407 | body.mockResolvedValue('mockBody'); 408 | 409 | const action = { 410 | [RSAA]: { 411 | endpoint: 'http://127.0.0.1/api/users/1', 412 | method: 'GET', 413 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 414 | body 415 | } 416 | }; 417 | 418 | await doTestMiddleware({ 419 | action, 420 | response: { 421 | body: JSON.stringify({ data: '12345' }), 422 | status: 200, 423 | headers: { 424 | 'Content-Type': 'application/json' 425 | } 426 | } 427 | }); 428 | 429 | expect(body).toMatchSnapshot({}, 'body()'); 430 | }); 431 | 432 | it('must use an [RSAA].endpoint function when present', async () => { 433 | const endpoint = jest.fn(); 434 | endpoint.mockReturnValue('http://127.0.0.1/api/users/1'); 435 | 436 | const action = { 437 | [RSAA]: { 438 | endpoint, 439 | method: 'GET', 440 | types: ['REQUEST', 'SUCCESS', 'FAILURE'] 441 | } 442 | }; 443 | 444 | await doTestMiddleware({ 445 | action, 446 | response: { 447 | body: JSON.stringify({ data: '12345' }), 448 | status: 200, 449 | headers: { 450 | 'Content-Type': 'application/json' 451 | } 452 | } 453 | }); 454 | 455 | expect(endpoint).toMatchSnapshot({}, 'endpoint()'); 456 | }); 457 | 458 | it('must use an async [RSAA].endpoint function when present', async () => { 459 | const endpoint = jest.fn(); 460 | endpoint.mockResolvedValue('http://127.0.0.1/api/users/1'); 461 | 462 | const action = { 463 | [RSAA]: { 464 | endpoint, 465 | method: 'GET', 466 | types: ['REQUEST', 'SUCCESS', 'FAILURE'] 467 | } 468 | }; 469 | 470 | await doTestMiddleware({ 471 | action, 472 | response: { 473 | body: JSON.stringify({ data: '12345' }), 474 | status: 200, 475 | headers: { 476 | 'Content-Type': 'application/json' 477 | } 478 | } 479 | }); 480 | 481 | expect(endpoint).toMatchSnapshot({}, 'endpoint()'); 482 | }); 483 | 484 | it('must use an [RSAA].headers function when present', async () => { 485 | const headers = jest.fn(); 486 | headers.mockReturnValue({ 'Test-Header': 'test' }); 487 | 488 | const action = { 489 | [RSAA]: { 490 | endpoint: 'http://127.0.0.1/api/users/1', 491 | method: 'GET', 492 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 493 | headers 494 | } 495 | }; 496 | 497 | await doTestMiddleware({ 498 | action, 499 | response: { 500 | body: JSON.stringify({ data: '12345' }), 501 | status: 200, 502 | headers: { 503 | 'Content-Type': 'application/json' 504 | } 505 | } 506 | }); 507 | 508 | expect(headers).toMatchSnapshot({}, 'headers()'); 509 | }); 510 | 511 | it('must use an async [RSAA].headers function when present', async () => { 512 | const headers = jest.fn(); 513 | headers.mockResolvedValue({ 'Test-Header': 'test' }); 514 | 515 | const action = { 516 | [RSAA]: { 517 | endpoint: 'http://127.0.0.1/api/users/1', 518 | method: 'GET', 519 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 520 | headers 521 | } 522 | }; 523 | 524 | await doTestMiddleware({ 525 | action, 526 | response: { 527 | body: JSON.stringify({ data: '12345' }), 528 | status: 200, 529 | headers: { 530 | 'Content-Type': 'application/json' 531 | } 532 | } 533 | }); 534 | 535 | expect(headers).toMatchSnapshot({}, 'headers()'); 536 | }); 537 | 538 | it('must use an [RSAA].options function when present', async () => { 539 | const options = jest.fn(); 540 | options.mockReturnValue({}); 541 | 542 | const action = { 543 | [RSAA]: { 544 | endpoint: 'http://127.0.0.1/api/users/1', 545 | method: 'GET', 546 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 547 | options 548 | } 549 | }; 550 | 551 | await doTestMiddleware({ 552 | action, 553 | response: { 554 | body: JSON.stringify({ data: '12345' }), 555 | status: 200, 556 | headers: { 557 | 'Content-Type': 'application/json' 558 | } 559 | } 560 | }); 561 | 562 | expect(options).toMatchSnapshot({}, 'options()'); 563 | }); 564 | 565 | it('must use an async [RSAA].options function when present', async () => { 566 | const options = jest.fn(); 567 | options.mockResolvedValue({}); 568 | 569 | const action = { 570 | [RSAA]: { 571 | endpoint: 'http://127.0.0.1/api/users/1', 572 | method: 'GET', 573 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 574 | options 575 | } 576 | }; 577 | 578 | await doTestMiddleware({ 579 | action, 580 | response: { 581 | body: JSON.stringify({ data: '12345' }), 582 | status: 200, 583 | headers: { 584 | 'Content-Type': 'application/json' 585 | } 586 | } 587 | }); 588 | 589 | expect(options).toMatchSnapshot({}, 'options()'); 590 | }); 591 | 592 | it('must use an [RSAA].ok function when present', async () => { 593 | const ok = jest.fn(); 594 | ok.mockReturnValue(true); 595 | 596 | const action = { 597 | [RSAA]: { 598 | endpoint: 'http://127.0.0.1/api/users/1', 599 | method: 'GET', 600 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 601 | ok 602 | } 603 | }; 604 | 605 | await doTestMiddleware({ 606 | action, 607 | response: { 608 | body: JSON.stringify({ data: '12345' }), 609 | status: 200, 610 | headers: { 611 | 'Content-Type': 'application/json' 612 | } 613 | } 614 | }); 615 | 616 | expect(ok).toMatchSnapshot({}, 'ok()'); 617 | }); 618 | 619 | it('must dispatch a failure FSA when [RSAA].ok returns false on a successful request', async () => { 620 | const ok = jest.fn(); 621 | ok.mockReturnValue(false); 622 | 623 | const action = { 624 | [RSAA]: { 625 | endpoint: 'http://127.0.0.1/api/users/1', 626 | method: 'GET', 627 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 628 | ok 629 | } 630 | }; 631 | 632 | await doTestMiddleware({ 633 | action, 634 | response: { 635 | body: JSON.stringify({ data: '12345' }), 636 | status: 200, 637 | headers: { 638 | 'Content-Type': 'application/json' 639 | } 640 | } 641 | }); 642 | 643 | expect(ok).toMatchSnapshot({}, 'ok()'); 644 | }); 645 | 646 | it('must use a [RSAA].fetch custom fetch wrapper when present', async () => { 647 | const myFetch = async (endpoint, opts) => { 648 | const res = await fetch(endpoint, opts); 649 | const json = await res.json(); 650 | 651 | return new Response( 652 | JSON.stringify({ 653 | ...json, 654 | foo: 'bar' 655 | }), 656 | { 657 | // Example of custom `res.ok` 658 | status: json.error ? 500 : 200, 659 | headers: { 660 | 'Content-Type': 'application/json' 661 | } 662 | } 663 | ); 664 | }; 665 | 666 | const action = { 667 | [RSAA]: { 668 | endpoint: 'http://127.0.0.1/api/users/1', 669 | method: 'GET', 670 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 671 | fetch: myFetch 672 | } 673 | }; 674 | 675 | await doTestMiddleware({ 676 | action, 677 | response: { 678 | body: JSON.stringify({ 679 | id: 1, 680 | name: 'Alan', 681 | error: false 682 | }), 683 | status: 200, 684 | headers: { 685 | 'Content-Type': 'application/json' 686 | } 687 | } 688 | }); 689 | }); 690 | 691 | it('must dispatch correct error payload when [RSAA].fetch wrapper returns an error response', async () => { 692 | const myFetch = async (endpoint, opts) => { 693 | return new Response( 694 | JSON.stringify({ 695 | foo: 'bar' 696 | }), 697 | { 698 | status: 500, 699 | headers: { 700 | 'Content-Type': 'application/json' 701 | } 702 | } 703 | ); 704 | }; 705 | 706 | const action = { 707 | [RSAA]: { 708 | endpoint: 'http://127.0.0.1/api/users/1', 709 | method: 'GET', 710 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 711 | fetch: myFetch 712 | } 713 | }; 714 | 715 | await doTestMiddleware({ 716 | action 717 | }); 718 | }); 719 | 720 | it('must use payload property of request type descriptor when it is a function', async () => { 721 | const payload = jest.fn(); 722 | payload.mockReturnValue('requestPayload'); 723 | 724 | const action = { 725 | [RSAA]: { 726 | endpoint: 'http://127.0.0.1/api/users/1', 727 | method: 'GET', 728 | types: [ 729 | { 730 | type: 'REQUEST', 731 | meta: 'requestMeta', 732 | payload 733 | }, 734 | 'SUCCESS', 735 | 'FAILURE' 736 | ] 737 | } 738 | }; 739 | 740 | await doTestMiddleware({ 741 | action, 742 | response: { 743 | body: JSON.stringify({ data: '12345' }), 744 | status: 200, 745 | headers: { 746 | 'Content-Type': 'application/json' 747 | } 748 | } 749 | }); 750 | 751 | expect(payload).toMatchSnapshot({}, 'payload()'); 752 | }); 753 | 754 | it('must use meta property of request type descriptor when it is a function', async () => { 755 | const meta = jest.fn(); 756 | meta.mockReturnValue('requestMeta'); 757 | 758 | const action = { 759 | [RSAA]: { 760 | endpoint: 'http://127.0.0.1/api/users/1', 761 | method: 'GET', 762 | types: [ 763 | { 764 | type: 'REQUEST', 765 | meta, 766 | payload: 'requestPayload' 767 | }, 768 | 'SUCCESS', 769 | 'FAILURE' 770 | ] 771 | } 772 | }; 773 | 774 | await doTestMiddleware({ 775 | action, 776 | response: { 777 | body: JSON.stringify({ data: '12345' }), 778 | status: 200, 779 | headers: { 780 | 'Content-Type': 'application/json' 781 | } 782 | } 783 | }); 784 | 785 | expect(meta).toMatchSnapshot({}, 'meta()'); 786 | }); 787 | 788 | it('must dispatch a success FSA on a successful API call with a non-empty JSON response', async () => { 789 | const action = { 790 | [RSAA]: { 791 | endpoint: 'http://127.0.0.1/api/users/1', 792 | method: 'GET', 793 | types: [ 794 | { 795 | type: 'REQUEST', 796 | payload: 'requestPayload', 797 | meta: 'requestMeta' 798 | }, 799 | { 800 | type: 'SUCCESS', 801 | meta: 'successMeta' 802 | }, 803 | 'FAILURE' 804 | ] 805 | } 806 | }; 807 | 808 | await doTestMiddleware({ 809 | action, 810 | response: { 811 | body: JSON.stringify({ username: 'Alice' }), 812 | status: 200, 813 | headers: { 814 | 'Content-Type': 'application/json' 815 | } 816 | } 817 | }); 818 | }); 819 | 820 | it('must dispatch a success FSA on a successful API call with an empty JSON response', async () => { 821 | const action = { 822 | [RSAA]: { 823 | endpoint: 'http://127.0.0.1/api/users/1', 824 | method: 'GET', 825 | types: [ 826 | { 827 | type: 'REQUEST', 828 | payload: 'requestPayload', 829 | meta: 'requestMeta' 830 | }, 831 | { 832 | type: 'SUCCESS', 833 | meta: 'successMeta' 834 | }, 835 | 'FAILURE' 836 | ] 837 | } 838 | }; 839 | 840 | await doTestMiddleware({ 841 | action, 842 | response: { 843 | body: JSON.stringify({}), 844 | status: 200, 845 | headers: { 846 | 'Content-Type': 'application/json' 847 | } 848 | } 849 | }); 850 | }); 851 | 852 | it('must dispatch a success FSA with an error state on a successful API call with an invalid JSON response', async () => { 853 | const action = { 854 | [RSAA]: { 855 | endpoint: 'http://127.0.0.1/api/users/1', 856 | method: 'GET', 857 | types: [ 858 | { 859 | type: 'REQUEST', 860 | payload: 'requestPayload', 861 | meta: 'requestMeta' 862 | }, 863 | { 864 | type: 'SUCCESS', 865 | meta: 'successMeta', 866 | payload: () => { 867 | throw new InternalError( 868 | 'Expected error - simulating invalid JSON' 869 | ); 870 | } 871 | }, 872 | 'FAILURE' 873 | ] 874 | } 875 | }; 876 | 877 | await doTestMiddleware({ 878 | action, 879 | response: { 880 | body: '', 881 | status: 200, 882 | headers: { 883 | 'Content-Type': 'application/json' 884 | } 885 | } 886 | }); 887 | }); 888 | 889 | it('must dispatch a success FSA on a successful API call with a non-JSON response', async () => { 890 | const action = { 891 | [RSAA]: { 892 | endpoint: 'http://127.0.0.1/api/users/1', 893 | method: 'GET', 894 | types: [ 895 | { 896 | type: 'REQUEST', 897 | payload: 'requestPayload', 898 | meta: 'requestMeta' 899 | }, 900 | { 901 | type: 'SUCCESS', 902 | meta: 'successMeta' 903 | }, 904 | 'FAILURE' 905 | ] 906 | } 907 | }; 908 | 909 | await doTestMiddleware({ 910 | action, 911 | response: { 912 | body: null, 913 | status: 200 914 | } 915 | }); 916 | }); 917 | 918 | it('must dispatch a failure FSA on an unsuccessful API call with a non-empty JSON response', async () => { 919 | const action = { 920 | [RSAA]: { 921 | endpoint: 'http://127.0.0.1/api/users/1', 922 | method: 'GET', 923 | types: [ 924 | { 925 | type: 'REQUEST', 926 | payload: 'requestPayload', 927 | meta: 'requestMeta' 928 | }, 929 | 'SUCCESS', 930 | { 931 | type: 'FAILURE', 932 | meta: 'failureMeta' 933 | } 934 | ] 935 | } 936 | }; 937 | 938 | await doTestMiddleware({ 939 | action, 940 | response: { 941 | body: JSON.stringify({ error: 'Resource not found' }), 942 | status: 404, 943 | headers: { 944 | 'Content-Type': 'application/json' 945 | } 946 | } 947 | }); 948 | }); 949 | 950 | it('must dispatch a failure FSA on an unsuccessful API call with an empty JSON response', async () => { 951 | const action = { 952 | [RSAA]: { 953 | endpoint: 'http://127.0.0.1/api/users/1', 954 | method: 'GET', 955 | types: [ 956 | { 957 | type: 'REQUEST', 958 | payload: 'requestPayload', 959 | meta: 'requestMeta' 960 | }, 961 | 'SUCCESS', 962 | { 963 | type: 'FAILURE', 964 | meta: 'failureMeta' 965 | } 966 | ] 967 | } 968 | }; 969 | 970 | await doTestMiddleware({ 971 | action, 972 | response: { 973 | body: JSON.stringify({}), 974 | status: 404, 975 | headers: { 976 | 'Content-Type': 'application/json' 977 | } 978 | } 979 | }); 980 | }); 981 | 982 | it('must dispatch a failure FSA on an unsuccessful API call with a non-JSON response', async () => { 983 | const action = { 984 | [RSAA]: { 985 | endpoint: 'http://127.0.0.1/api/users/1', 986 | method: 'GET', 987 | types: [ 988 | { 989 | type: 'REQUEST', 990 | payload: 'requestPayload', 991 | meta: 'requestMeta' 992 | }, 993 | 'SUCCESS', 994 | { 995 | type: 'FAILURE', 996 | meta: 'failureMeta' 997 | } 998 | ] 999 | } 1000 | }; 1001 | 1002 | await doTestMiddleware({ 1003 | action, 1004 | response: { 1005 | body: '', 1006 | status: 404 1007 | } 1008 | }); 1009 | }); 1010 | }); 1011 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | import { InternalError, ApiError } from './errors'; 2 | import RSAA from './RSAA'; 3 | 4 | /** 5 | * Extract JSON body from a server response 6 | * 7 | * @function getJSON 8 | * @access public 9 | * @param {object} res - A raw response object 10 | * @returns {promise|undefined} 11 | */ 12 | async function getJSON(res) { 13 | const contentType = res.headers.get('Content-Type'); 14 | const emptyCodes = [204, 205]; 15 | 16 | if ( 17 | !~emptyCodes.indexOf(res.status) && 18 | contentType && 19 | ~contentType.indexOf('json') 20 | ) { 21 | return await res.json(); 22 | } else { 23 | return await Promise.resolve(); 24 | } 25 | } 26 | 27 | /** 28 | * Blow up string or symbol types into full-fledged type descriptors, 29 | * and add defaults 30 | * 31 | * @function normalizeTypeDescriptors 32 | * @access private 33 | * @param {array} types - The [RSAA].types from a validated RSAA 34 | * @returns {array} 35 | */ 36 | function normalizeTypeDescriptors(types) { 37 | let [requestType, successType, failureType] = types; 38 | 39 | if (typeof requestType === 'string' || typeof requestType === 'symbol') { 40 | requestType = { type: requestType }; 41 | } 42 | 43 | if (typeof successType === 'string' || typeof successType === 'symbol') { 44 | successType = { type: successType }; 45 | } 46 | successType = { 47 | payload: (action, state, res) => getJSON(res), 48 | ...successType 49 | }; 50 | 51 | if (typeof failureType === 'string' || typeof failureType === 'symbol') { 52 | failureType = { type: failureType }; 53 | } 54 | failureType = { 55 | payload: (action, state, res) => 56 | getJSON(res).then(json => new ApiError(res.status, res.statusText, json)), 57 | ...failureType 58 | }; 59 | 60 | return [requestType, successType, failureType]; 61 | } 62 | 63 | /** 64 | * Evaluate a type descriptor to an FSA 65 | * 66 | * @function actionWith 67 | * @access private 68 | * @param {object} descriptor - A type descriptor 69 | * @param {array} args - The array of arguments for `payload` and `meta` function properties 70 | * @returns {object} 71 | */ 72 | async function actionWith(descriptor, args = []) { 73 | try { 74 | descriptor.payload = 75 | typeof descriptor.payload === 'function' 76 | ? await descriptor.payload(...args) 77 | : descriptor.payload; 78 | } catch (e) { 79 | descriptor.payload = new InternalError(e.message); 80 | descriptor.error = true; 81 | } 82 | 83 | try { 84 | descriptor.meta = 85 | typeof descriptor.meta === 'function' 86 | ? await descriptor.meta(...args) 87 | : descriptor.meta; 88 | } catch (e) { 89 | delete descriptor.meta; 90 | descriptor.payload = new InternalError(e.message); 91 | descriptor.error = true; 92 | } 93 | 94 | return descriptor; 95 | } 96 | 97 | /** 98 | * Create RSAA action 99 | * 100 | * @function createAction 101 | * @access public 102 | * @param {object} clientCall - The options for the RSAA action 103 | * @returns {object} RSAA Action 104 | */ 105 | function createAction(clientCall) { 106 | return { [RSAA]: clientCall }; 107 | } 108 | 109 | export { getJSON, normalizeTypeDescriptors, actionWith, createAction }; 110 | -------------------------------------------------------------------------------- /src/util.test.js: -------------------------------------------------------------------------------- 1 | // Public package exports 2 | import { RSAA, getJSON } from './index.js'; 3 | 4 | // Private package import 5 | import { normalizeTypeDescriptors, actionWith, createAction } from './util'; 6 | 7 | describe('#normalizeTypeDescriptors', () => { 8 | it('handles string types', () => { 9 | const types = ['REQUEST', 'SUCCESS', 'FAILURE']; 10 | const descriptors = normalizeTypeDescriptors(types); 11 | expect(descriptors).toMatchSnapshot(); 12 | }); 13 | 14 | it('handles object types', () => { 15 | const types = [ 16 | { 17 | type: 'REQUEST', 18 | payload: 'requestPayload', 19 | meta: 'requestMeta' 20 | }, 21 | { 22 | type: 'SUCCESS', 23 | payload: 'successPayload', 24 | meta: 'successMeta' 25 | }, 26 | { 27 | type: 'FAILURE', 28 | payload: 'failurePayload', 29 | meta: 'failureMeta' 30 | } 31 | ]; 32 | const descriptors = normalizeTypeDescriptors(types); 33 | expect(descriptors).toMatchSnapshot(); 34 | }); 35 | }); 36 | 37 | describe('#actionWith', () => { 38 | it('handles string payload and meta descriptor properties', async () => { 39 | const fsa = await actionWith({ 40 | type: 'REQUEST', 41 | payload: 'somePayload', 42 | meta: 'someMeta', 43 | error: true 44 | }); 45 | 46 | expect(fsa).toMatchSnapshot(); 47 | }); 48 | 49 | it('handles function payload and meta descriptor properties', async () => { 50 | const fsa = await actionWith({ 51 | type: 'REQUEST', 52 | payload: () => 'somePayloadFromFn', 53 | meta: () => 'someMetaFromFn' 54 | }); 55 | expect(fsa).toMatchSnapshot(); 56 | }); 57 | 58 | it('passes function payload and meta descriptor properties arguments', async () => { 59 | const payload = jest.fn(); 60 | payload.mockReturnValue('somePayloadFromMock'); 61 | const meta = jest.fn(); 62 | meta.mockReturnValue('someMetaFromMock'); 63 | 64 | const passedArgs = ['action', 'state', 'res']; 65 | const fsa = await actionWith( 66 | { 67 | type: 'REQUEST', 68 | payload, 69 | meta 70 | }, 71 | passedArgs 72 | ); 73 | 74 | expect(payload).toHaveBeenCalledWith(...passedArgs); 75 | expect(meta).toHaveBeenCalledWith(...passedArgs); 76 | }); 77 | 78 | it('handles an error in the payload function', async () => { 79 | const fsa = await actionWith({ 80 | type: 'REQUEST', 81 | payload: () => { 82 | throw new Error('test error in payload function'); 83 | } 84 | }); 85 | 86 | expect(fsa).toMatchSnapshot(); 87 | }); 88 | 89 | it('handles an error in the meta function', async () => { 90 | const fsa = await actionWith({ 91 | type: 'REQUEST', 92 | meta: () => { 93 | throw new Error('test error in meta function'); 94 | } 95 | }); 96 | 97 | expect(fsa).toMatchSnapshot(); 98 | }); 99 | 100 | it('handles a synchronous payload function', async () => { 101 | const fsa = await actionWith({ 102 | type: 'REQUEST', 103 | payload: () => 'somePayload' 104 | }); 105 | 106 | expect(fsa).toMatchSnapshot(); 107 | }); 108 | 109 | it('handles an asynchronous payload function', async () => { 110 | const fsa = await actionWith({ 111 | type: 'REQUEST', 112 | payload: new Promise(resolve => 113 | setTimeout(() => resolve('somePayloadAsync'), 250) 114 | ) 115 | }); 116 | 117 | expect(fsa).toMatchSnapshot(); 118 | }); 119 | 120 | it('handles a synchronous meta function', async () => { 121 | const fsa = await actionWith({ 122 | type: 'REQUEST', 123 | meta: () => 'someMeta' 124 | }); 125 | 126 | expect(fsa).toMatchSnapshot(); 127 | }); 128 | 129 | it('handles an asynchronous meta function', async () => { 130 | const fsa = await actionWith({ 131 | type: 'REQUEST', 132 | meta: new Promise(resolve => 133 | setTimeout(() => resolve('someMetaAsync'), 250) 134 | ) 135 | }); 136 | 137 | expect(fsa).toMatchSnapshot(); 138 | }); 139 | }); 140 | 141 | describe('#getJSON', () => { 142 | it("returns the JSON body of a response with a JSONy 'Content-Type' header", async () => { 143 | const res = { 144 | headers: { 145 | get(name) { 146 | return name === 'Content-Type' ? 'application/json' : undefined; 147 | } 148 | }, 149 | json() { 150 | return Promise.resolve({ message: 'ok' }); 151 | } 152 | }; 153 | 154 | const result = await getJSON(res); 155 | expect(result).toMatchSnapshot(); 156 | }); 157 | 158 | it("returns a resolved promise for a response with a not-JSONy 'Content-Type' header", async () => { 159 | const res = { 160 | headers: { 161 | get(name) { 162 | return name === 'Content-Type' ? 'not it' : undefined; 163 | } 164 | } 165 | }; 166 | const result = await getJSON(res); 167 | expect(result).toBeUndefined(); 168 | }); 169 | }); 170 | 171 | describe('#createAction', () => { 172 | it('returns valid RSAA action', () => { 173 | const apiCall = {}; 174 | expect(createAction(apiCall)).toHaveProperty(RSAA, apiCall); 175 | }); 176 | }); 177 | -------------------------------------------------------------------------------- /src/validation.js: -------------------------------------------------------------------------------- 1 | import RSAA from './RSAA'; 2 | 3 | /** 4 | * Is the argument a plain object? 5 | * Inspired by lodash.isplainobject 6 | * 7 | * @function isPlainObject 8 | * @param {object} obj - The object to check 9 | * @returns {boolean} 10 | */ 11 | function isPlainObject(obj) { 12 | return ( 13 | obj && 14 | typeof obj == 'object' && 15 | Object.getPrototypeOf(obj) === Object.prototype 16 | ); 17 | } 18 | 19 | /** 20 | * Is the given action a plain JavaScript object with an [RSAA] property? 21 | * 22 | * @function isRSAA 23 | * @access public 24 | * @param {object} action - The action to check 25 | * @returns {boolean} 26 | */ 27 | function isRSAA(action) { 28 | return isPlainObject(action) && action.hasOwnProperty(RSAA); 29 | } 30 | 31 | /** 32 | * Is the given object a valid type descriptor? 33 | * 34 | * @function isValidTypeDescriptor 35 | * @access private 36 | * @param {object} obj - The object to check agains the type descriptor definition 37 | * @returns {boolean} 38 | */ 39 | function isValidTypeDescriptor(obj) { 40 | const validKeys = ['type', 'payload', 'meta']; 41 | 42 | if (!isPlainObject(obj)) { 43 | return false; 44 | } 45 | for (let key in obj) { 46 | if (!~validKeys.indexOf(key)) { 47 | return false; 48 | } 49 | } 50 | if (!('type' in obj)) { 51 | return false; 52 | } else if (typeof obj.type !== 'string' && typeof obj.type !== 'symbol') { 53 | return false; 54 | } 55 | 56 | return true; 57 | } 58 | 59 | /** 60 | * Checks an action against the RSAA definition, returning a (possibly empty) 61 | * array of validation errors. 62 | * 63 | * @function validateRSAA 64 | * @access public 65 | * @param {object} action - The action to check against the RSAA definition 66 | * @returns {array} 67 | */ 68 | function validateRSAA(action) { 69 | var validationErrors = []; 70 | const validCallAPIKeys = [ 71 | 'endpoint', 72 | 'options', 73 | 'method', 74 | 'body', 75 | 'headers', 76 | 'credentials', 77 | 'bailout', 78 | 'types', 79 | 'fetch', 80 | 'ok' 81 | ]; 82 | const validMethods = [ 83 | 'GET', 84 | 'HEAD', 85 | 'POST', 86 | 'PUT', 87 | 'PATCH', 88 | 'DELETE', 89 | 'OPTIONS' 90 | ]; 91 | const validCredentials = ['omit', 'same-origin', 'include']; 92 | 93 | if (!isRSAA(action)) { 94 | validationErrors.push( 95 | 'RSAAs must be plain JavaScript objects with an [RSAA] property' 96 | ); 97 | return validationErrors; 98 | } 99 | 100 | const callAPI = action[RSAA]; 101 | if (!isPlainObject(callAPI)) { 102 | validationErrors.push('[RSAA] property must be a plain JavaScript object'); 103 | } 104 | for (let key in callAPI) { 105 | if (!~validCallAPIKeys.indexOf(key)) { 106 | validationErrors.push(`Invalid [RSAA] key: ${key}`); 107 | } 108 | } 109 | 110 | const { 111 | endpoint, 112 | method, 113 | headers, 114 | options, 115 | credentials, 116 | types, 117 | bailout, 118 | fetch, 119 | ok 120 | } = callAPI; 121 | if (typeof endpoint === 'undefined') { 122 | validationErrors.push('[RSAA] must have an endpoint property'); 123 | } else if (typeof endpoint !== 'string' && typeof endpoint !== 'function') { 124 | validationErrors.push( 125 | '[RSAA].endpoint property must be a string or a function' 126 | ); 127 | } 128 | if (typeof method === 'undefined') { 129 | validationErrors.push('[RSAA] must have a method property'); 130 | } else if (typeof method !== 'string') { 131 | validationErrors.push('[RSAA].method property must be a string'); 132 | } else if (!~validMethods.indexOf(method.toUpperCase())) { 133 | validationErrors.push(`Invalid [RSAA].method: ${method.toUpperCase()}`); 134 | } 135 | 136 | if ( 137 | typeof headers !== 'undefined' && 138 | !isPlainObject(headers) && 139 | typeof headers !== 'function' 140 | ) { 141 | validationErrors.push( 142 | '[RSAA].headers property must be undefined, a plain JavaScript object, or a function' 143 | ); 144 | } 145 | if ( 146 | typeof options !== 'undefined' && 147 | !isPlainObject(options) && 148 | typeof options !== 'function' 149 | ) { 150 | validationErrors.push( 151 | '[RSAA].options property must be undefined, a plain JavaScript object, or a function' 152 | ); 153 | } 154 | if (typeof credentials !== 'undefined') { 155 | if (typeof credentials !== 'string') { 156 | validationErrors.push( 157 | '[RSAA].credentials property must be undefined, or a string' 158 | ); 159 | } else if (!~validCredentials.indexOf(credentials)) { 160 | validationErrors.push(`Invalid [RSAA].credentials: ${credentials}`); 161 | } 162 | } 163 | if ( 164 | typeof bailout !== 'undefined' && 165 | typeof bailout !== 'boolean' && 166 | typeof bailout !== 'function' 167 | ) { 168 | validationErrors.push( 169 | '[RSAA].bailout property must be undefined, a boolean, or a function' 170 | ); 171 | } 172 | 173 | if (typeof types === 'undefined') { 174 | validationErrors.push('[RSAA] must have a types property'); 175 | } else if (!Array.isArray(types) || types.length !== 3) { 176 | validationErrors.push('[RSAA].types property must be an array of length 3'); 177 | } else { 178 | const [requestType, successType, failureType] = types; 179 | if ( 180 | typeof requestType !== 'string' && 181 | typeof requestType !== 'symbol' && 182 | !isValidTypeDescriptor(requestType) 183 | ) { 184 | validationErrors.push('Invalid request type'); 185 | } 186 | if ( 187 | typeof successType !== 'string' && 188 | typeof successType !== 'symbol' && 189 | !isValidTypeDescriptor(successType) 190 | ) { 191 | validationErrors.push('Invalid success type'); 192 | } 193 | if ( 194 | typeof failureType !== 'string' && 195 | typeof failureType !== 'symbol' && 196 | !isValidTypeDescriptor(failureType) 197 | ) { 198 | validationErrors.push('Invalid failure type'); 199 | } 200 | } 201 | 202 | if (typeof fetch !== 'undefined') { 203 | if (typeof fetch !== 'function') { 204 | validationErrors.push('[RSAA].fetch property must be a function'); 205 | } 206 | } 207 | 208 | if (typeof ok !== 'undefined') { 209 | if (typeof ok !== 'function') { 210 | validationErrors.push('[RSAA].ok property must be a function'); 211 | } 212 | } 213 | 214 | return validationErrors; 215 | } 216 | 217 | /** 218 | * Is the given action a valid RSAA? 219 | * 220 | * @function isValidRSAA 221 | * @access public 222 | * @param {object} action - The action to check against the RSAA definition 223 | * @returns {boolean} 224 | */ 225 | function isValidRSAA(action) { 226 | return !validateRSAA(action).length; 227 | } 228 | 229 | export { isRSAA, isValidTypeDescriptor, validateRSAA, isValidRSAA }; 230 | -------------------------------------------------------------------------------- /src/validation.test.js: -------------------------------------------------------------------------------- 1 | // Public package exports 2 | import { RSAA, isRSAA, validateRSAA, isValidRSAA } from 'redux-api-middleware'; 3 | 4 | // Private package import 5 | import { isValidTypeDescriptor } from './validation'; 6 | 7 | describe('#isValidTypeDescriptor', () => { 8 | it('must be a plain JavaScript object', () => { 9 | var descriptor = ''; 10 | expect(isValidTypeDescriptor(descriptor)).toBeFalsy(); 11 | }); 12 | 13 | it('must not have properties other than type, payload and meta', () => { 14 | var descriptor = { 15 | type: '', 16 | invalidKey: '' 17 | }; 18 | expect(isValidTypeDescriptor(descriptor)).toBeFalsy(); 19 | }); 20 | 21 | it('must have a type property', () => { 22 | var descriptor = {}; 23 | expect(isValidTypeDescriptor(descriptor)).toBeFalsy(); 24 | }); 25 | 26 | it('must not have a type property that is not a string or a symbol', () => { 27 | var descriptor = { 28 | type: {} 29 | }; 30 | expect(isValidTypeDescriptor(descriptor)).toBeFalsy(); 31 | }); 32 | 33 | it('may have a type property that is a string', () => { 34 | var descriptor = { 35 | type: '' 36 | }; 37 | expect(isValidTypeDescriptor(descriptor)).toBeTruthy(); 38 | }); 39 | 40 | it('may have a type property that is a symbol', () => { 41 | var descriptor = { 42 | type: Symbol() 43 | }; 44 | expect(isValidTypeDescriptor(descriptor)).toBeTruthy(); 45 | }); 46 | }); 47 | 48 | describe('#isRSAA', () => { 49 | it('RSAAs must be plain JavaScript objects', () => { 50 | expect(isRSAA('')).toBeFalsy(); 51 | }); 52 | 53 | it('RSAAs must have an [RSAA] property', () => { 54 | expect(isRSAA({})).toBeFalsy(); 55 | }); 56 | 57 | it('returns true for an RSAA', () => { 58 | expect(isRSAA({ [RSAA]: {} })).toBeTruthy(); 59 | }); 60 | }); 61 | 62 | describe('#validateRSAA / #isValidRSAA', () => { 63 | it('handles invalid actions', () => { 64 | expect(isValidRSAA('')).toBeFalsy(); 65 | expect(validateRSAA('')).toMatchSnapshot(); 66 | }); 67 | 68 | it('handles invalid RSAA value (string)', () => { 69 | const action = { 70 | [RSAA]: '' 71 | }; 72 | 73 | expect(isValidRSAA(action)).toBeFalsy(); 74 | expect(validateRSAA(action)).toMatchSnapshot(); 75 | }); 76 | 77 | it('handles invalid RSAA value (invalid object)', () => { 78 | const action = { 79 | [RSAA]: { invalidKey: '' } 80 | }; 81 | 82 | expect(isValidRSAA(action)).toBeFalsy(); 83 | expect(validateRSAA(action)).toMatchSnapshot(); 84 | }); 85 | 86 | it('handles missing RSAA properties', () => { 87 | const action = { 88 | [RSAA]: {} 89 | }; 90 | 91 | expect(isValidRSAA(action)).toBeFalsy(); 92 | expect(validateRSAA(action)).toMatchSnapshot(); 93 | }); 94 | 95 | it('handles invalid [RSAA].endpoint property', () => { 96 | const action = { 97 | [RSAA]: { 98 | endpoint: {}, 99 | method: 'GET', 100 | types: ['REQUEST', 'SUCCESS', 'FAILURE'] 101 | } 102 | }; 103 | 104 | expect(isValidRSAA(action)).toBeFalsy(); 105 | expect(validateRSAA(action)).toMatchSnapshot(); 106 | }); 107 | 108 | it('handles invalid [RSAA].method property (object)', () => { 109 | const action = { 110 | [RSAA]: { 111 | endpoint: '', 112 | method: {}, 113 | types: ['REQUEST', 'SUCCESS', 'FAILURE'] 114 | } 115 | }; 116 | 117 | expect(isValidRSAA(action)).toBeFalsy(); 118 | expect(validateRSAA(action)).toMatchSnapshot(); 119 | }); 120 | 121 | it('handles invalid [RSAA].method property (invalid string)', () => { 122 | const action = { 123 | [RSAA]: { 124 | endpoint: '', 125 | method: 'INVALID_METHOD', 126 | types: ['REQUEST', 'SUCCESS', 'FAILURE'] 127 | } 128 | }; 129 | 130 | expect(isValidRSAA(action)).toBeFalsy(); 131 | expect(validateRSAA(action)).toMatchSnapshot(); 132 | }); 133 | 134 | it('handles invalid [RSAA].headers property', () => { 135 | const action = { 136 | [RSAA]: { 137 | endpoint: '', 138 | method: 'GET', 139 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 140 | headers: '' 141 | } 142 | }; 143 | 144 | expect(isValidRSAA(action)).toBeFalsy(); 145 | expect(validateRSAA(action)).toMatchSnapshot(); 146 | }); 147 | 148 | it('handles invalid [RSAA].credentials property (object)', () => { 149 | const action = { 150 | [RSAA]: { 151 | endpoint: '', 152 | method: 'GET', 153 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 154 | credentials: {} 155 | } 156 | }; 157 | 158 | expect(isValidRSAA(action)).toBeFalsy(); 159 | expect(validateRSAA(action)).toMatchSnapshot(); 160 | }); 161 | 162 | it('handles invalid [RSAA].credentials property (invalid string)', () => { 163 | const action = { 164 | [RSAA]: { 165 | endpoint: '', 166 | method: 'GET', 167 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 168 | credentials: 'InvalidCredentials' 169 | } 170 | }; 171 | 172 | expect(isValidRSAA(action)).toBeFalsy(); 173 | expect(validateRSAA(action)).toMatchSnapshot(); 174 | }); 175 | 176 | it('handles invalid [RSAA].bailout property', () => { 177 | const action = { 178 | [RSAA]: { 179 | endpoint: '', 180 | method: 'GET', 181 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 182 | bailout: '' 183 | } 184 | }; 185 | 186 | expect(isValidRSAA(action)).toBeFalsy(); 187 | expect(validateRSAA(action)).toMatchSnapshot(); 188 | }); 189 | 190 | it('handles invalid [RSAA].types property (object)', () => { 191 | const action = { 192 | [RSAA]: { 193 | endpoint: '', 194 | method: 'GET', 195 | types: {} 196 | } 197 | }; 198 | 199 | expect(isValidRSAA(action)).toBeFalsy(); 200 | expect(validateRSAA(action)).toMatchSnapshot(); 201 | }); 202 | 203 | it('handles invalid [RSAA].types property (wrong length)', () => { 204 | const action = { 205 | [RSAA]: { 206 | endpoint: '', 207 | method: 'GET', 208 | types: ['a', 'b'] 209 | } 210 | }; 211 | 212 | expect(isValidRSAA(action)).toBeFalsy(); 213 | expect(validateRSAA(action)).toMatchSnapshot(); 214 | }); 215 | 216 | it('handles invalid [RSAA].types property (invalid objects)', () => { 217 | const action = { 218 | [RSAA]: { 219 | endpoint: '', 220 | method: 'GET', 221 | types: [{}, {}, {}] 222 | } 223 | }; 224 | 225 | expect(isValidRSAA(action)).toBeFalsy(); 226 | expect(validateRSAA(action)).toMatchSnapshot(); 227 | }); 228 | 229 | it('handles invalid [RSAA].options property', () => { 230 | const action = { 231 | [RSAA]: { 232 | endpoint: '', 233 | method: 'GET', 234 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 235 | options: '' 236 | } 237 | }; 238 | 239 | expect(isValidRSAA(action)).toBeFalsy(); 240 | expect(validateRSAA(action)).toMatchSnapshot(); 241 | }); 242 | 243 | it('handles invalid [RSAA].fetch property', () => { 244 | const action = { 245 | [RSAA]: { 246 | endpoint: '', 247 | method: 'GET', 248 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 249 | fetch: {} 250 | } 251 | }; 252 | 253 | expect(isValidRSAA(action)).toBeFalsy(); 254 | expect(validateRSAA(action)).toMatchSnapshot(); 255 | }); 256 | 257 | it('handles invalid [RSAA].ok property', () => { 258 | const action = { 259 | [RSAA]: { 260 | endpoint: '', 261 | method: 'GET', 262 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 263 | ok: {} 264 | } 265 | }; 266 | 267 | expect(isValidRSAA(action)).toBeFalsy(); 268 | expect(validateRSAA(action)).toMatchSnapshot(); 269 | }); 270 | 271 | it('handles valid RSAA with endpoint string', () => { 272 | const action = { 273 | [RSAA]: { 274 | endpoint: '', 275 | method: 'GET', 276 | types: ['REQUEST', 'SUCCESS', 'FAILURE'] 277 | } 278 | }; 279 | 280 | expect(isValidRSAA(action)).toBeTruthy(); 281 | expect(validateRSAA(action)).toMatchSnapshot(); 282 | }); 283 | 284 | it('handles valid RSAA with endpoint function', () => { 285 | const action = { 286 | [RSAA]: { 287 | endpoint: () => '', 288 | method: 'GET', 289 | types: ['REQUEST', 'SUCCESS', 'FAILURE'] 290 | } 291 | }; 292 | 293 | expect(isValidRSAA(action)).toBeTruthy(); 294 | expect(validateRSAA(action)).toMatchSnapshot(); 295 | }); 296 | 297 | it('handles valid RSAA with headers object', () => { 298 | const action = { 299 | [RSAA]: { 300 | endpoint: '', 301 | method: 'GET', 302 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 303 | headers: {} 304 | } 305 | }; 306 | 307 | expect(isValidRSAA(action)).toBeTruthy(); 308 | expect(validateRSAA(action)).toMatchSnapshot(); 309 | }); 310 | 311 | it('handles valid RSAA with headers function', () => { 312 | const action = { 313 | [RSAA]: { 314 | endpoint: '', 315 | method: 'GET', 316 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 317 | headers: () => ({}) 318 | } 319 | }; 320 | 321 | expect(isValidRSAA(action)).toBeTruthy(); 322 | expect(validateRSAA(action)).toMatchSnapshot(); 323 | }); 324 | 325 | it('handles valid RSAA with bailout boolean', () => { 326 | const action = { 327 | [RSAA]: { 328 | endpoint: '', 329 | method: 'GET', 330 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 331 | bailout: false 332 | } 333 | }; 334 | 335 | expect(isValidRSAA(action)).toBeTruthy(); 336 | expect(validateRSAA(action)).toMatchSnapshot(); 337 | }); 338 | 339 | it('handles valid RSAA with bailout function', () => { 340 | const action = { 341 | [RSAA]: { 342 | endpoint: '', 343 | method: 'GET', 344 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 345 | bailout: () => false 346 | } 347 | }; 348 | 349 | expect(isValidRSAA(action)).toBeTruthy(); 350 | expect(validateRSAA(action)).toMatchSnapshot(); 351 | }); 352 | 353 | it('handles valid RSAA with types of symbols', () => { 354 | const action = { 355 | [RSAA]: { 356 | endpoint: '', 357 | method: 'GET', 358 | types: [Symbol(), Symbol(), Symbol()] 359 | } 360 | }; 361 | 362 | expect(isValidRSAA(action)).toBeTruthy(); 363 | expect(validateRSAA(action)).toMatchSnapshot(); 364 | }); 365 | 366 | it('handles valid RSAA with types of type descriptors', () => { 367 | const action = { 368 | [RSAA]: { 369 | endpoint: '', 370 | method: 'GET', 371 | types: [ 372 | { 373 | type: 'REQUEST', 374 | payload: 'requestPayload', 375 | meta: 'requestMeta' 376 | }, 377 | { 378 | type: 'SUCCESS', 379 | payload: 'successPayload', 380 | meta: 'successMeta' 381 | }, 382 | { 383 | type: 'FAILURE', 384 | payload: 'failurePayload', 385 | meta: 'failureMeta' 386 | } 387 | ] 388 | } 389 | }; 390 | 391 | expect(isValidRSAA(action)).toBeTruthy(); 392 | expect(validateRSAA(action)).toMatchSnapshot(); 393 | }); 394 | 395 | it('handles valid RSAA with options object', () => { 396 | const action = { 397 | [RSAA]: { 398 | endpoint: '', 399 | method: 'GET', 400 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 401 | options: {} 402 | } 403 | }; 404 | 405 | expect(isValidRSAA(action)).toBeTruthy(); 406 | expect(validateRSAA(action)).toMatchSnapshot(); 407 | }); 408 | 409 | it('handles valid RSAA with options function', () => { 410 | const action = { 411 | [RSAA]: { 412 | endpoint: '', 413 | method: 'GET', 414 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 415 | options: () => ({}) 416 | } 417 | }; 418 | 419 | expect(isValidRSAA(action)).toBeTruthy(); 420 | expect(validateRSAA(action)).toMatchSnapshot(); 421 | }); 422 | 423 | it('handles valid RSAA with fetch function', () => { 424 | const action = { 425 | [RSAA]: { 426 | endpoint: '', 427 | method: 'GET', 428 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 429 | fetch: () => {} 430 | } 431 | }; 432 | 433 | expect(isValidRSAA(action)).toBeTruthy(); 434 | expect(validateRSAA(action)).toMatchSnapshot(); 435 | }); 436 | 437 | it('handles top-level string properties other than RSAA', () => { 438 | const action = { 439 | [RSAA]: { 440 | endpoint: '', 441 | method: 'GET', 442 | types: ['REQUEST', 'SUCCESS', 'FAILURE'] 443 | }, 444 | anotherKey: 'foo' 445 | }; 446 | 447 | expect(isValidRSAA(action)).toBeTruthy(); 448 | expect(validateRSAA(action)).toMatchSnapshot(); 449 | }); 450 | 451 | it('handles top-level symbol properties other than RSAA', () => { 452 | const action = { 453 | [RSAA]: { 454 | endpoint: '', 455 | method: 'GET', 456 | types: ['REQUEST', 'SUCCESS', 'FAILURE'] 457 | }, 458 | [Symbol('action30 Symbol')]: 'foo' 459 | }; 460 | 461 | expect(isValidRSAA(action)).toBeTruthy(); 462 | expect(validateRSAA(action)).toMatchSnapshot(); 463 | }); 464 | }); 465 | -------------------------------------------------------------------------------- /test/setupJest.js: -------------------------------------------------------------------------------- 1 | global.fetch = require('jest-fetch-mock') 2 | --------------------------------------------------------------------------------