Last active
April 21, 2016 16:16
-
-
Save nodkz/d9a6380d55067192295382e8e490f39f to your computer and use it in GitHub Desktop.
BatchRelayNetworkLayer + ExpressWrapMiddleware
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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) | |
); | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}/`); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.