Skip to content

Instantly share code, notes, and snippets.

@nodkz
Last active April 21, 2016 16:16
Show Gist options
  • Save nodkz/d9a6380d55067192295382e8e490f39f to your computer and use it in GitHub Desktop.
Save nodkz/d9a6380d55067192295382e8e490f39f to your computer and use it in GitHub Desktop.
BatchRelayNetworkLayer + ExpressWrapMiddleware
/* eslint-disable prefer-template, arrow-body-style */
import 'whatwg-fetch';
// import fetchWithRetries from 'fbjs/lib/fetchWithRetries';
class AuthError extends Error {}
/**
* Rejects HTTP responses with a status code that is not >= 200 and < 300.
* This is done to follow the internal behavior of `fetchWithRetries`.
*/
function throwOnServerError(response) {
if (response.status >= 200 && response.status < 300) {
return response;
}
throw response;
}
function log(...msg) {
console.log('[RELAY]', ...msg);
}
/**
* Formats an error response from GraphQL server request.
*/
function formatRequestErrors(request, errors) {
const CONTEXT_BEFORE = 20;
const CONTEXT_LENGTH = 60;
const queryLines = request.getQueryString().split('\n');
return errors.map(({ locations, message }, ii) => {
const prefix = `${ii + 1}. `;
const indent = ' '.repeat(prefix.length);
// custom errors thrown in graphql-server may not have locations
const locationMessage = locations ?
('\n' + locations.map(({ column, line }) => {
const queryLine = queryLines[line - 1];
const offset = Math.min(column - 1, CONTEXT_BEFORE);
return [
queryLine.substr(column - 1 - offset, CONTEXT_LENGTH),
`${' '.repeat(offset)}^^^`,
].map(messageLine => indent + messageLine).join('\n');
}).join('\n')) :
'';
return prefix + message + locationMessage;
}).join('\n');
}
export class BatchRelayNetworkLayer {
constructor(uri, options) {
const { batchUri, ...init } = options;
this._uri = uri;
this._batchUri = batchUri;
this._init = { ...init };
}
supports = (...options) => {
// Does not support the only defined option, "defer".
return false;
};
_resolveQueryResponse = (request, payload) => {
if (payload.hasOwnProperty('errors')) {
const error = new Error(
'Server request for query `' + request.getDebugName() + '` ' +
'failed for the following reasons:\n\n' +
formatRequestErrors(request, payload.errors)
);
error.source = payload;
request.reject(error);
} else if (!payload.hasOwnProperty('data')) {
request.reject(new Error(
'Server response was missing for query `' + request.getDebugName() +
'`.'
));
} else {
request.resolve({ response: payload.data });
}
};
sendQueries = (requests) => {
if (requests.length > 1) {
this._sendBatchQuery(requests).then(
result => result.json()
).then((response) => {
response.forEach((payload) => {
const request = requests.find(r => r.getID() === payload.id);
if (request) {
this._resolveQueryResponse(request, payload.payload);
}
});
}).catch(
error => requests.forEach(r => r.reject(error))
);
} else {
return Promise.all(requests.map(request => {
this._sendQuery(request).then(
result => result.json()
).then(payload => {
this._resolveQueryResponse(request, payload);
}).catch(
error => request.reject(error)
);
}));
}
};
/**
* Sends a POST request and retries if the request fails or times out.
*/
_sendQuery = (request) => {
return fetch(this._uri, { // TODO fetchWithRetries
...this._init,
body: JSON.stringify({
query: request.getQueryString(),
variables: request.getVariables(),
}),
headers: {
...this._init.headers,
Accept: '*/*',
'Content-Type': 'application/json',
},
method: 'POST',
}).then(throwOnServerError);
};
/**
* Sends a POST request and retries if the request fails or times out.
*/
_sendBatchQuery = (requests) => {
return fetch(this._batchUri, { // TODO fetchWithRetries
...this._init,
body: JSON.stringify(requests.map((request) => ({
id: request.getID(),
query: request.getQueryString(),
variables: request.getVariables(),
}))),
headers: {
...this._init.headers,
Accept: '*/*',
'Content-Type': 'application/json',
},
method: 'POST',
}).then(throwOnServerError);
};
/**
* Sends a POST request with optional files.
*/
_sendMutation = (request) => {
let init;
const files = request.getFiles();
if (files) {
if (!global.FormData) {
throw new Error('Uploading files without `FormData` not supported.');
}
const formData = new FormData();
formData.append('query', request.getQueryString());
formData.append('variables', JSON.stringify(request.getVariables()));
for (const filename in files) {
if (files.hasOwnProperty(filename)) {
formData.append(filename, files[filename]);
}
}
init = {
...this._init,
body: formData,
method: 'POST',
};
} else {
init = {
...this._init,
body: JSON.stringify({
query: request.getQueryString(),
variables: request.getVariables(),
}),
headers: {
...this._init.headers,
Accept: '*/*',
'Content-Type': 'application/json',
},
method: 'POST',
};
}
return fetch(this._uri, init).then(throwOnServerError);
};
sendMutation = (request) => {
return this._sendMutation(request).then(
result => result.json()
).then(payload => {
if (payload.hasOwnProperty('errors')) {
const error = new Error(
'Server request for mutation `' + request.getDebugName() + '` ' +
'failed for the following reasons:\n\n' +
formatRequestErrors(request, payload.errors)
);
error.source = payload;
request.reject(error);
} else {
request.resolve({ response: payload.data });
}
}).catch(
error => request.reject(error)
);
};
}
import { BatchRelayNetworkLayer } from './batchRelayNetworkLayer';
const batchNetworkLayer = new BatchRelayNetworkLayer(
`${location.protocol}//${location.host}/graphql`,
{
batchUri: `${location.protocol}//${location.host}/graphql/batch`,
fetchTimeout: 10000,
}
);
Relay.injectNetworkLayer(batchNetworkLayer);
import 'babel-polyfill';
import express from 'express';
import graphqlHTTP from 'express-graphql';
import GraphQLSchema from './_schema/';
const port = 3000;
const app = express();
const graphQLMiddleware = graphqlHTTP(req => ({
schema: GraphQLSchema,
graphiql: true,
pretty: true,
}));
// only for batch graphql queries
app.use('/graphql/batch',
bodyParser.json(),
(req, res, next) => {
Promise.all(
req.body.map(data =>
new Promise((resolve) => {
const subRequest = {
__proto__: express.request,
...req,
body: data,
};
const subResponse = {
status(st) { this._status = st; return this; },
set() { return this; },
send(payload) {
resolve({ status: this._status, id: data.id, payload });
},
};
graphQLMiddleware(subRequest, subResponse);
})
)
).then(
(responses) => {
const response = [];
responses.forEach(({ status, id, payload }) => {
if (status) { res.status(status); }
response.push({
id,
payload: JSON.parse(payload),
});
});
res.send(response);
next();
}
);
}
);
// also provide source for regular graphql queries
app.use('/graphql', graphQLMiddleware);
server.listen(port, () => {
console.log(`The server is running at http://localhost:${port}/`);
});
@nodkz
Copy link
Author

nodkz commented Apr 21, 2016

In this solution presents one performance lack:

  • graphQLMiddleware returns stringified payload, so I should parse it and combine with id, and after that express again implicitly stringify it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment