Skip to content

Commit

Permalink
Merge pull request #150 from Aftonbladet/next-options-param
Browse files Browse the repository at this point in the history
next: Adds options parameter
  • Loading branch information
nason committed Sep 21, 2017
2 parents 6d9f37e + 10df9ec commit a54f17a
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 18 deletions.
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

0 comments on commit a54f17a

Please sign in to comment.