Skip to content

Commit a5b6f1b

Browse files
committed
fix: Bunch of small fixes, refactoring, cleanups
1 parent 3d74fc1 commit a5b6f1b

File tree

9 files changed

+200
-171
lines changed

9 files changed

+200
-171
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
ReactRelayNetworkLayer
22
======================
33
[![](https://img.shields.io/npm/v/react-relay-network-layer.svg)](https://www.npmjs.com/package/react-relay-network-layer)
4-
[![npm](https://img.shields.io/npm/dt/react-relay-network-layer.svg)](https://www.npmjs.com/package/react-relay-network-layer)
4+
[![npm](https://img.shields.io/npm/dt/react-relay-network-layer.svg)](http://www.npmtrends.com/react-relay-network-layer)
55
[![Travis](https://img.shields.io/travis/nodkz/react-relay-network-layer.svg?maxAge=2592000)](https://travis-ci.org/nodkz/react-relay-network-layer)
66
[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
77
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
@@ -43,7 +43,7 @@ Previous documentation for version 1.x.x can be found [here](https://github.com/
4343
- `batchUrl` - string or function(requestMap). Url of the server endpoint for batch request execution (default: `/graphql/batch`)
4444
- `batchTimeout` - integer in milliseconds, period of time for gathering multiple requests before sending them to the server (default: `0`)
4545
- `allowMutations` - by default batching disabled for mutations, you may enable it passing `true` (default: `false`)
46-
- `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. (default: 102400 (roughly 100kb))
46+
- `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. (default: `102400` characters, roughly 100kb for 1-byte characters or 200kb for 2-byte characters)
4747
- **retryMiddleware** - for request retry if the initial request fails.
4848
- `fetchTimeout` - number in milliseconds that defines in how much time will request timeout after it has been sent to the server (default: `15000`).
4949
- `retryDelays` - array of millisecond that defines the values on which retries are based on (default: `[1000, 3000]`).

src/createRequestError.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* Formats an error response from GraphQL server request.
3+
*/
4+
function formatRequestErrors(request, errors) {
5+
const CONTEXT_BEFORE = 20;
6+
const CONTEXT_LENGTH = 60;
7+
8+
if (!request.getQueryString) {
9+
return errors.join('\n');
10+
}
11+
12+
const queryLines = request.getQueryString().split('\n');
13+
return errors
14+
.map(({ locations, message }, ii) => {
15+
const prefix = `${ii + 1}. `;
16+
const indent = ' '.repeat(prefix.length);
17+
18+
// custom errors thrown in graphql-server may not have locations
19+
const locationMessage = locations
20+
? '\n' +
21+
locations
22+
.map(({ column, line }) => {
23+
const queryLine = queryLines[line - 1];
24+
const offset = Math.min(column - 1, CONTEXT_BEFORE);
25+
return [
26+
queryLine.substr(column - 1 - offset, CONTEXT_LENGTH),
27+
`${' '.repeat(Math.max(offset, 0))}^^^`,
28+
]
29+
.map(messageLine => indent + messageLine)
30+
.join('\n');
31+
})
32+
.join('\n')
33+
: '';
34+
return prefix + message + locationMessage;
35+
})
36+
.join('\n');
37+
}
38+
39+
export default function createRequestError(
40+
request,
41+
requestType,
42+
responseStatus,
43+
payload
44+
) {
45+
let errorReason;
46+
if (typeof payload === 'object' && payload.errors) {
47+
errorReason = formatRequestErrors(request, payload.errors);
48+
} else if (payload) {
49+
errorReason = payload;
50+
} else {
51+
errorReason = `Server response had an error status: ${responseStatus}`;
52+
}
53+
54+
let error;
55+
if (request.getDebugName) {
56+
// for native Relay request
57+
error = new Error(
58+
`Server request for ${requestType} \`${request.getDebugName()}\` ` +
59+
`failed for the following reasons:\n\n${errorReason}`
60+
);
61+
} else if (request && typeof request === 'object') {
62+
// for batch request
63+
const ids = Object.keys(request);
64+
error = new Error(
65+
`Server request for \`BATCH_QUERY:${ids.join(':')}\` ` +
66+
`failed for the following reasons:\n\n${errorReason}`
67+
);
68+
} else {
69+
error = new Error(
70+
`Server request failed for the following reasons:\n\n${errorReason}`
71+
);
72+
}
73+
74+
error.source = payload;
75+
error.status = responseStatus;
76+
return error;
77+
}

src/fetchWrapper.js renamed to src/fetchWithMiddleware.js

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
/* eslint-disable no-param-reassign */
22

3-
export default function fetchWrapper(reqFromRelay, middlewares) {
3+
import createRequestError from './createRequestError';
4+
5+
export default function fetchWithMiddleware(reqFromRelay, middlewares) {
46
const fetchAfterAllWrappers = req => {
57
let { url, ...opts } = req;
68

@@ -12,27 +14,46 @@ export default function fetchWrapper(reqFromRelay, middlewares) {
1214
}
1315
}
1416

15-
return fetch(url, opts).then(res => {
16-
// sub-promise for combining `res` with parsed json
17-
return res
18-
.json()
19-
.then(json => {
20-
res.json = json;
17+
return fetch(url, opts)
18+
.then(res => {
19+
if (res.status >= 200 && res.status < 300) {
2120
return res;
22-
})
23-
.catch(e => {
24-
console.warn('error parsing response json', e); // eslint-disable-line no-console
25-
res.json = {};
21+
}
22+
return res.text().then(text => {
23+
const err = new Error(text);
24+
err.fetchResponse = res;
25+
throw err;
26+
});
27+
})
28+
.then(res => {
29+
// sub-promise for combining `res` with parsed json
30+
return res.json().then(json => {
31+
res.json = json;
2632
return res;
2733
});
28-
});
34+
});
2935
};
3036

3137
const wrappedFetch = compose(...middlewares)(fetchAfterAllWrappers);
3238

33-
return wrappedFetch(reqFromRelay)
34-
.then(throwOnServerError)
35-
.then(res => res.json);
39+
return wrappedFetch(reqFromRelay).then(res => {
40+
const payload = res.json;
41+
if ({}.hasOwnProperty.call(payload, 'errors')) {
42+
throw createRequestError(
43+
reqFromRelay.relayReqObj,
44+
reqFromRelay.relayReqType,
45+
'200',
46+
payload
47+
);
48+
} else if (!{}.hasOwnProperty.call(payload, 'data')) {
49+
throw new Error(
50+
'Server response.data was missing for query `' +
51+
reqFromRelay.relayReqObj.getDebugName() +
52+
'`.'
53+
);
54+
}
55+
return payload.data;
56+
});
3657
}
3758

3859
/**
@@ -55,11 +76,3 @@ function compose(...funcs) {
5576
rest.reduceRight((composed, f) => f(composed), last(...args));
5677
}
5778
}
58-
59-
function throwOnServerError(response) {
60-
if (response.status >= 200 && response.status < 300) {
61-
return response;
62-
}
63-
64-
throw response;
65-
}

src/formatRequestErrors.js

Lines changed: 0 additions & 33 deletions
This file was deleted.

src/middleware/batch.js

Lines changed: 50 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,6 @@ import { isFunction } from '../utils';
55
// Max out at roughly 100kb (express-graphql imposed max)
66
const DEFAULT_BATCH_SIZE = 102400;
77

8-
function copyBatchResponse(batchResponse, res) {
9-
// Supports graphql-js, graphql-graphene and apollo-server responses
10-
const json = res.payload ? res.payload : res;
11-
return {
12-
ok: batchResponse.ok,
13-
status: batchResponse.status,
14-
json,
15-
};
16-
}
17-
188
export default function batchMiddleware(opts = {}) {
199
const batchTimeout = opts.batchTimeout || 0; // 0 is the same as nextTick in nodeJS
2010
const allowMutations = opts.allowMutations || false;
@@ -24,7 +14,12 @@ export default function batchMiddleware(opts = {}) {
2414

2515
return next =>
2616
req => {
27-
if (req.relayReqType === 'mutation' && !allowMutations) {
17+
// never batch mutations with files
18+
// mutation without files can be batched if allowMutations = true
19+
if (
20+
req.relayReqType === 'mutation' &&
21+
(!allowMutations || (global.FormData && req.body instanceof FormData))
22+
) {
2823
return next(req);
2924
}
3025

@@ -53,7 +48,17 @@ function passThroughBatch(req, next, opts) {
5348

5449
// queue request
5550
return new Promise((resolve, reject) => {
56-
singleton.batcher.requestMap[req.relayReqId] = { req, resolve, reject };
51+
singleton.batcher.requestMap[req.relayReqId] = {
52+
req,
53+
completeOk: res => {
54+
req.done = true;
55+
resolve(res);
56+
},
57+
completeErr: err => {
58+
req.done = true;
59+
reject(err);
60+
},
61+
};
5762
});
5863
}
5964

@@ -67,19 +72,9 @@ function prepareNewBatcher(next, opts) {
6772
setTimeout(
6873
() => {
6974
batcher.acceptRequests = false;
70-
sendRequests(batcher.requestMap, next, opts).then(() => {
71-
// check that server returns responses for all requests
72-
Object.keys(batcher.requestMap).forEach(id => {
73-
if (!batcher.requestMap[id].done) {
74-
batcher.requestMap[id].reject(
75-
new Error(
76-
`Server does not return response for request with id ${id} \n` +
77-
`eg. { "id": "${id}", "data": {} }`
78-
)
79-
);
80-
}
81-
});
82-
});
75+
sendRequests(batcher.requestMap, next, opts)
76+
.then(() => finalizeUncompleted(batcher))
77+
.catch(() => finalizeUncompleted(batcher));
8378
},
8479
opts.batchTimeout
8580
);
@@ -95,8 +90,7 @@ function sendRequests(requestMap, next, opts) {
9590
const request = requestMap[ids[0]];
9691

9792
return next(request.req).then(res => {
98-
request.done = true;
99-
request.resolve(res);
93+
request.completeOk(res);
10094
});
10195
} else if (ids.length > 1) {
10296
// SEND AS BATCHED QUERY
@@ -128,18 +122,43 @@ function sendRequests(requestMap, next, opts) {
128122
const request = requestMap[res.id];
129123
if (request) {
130124
const responsePayload = copyBatchResponse(batchResponse, res);
131-
request.done = true;
132-
request.resolve(responsePayload);
125+
request.completeOk(responsePayload);
133126
}
134127
});
135128
})
136129
.catch(e => {
137130
ids.forEach(id => {
138-
requestMap[id].done = true;
139-
requestMap[id].reject(e);
131+
requestMap[id].completeErr(e);
140132
});
141133
});
142134
}
143135

144136
return Promise.resolve();
145137
}
138+
139+
// check that server returns responses for all requests
140+
function finalizeUncompleted(batcher) {
141+
Object.keys(batcher.requestMap).forEach(id => {
142+
if (!batcher.requestMap[id].req.done) {
143+
batcher.requestMap[id].completeErr(
144+
new Error(
145+
`Server does not return response for request with id ${id} \n` +
146+
`eg. { "id": "${id}", "data": {} }`
147+
)
148+
);
149+
}
150+
});
151+
}
152+
153+
function copyBatchResponse(batchResponse, res) {
154+
// Fallback for graphql-graphene and apollo-server batch responses
155+
const json = res.payload || res;
156+
return {
157+
ok: batchResponse.ok,
158+
status: batchResponse.status,
159+
statusText: batchResponse.statusText,
160+
url: batchResponse.url,
161+
headers: batchResponse.headers,
162+
json,
163+
};
164+
}

src/middleware/url.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ export default function urlMiddleware(opts = {}) {
1616
}
1717
}
1818

19-
req.url = isFunction(urlOrThunk) ? urlOrThunk(req) : urlOrThunk;
19+
if (req.relayReqType !== 'batch-query') {
20+
req.url = isFunction(urlOrThunk) ? urlOrThunk(req) : urlOrThunk;
21+
}
2022

2123
return next(req);
2224
};

0 commit comments

Comments
 (0)