Skip to content

Commit

Permalink
feat(BatchMiddleware): add headers, method, mode, cache, `red…
Browse files Browse the repository at this point in the history
…irect` options for fetch
  • Loading branch information
nodkz committed Feb 28, 2018
1 parent ba4287f commit 22e03c7
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 17 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ Middlewares
- `batchTimeout` - integer in milliseconds, period of time for gathering multiple requests before sending them to the server. Will delay sending of the requests on specified in this option period of time, so be careful and keep this value small. (default: `0`)
- `maxBatchSize` - integer representing maximum size of request to be sent in a single batch. Once a request hits the provided size in length a new batch request is ran. Actual for hardcoded limit in 100kb per request in [express-graphql](https://github.com/graphql/express-graphql/blob/master/src/parseBody.js#L112) module. (default: `102400` characters, roughly 100kb for 1-byte characters or 200kb for 2-byte characters)
- `allowMutations` - by default batching disabled for mutations, you may enable it passing `true` (default: `false`)
- `method` - string, for request method type (default: `POST`)
- headers - Object with headers for fetch. Can be Promise or function(req).
- credentials - string, setting for fetch method, eg. 'same-origin' (default: empty).
- also you may provide `mode`, `cache`, `redirect` options for fetch method, for details see [fetch spec](https://fetch.spec.whatwg.org/#requests).
- **loggerMiddleware** - for logging requests and responses.
- `logger` - log function (default: `console.log.bind(console, '[RELAY-NETWORK]')`)
- An example of req/res output in console: <img width="968" alt="screen shot 2017-11-19 at 23 05 19" src="https://user-images.githubusercontent.com/1946920/33159466-557517e0-d03d-11e7-9711-ebdfe6e789c8.png">
Expand Down
19 changes: 12 additions & 7 deletions src/RelayRequestBatch.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,29 @@
import type { FetchOpts, Variables } from './definition';
import type RelayRequest from './RelayRequest';

type BatchFetchOpts = {
credentials?: string,
[name: string]: mixed,
}

export type Requests = RelayRequest[];

export default class RelayRequestBatch {
fetchOpts: $Shape<FetchOpts>;
requests: Requests;

constructor(requests: Requests, options: BatchFetchOpts) {
constructor(requests: Requests) {
this.requests = requests;
this.fetchOpts = {
method: 'POST',
headers: {},
body: this.prepareBody(),
...options
};
}

setFetchOption(name, value: mixed) {
this.fetchOpts[name] = value;
}

setFetchOptions(opts: Object) {
this.fetchOpts = {
...this.fetchOpts,
...opts,
};
}

Expand Down
117 changes: 117 additions & 0 deletions src/middlewares/__tests__/batch-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -442,4 +442,121 @@ describe('middlewares/batch', () => {
expect(singleReqs).toHaveLength(2);
});
});

it('should pass fetch options', async () => {
fetchMock.mock({
matcher: '/graphql/batch',
response: {
status: 200,
body: [{ id: 1, data: {} }, { id: 2, data: {} }],
},
method: 'POST',
});

const rnl = new RelayNetworkLayer([
batchMiddleware({
batchTimeout: 20,
credentials: 'include',
mode: 'cors',
cache: 'no-store',
redirect: 'follow',
}),
]);
const req1 = mockReq(1);
req1.execute(rnl);
const req2 = mockReq(2);
await req2.execute(rnl);

const batchReqs = fetchMock.calls('/graphql/batch');
expect(batchReqs).toHaveLength(1);
expect(fetchMock.lastOptions()).toEqual(
expect.objectContaining({
credentials: 'include',
mode: 'cors',
cache: 'no-store',
redirect: 'follow',
})
);
});

describe('headers option', () => {
it('`headers` option as Object', async () => {
fetchMock.mock({
matcher: '/graphql/batch',
response: {
status: 200,
body: [{ id: 1, data: {} }, { id: 2, data: {} }],
},
method: 'POST',
});
const rnl = new RelayNetworkLayer([
batchMiddleware({
headers: {
'custom-header': '123',
},
}),
]);
const req1 = mockReq(1);
const req2 = mockReq(2);
await Promise.all([req1.execute(rnl), req2.execute(rnl)]);
expect(fetchMock.lastOptions().headers).toEqual(
expect.objectContaining({
'custom-header': '123',
})
);
});

it('`headers` option as thunk', async () => {
fetchMock.mock({
matcher: '/graphql/batch',
response: {
status: 200,
body: [{ id: 1, data: {} }, { id: 2, data: {} }],
},
method: 'POST',
});
const rnl = new RelayNetworkLayer([
batchMiddleware({
headers: () => ({
'thunk-header': '333',
}),
}),
]);
const req1 = mockReq(1);
const req2 = mockReq(2);
await Promise.all([req1.execute(rnl), req2.execute(rnl)]);
expect(fetchMock.lastOptions().headers).toEqual(
expect.objectContaining({
'thunk-header': '333',
})
);
});

it('`headers` option as thunk with Promise', async () => {
fetchMock.mock({
matcher: '/graphql/batch',
response: {
status: 200,
body: [{ id: 1, data: {} }, { id: 2, data: {} }],
},
method: 'POST',
});
const rnl = new RelayNetworkLayer([
batchMiddleware({
headers: () =>
Promise.resolve({
'thunk-header': 'as promise',
}),
}),
]);
const req1 = mockReq(1);
const req2 = mockReq(2);
await Promise.all([req1.execute(rnl), req2.execute(rnl)]);
expect(fetchMock.lastOptions().headers).toEqual(
expect.objectContaining({
'thunk-header': 'as promise',
})
);
});
});
});
41 changes: 31 additions & 10 deletions src/middlewares/batch.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,25 @@ import { isFunction } from '../utils';
import RelayRequestBatch from '../RelayRequestBatch';
import RelayRequest from '../RelayRequest';
import type RelayResponse from '../RelayResponse';
import type { Middleware } from '../definition';
import type { Middleware, FetchOpts } from '../definition';

// Max out at roughly 100kb (express-graphql imposed max)
const DEFAULT_BATCH_SIZE = 102400;

type Headers = { [name: string]: string };

export type BatchMiddlewareOpts = {|
batchUrl?: string | Promise<string> | ((requestMap: BatchRequestMap) => string | Promise<string>),
batchTimeout?: number,
maxBatchSize?: number,
allowMutations?: boolean,
credentials?: string,
method?: 'POST' | 'GET',
headers?: Headers | Promise<Headers> | ((req: RelayRequestBatch) => Headers | Promise<Headers>),
// Avaliable request modes in fetch options. For details see https://fetch.spec.whatwg.org/#requests
credentials?: $PropertyType<FetchOpts, 'credentials'>,
mode?: $PropertyType<FetchOpts, 'mode'>,
cache?: $PropertyType<FetchOpts, 'cache'>,
redirect?: $PropertyType<FetchOpts, 'redirect'>,
|};

export type BatchRequestMap = {
Expand All @@ -42,9 +50,16 @@ export default function batchMiddleware(options?: BatchMiddlewareOpts): Middlewa
const allowMutations = opts.allowMutations || false;
const batchUrl = opts.batchUrl || '/graphql/batch';
const maxBatchSize = opts.maxBatchSize || DEFAULT_BATCH_SIZE;
const credentials = opts.credentials;
const singleton = {};

const fetchOpts = {};
if (opts.method) fetchOpts.method = opts.method;
if (opts.credentials) fetchOpts.credentials = opts.credentials;
if (opts.mode) fetchOpts.mode = opts.mode;
if (opts.cache) fetchOpts.cache = opts.cache;
if (opts.redirect) fetchOpts.redirect = opts.redirect;
if (opts.headers) fetchOpts.headersOrThunk = opts.headers;

return next => req => {
// do not batch mutations unless allowMutations = true
if (req.isMutation() && !allowMutations) {
Expand All @@ -67,7 +82,7 @@ export default function batchMiddleware(options?: BatchMiddlewareOpts): Middlewa
batchUrl,
singleton,
maxBatchSize,
credentials,
fetchOpts,
});
};
}
Expand Down Expand Up @@ -159,14 +174,20 @@ async function sendRequests(requestMap: BatchRequestMap, next, opts) {
} else if (ids.length > 1) {
// SEND AS BATCHED QUERY

const batchFetchOpts = {
credentials: opts.credentials,
}

const batchRequest = new RelayRequestBatch(ids.map(id => requestMap[id].req), batchFetchOpts);
const batchRequest = new RelayRequestBatch(ids.map(id => requestMap[id].req));
// $FlowFixMe
const url = await (isFunction(opts.batchUrl) ? opts.batchUrl(requestMap) : opts.batchUrl);
batchRequest.fetchOpts.url = url;
batchRequest.setFetchOption('url', url);

const { headersOrThunk, ...fetchOpts } = opts.fetchOpts;
batchRequest.setFetchOptions(fetchOpts);

if (headersOrThunk) {
const headers = await (isFunction(headersOrThunk)
? headersOrThunk(batchRequest)
: headersOrThunk);
batchRequest.setFetchOption('headers', headers);
}

try {
const batchResponse = await next(batchRequest);
Expand Down

0 comments on commit 22e03c7

Please sign in to comment.