Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

next: Adds options parameter #150

Merged
merged 14 commits into from
Sep 21, 2017
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ node_modules
*.log
lib
coverage
.idea
32 changes: 29 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ It must be one of the strings `GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE` or

The body of the API call.

`redux-api-middleware` uses the [`Fetch API`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to make the API call. `[RSAA].body` should hence be a valid body according to the 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.
`redux-api-middleware` uses the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to make the API call. `[RSAA].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.

#### `[RSAA].headers`

Expand All @@ -164,6 +164,25 @@ It is usually an object, with the keys specifying the header names and the value

It may also be a function taking the state of your Redux store as its argument, and returning an object of headers as above.

#### `[RSAA].options`

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.

It is usually an object with the options keys/values. For example, you can specify a network timeout for node.js code
in the following way.

```js
{
[RSAA]: {
...
options: { timeout: 3000 }
...
}
}
```

It may also be a function taking the state of your Redux store as its argument, and returning an object of options as above.

#### `[RSAA].credentials`

Whether or not to send cookies with the API call.
Expand Down Expand Up @@ -540,12 +559,13 @@ The `[RSAA]` property MAY

- have a `body` property,
- have a `headers` property,
- have an `options` property,
- have a `credentials` property,
- have a `bailout` property.

The `[RSAA]` property MUST NOT

- include properties other than `endpoint`, `method`, `types`, `body`, `headers`, `credentials`, and `bailout`.
- include properties other than `endpoint`, `method`, `types`, `body`, `headers`, `options`, `credentials`, and `bailout`.

#### `[RSAA].endpoint`

Expand All @@ -557,12 +577,18 @@ The `[RSAA].method` property MUST be one of the strings `GET`, `HEAD`, `POST`, `

#### `[RSAA].body`

The optional `[RSAA].body` property SHOULD be a valid body according to the the [fetch specification](https://fetch.spec.whatwg.org).
The optional `[RSAA].body` property SHOULD be a valid body according to the [fetch specification](https://fetch.spec.whatwg.org).

#### `[RSAA].headers`

The optional `[RSAA].headers` property MUST be a plain JavaScript object or a function. In the second case, the function SHOULD return a plain JavaScript object.

#### `[RSAA].options`

The optional `[RSAA].options` property MUST be a plain JavaScript object or a function. In the second case, the function SHOULD return a plain JavaScript object.
The options object can contain any options supported by the effective fetch implementation.
See [MDN fetch](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch) or [node-fetch](https://github.com/bitinn/node-fetch#options).

#### `[RSAA].credentials`

The optional `[RSAA].credentials` property MUST be one of the strings `omit`, `same-origin` or `include`.
Expand Down
1 change: 0 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/**
* Redux middleware for calling an API
* @module redux-api-middleware
* @requires isomorphic-fetch
* @requires lodash.isplainobject
* @exports {string} RSAA
* @exports {string} CALL_API - alias of RSAA, to be deprecated in v3
Expand Down
27 changes: 22 additions & 5 deletions src/middleware.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import isPlainObject from 'lodash.isplainobject';

import RSAA from './RSAA';
import { isRSAA, validateRSAA } from './validation';
import { InvalidRSAA, RequestError, ApiError } from './errors';
import { getJSON, normalizeTypeDescriptors, actionWith } from './util';
import { InvalidRSAA, RequestError } from './errors';
import { normalizeTypeDescriptors, actionWith } from './util';

/**
* A Redux middleware that processes RSAA actions.
Expand Down Expand Up @@ -38,7 +36,7 @@ function apiMiddleware({ getState }) {

// Parse the validated RSAA action
const callAPI = action[RSAA];
var { endpoint, headers } = callAPI;
var { endpoint, headers, options = {} } = callAPI;
const { method, body, credentials, bailout, types } = callAPI;
const [requestType, successType, failureType] = normalizeTypeDescriptors(
types
Expand Down Expand Up @@ -101,12 +99,31 @@ function apiMiddleware({ getState }) {
}
}

// Process [RSAA].options function
if (typeof options === 'function') {
try {
options = options(getState());
} catch (e) {
return next(
await actionWith(
{
...requestType,
payload: new RequestError('[RSAA].options function failed'),
error: true
},
[action, getState()]
)
);
}
}

// We can now dispatch the request FSA
next(await actionWith(requestType, [action, getState()]));

try {
// Make the API call
var res = await fetch(endpoint, {
...options,
method,
body,
credentials,
Expand Down
20 changes: 19 additions & 1 deletion src/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ function validateRSAA(action) {
var validationErrors = [];
const validCallAPIKeys = [
'endpoint',
'options',
'method',
'body',
'headers',
Expand Down Expand Up @@ -95,7 +96,15 @@ function validateRSAA(action) {
}
}

const { endpoint, method, headers, credentials, types, bailout } = callAPI;
const {
endpoint,
method,
headers,
options,
credentials,
types,
bailout
} = callAPI;
if (typeof endpoint === 'undefined') {
validationErrors.push('[RSAA] must have an endpoint property');
} else if (typeof endpoint !== 'string' && typeof endpoint !== 'function') {
Expand All @@ -120,6 +129,15 @@ function validateRSAA(action) {
'[RSAA].headers property must be undefined, a plain JavaScript object, or a function'
);
}
if (
typeof options !== 'undefined' &&
!isPlainObject(options) &&
typeof options !== 'function'
) {
validationErrors.push(
'[RSAA].options property must be undefined, a plain JavaScript object, or a function'
);
}
if (typeof credentials !== 'undefined') {
if (typeof credentials !== 'string') {
validationErrors.push(
Expand Down
140 changes: 132 additions & 8 deletions test/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import test from 'tape';
import fetch from 'isomorphic-fetch';
import { Schema, normalize, arrayOf } from 'normalizr';
import 'isomorphic-fetch';
import nock from 'nock';

// Public package exports
Expand Down Expand Up @@ -131,11 +130,11 @@ test('validateRSAA/isValidRSAA must identify conformant RSAAs', t => {
};
t.ok(
validateRSAA(action4).includes('Invalid [RSAA] key: invalidKey'),
'[RSAA] must not have properties other than endpoint, method, types, body, headers, credentials, and bailout (validateRSAA)'
'[RSAA] must not have properties other than endpoint, method, types, body, headers, credentials, options and bailout (validateRSAA)'
);
t.notOk(
isValidRSAA(action4),
'[RSAA] must not have properties other than endpoint, method, types, body, headers, credentials, and bailout (isValidRSAA)'
'[RSAA] must not have properties other than endpoint, method, types, body, headers, credentials, options and bailout (isValidRSAA)'
);

const action5 = {
Expand Down Expand Up @@ -480,6 +479,58 @@ test('validateRSAA/isValidRSAA must identify conformant RSAAs', t => {
'Each element in [RSAA].types may be a type descriptor (isRSAA)'
);

const action24 = {
[RSAA]: {
endpoint: '',
method: 'GET',
types: ['REQUEST', 'SUCCESS', 'FAILURE'],
options: ''
}
};
t.ok(
validateRSAA(action24).includes(
'[RSAA].options property must be undefined, a plain JavaScript object, or a function'
),
'[RSAA].options property must be undefined, a plain JavaScript object, or a function (validateRSAA)'
);
t.notOk(
isValidRSAA(action24),
'[RSAA].options property must be undefined, a plain JavaScript object, or a function (isValidRSAA)'
);

const action25 = {
[RSAA]: {
endpoint: '',
method: 'GET',
types: ['REQUEST', 'SUCCESS', 'FAILURE'],
options: {}
}
};
t.equal(
validateRSAA(action25).length,
0,
'[RSAA].options may be a plain JavaScript object (validateRSAA)'
);
t.ok(
isValidRSAA(action25),
'[RSAA].options may be a plain JavaScript object (isRSAA)'
);

const action26 = {
[RSAA]: {
endpoint: '',
method: 'GET',
types: ['REQUEST', 'SUCCESS', 'FAILURE'],
options: () => {}
}
};
t.equal(
validateRSAA(action26).length,
0,
'[RSAA].options may be a function (validateRSAA)'
);
t.ok(isValidRSAA(action26), '[RSAA].options may be a function (isRSAA)');

t.end();
});

Expand Down Expand Up @@ -992,6 +1043,48 @@ test('apiMiddleware must dispatch an error request FSA when [RSAA].headers fails
actionHandler(anAction);
});

test('apiMiddleware must dispatch an error request FSA when [RSAA].options fails', t => {
const anAction = {
[RSAA]: {
endpoint: '',
method: 'GET',
options: () => {
throw new Error();
},
types: [
{
type: 'REQUEST',
payload: 'ignoredPayload',
meta: 'someMeta'
},
'SUCCESS',
'FAILURE'
]
}
};
const doGetState = () => {};
const nextHandler = apiMiddleware({ getState: doGetState });
const doNext = action => {
t.pass('next handler called');
t.equal(action.type, 'REQUEST', 'dispatched FSA has correct type property');
t.equal(
action.payload.message,
'[RSAA].options function failed',
'dispatched FSA has correct payload property'
);
t.equal(
action.meta,
'someMeta',
'dispatched FSA has correct meta property'
);
t.ok(action.error, 'dispatched FSA has correct error property');
};
const actionHandler = nextHandler(doNext);

t.plan(5);
actionHandler(anAction);
});

test('apiMiddleware must dispatch an error request FSA on a request error', t => {
const anAction = {
[RSAA]: {
Expand Down Expand Up @@ -1109,7 +1202,9 @@ test('apiMiddleware must use an [RSAA].bailout function when present', t => {
});

test('apiMiddleware must use an [RSAA].endpoint function when present', t => {
const api = nock('http://127.0.0.1').get('/api/users/1').reply(200);
const api = nock('http://127.0.0.1')
.get('/api/users/1')
.reply(200);
const anAction = {
[RSAA]: {
endpoint: () => {
Expand All @@ -1130,7 +1225,9 @@ test('apiMiddleware must use an [RSAA].endpoint function when present', t => {
});

test('apiMiddleware must use an [RSAA].headers function when present', t => {
const api = nock('http://127.0.0.1').get('/api/users/1').reply(200);
const api = nock('http://127.0.0.1')
.get('/api/users/1')
.reply(200);
const anAction = {
[RSAA]: {
endpoint: 'http://127.0.0.1/api/users/1',
Expand All @@ -1150,6 +1247,29 @@ test('apiMiddleware must use an [RSAA].headers function when present', t => {
actionHandler(anAction);
});

test('apiMiddleware must use an [RSAA].options function when present', t => {
const api = nock('http://127.0.0.1')
.get('/api/users/1')
.reply(200);
const anAction = {
[RSAA]: {
endpoint: 'http://127.0.0.1/api/users/1',
method: 'GET',
options: () => {
t.pass('[RSAA].options function called');
},
types: ['REQUEST', 'SUCCESS', 'FAILURE']
}
};
const doGetState = () => {};
const nextHandler = apiMiddleware({ getState: doGetState });
const doNext = action => {};
const actionHandler = nextHandler(doNext);

t.plan(1);
actionHandler(anAction);
});

test('apiMiddleware must dispatch a success FSA on a successful API call with a non-empty JSON response', t => {
const api = nock('http://127.0.0.1')
.get('/api/users/1')
Expand Down Expand Up @@ -1343,7 +1463,9 @@ test('apiMiddleware must dispatch a success FSA with an error state on a success
});

test('apiMiddleware must dispatch a success FSA on a successful API call with a non-JSON response', t => {
const api = nock('http://127.0.0.1').get('/api/users/1').reply(200);
const api = nock('http://127.0.0.1')
.get('/api/users/1')
.reply(200);
const anAction = {
[RSAA]: {
endpoint: 'http://127.0.0.1/api/users/1',
Expand Down Expand Up @@ -1531,7 +1653,9 @@ test('apiMiddleware must dispatch a failure FSA on an unsuccessful API call with
});

test('apiMiddleware must dispatch a failure FSA on an unsuccessful API call with a non-JSON response', t => {
const api = nock('http://127.0.0.1').get('/api/users/1').reply(404);
const api = nock('http://127.0.0.1')
.get('/api/users/1')
.reply(404);
const anAction = {
[RSAA]: {
endpoint: 'http://127.0.0.1/api/users/1',
Expand Down