Last active
October 28, 2020 03:06
-
-
Save papandreou/abd7b8f00f7bff699a06e21330a2004a to your computer and use it in GitHub Desktop.
graphqlInMultipartFormDataMiddleware.js
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
const { graphqlExpress } = require('graphql-server-express'); | |
const graphQlTools = require('graphql-tools'); | |
const schema = graphQlTools.makeExecutableSchema({ | |
typeDefs: schemaText, | |
resolvers: { | |
Mutation: { | |
async uploadFiles(obj, { input }, req, info }) { | |
// ... | |
try { | |
let fileUpload; | |
while ((fileUpload = await req.getNextFileAsync())) { | |
const {name, fileName, contentType} = fileUpload; | |
// ... do something with the uploaded file... | |
} | |
return { /* UploadFilesPayload ... */ }; | |
} finally { | |
// Don't leave unconsumed file uploads in case of an error | |
req.disposeRemainingFiles(); | |
} | |
} | |
} | |
} | |
}); | |
const app = express() | |
.use(require('./graphqlInMultipartFormDataMiddleware')()) | |
.use(graphqlExpress((req, res) => ({schema, context: req}))); | |
app.listen(1337); |
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
async function graphQlRequest(opts) { | |
await fetch('/api/graphql', { | |
method: 'POST', | |
credentials: 'same-origin', | |
...opts | |
}) | |
} | |
async function uploadFiles(variables) { | |
const query = `mutation { uploadFiles(...) { ... } }`; | |
const formData = new FormData(); | |
formData.append('request', JSON.stringify({ | |
query: query, | |
variables | |
})); | |
uploads.forEach(({ name, payload }) => formData.append(name, payload)); | |
await graphQlRequest({ body: formData }); | |
} |
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
const BusBoy = require('busboy'); | |
const httpErrors = require('httperrors'); | |
const Promise = require('bluebird'); | |
function consumeReadableStream(readableStream) { | |
return new Promise(function (resolve, reject) { | |
const chunks = []; | |
readableStream.on('data', function (chunk) { | |
chunks.push(chunk); | |
}).on('end', function () { | |
if (chunks.length === 0) { | |
resolve(new Buffer()); | |
} else if (typeof chunks[0] === 'string') { | |
resolve(chunks.join('')); | |
} else { | |
resolve(Buffer.concat(chunks.map(chunk => Buffer.isBuffer(chunk) ? chunk : new Buffer(chunk)))); | |
} | |
}).on('error', reject); | |
}); | |
} | |
module.exports = () => { | |
return (req, res, next) => { | |
if (req.is('multipart/form-data')) { | |
let seenFirstPart = false; | |
let hasEnded = false; | |
let hasDisposed = false; | |
const bufferedFiles = []; | |
let waitQueue = []; | |
function formEnded() { | |
hasEnded = true; | |
if (seenFirstPart) { | |
while (waitQueue && waitQueue.length > 0) { | |
waitQueue.shift().resolve(null); | |
} | |
} else { | |
next(new httpErrors.BadRequest('First part of a multipart/form-data request must be application/graphql')); | |
} | |
} | |
function formErrored(err) { | |
if (seenFirstPart) { | |
if (!hasDisposed) { | |
req.disposeRemainingFiles(); | |
} | |
} else { | |
next(new httpErrors.BadRequest(err)); | |
} | |
} | |
function newFileArrived(file) { | |
if (waitQueue.length > 0) { | |
waitQueue.shift().resolve(file); | |
} else { | |
bufferedFiles.push(file); | |
} | |
} | |
req.getNextFileAsync = () => { | |
if (hasDisposed) { | |
return Promise.reject(new Error('The incoming form has already been disposed')); | |
} else if (bufferedFiles.length > 0) { | |
return Promise.resolve(bufferedFiles.shift()); | |
} else if (hasEnded) { | |
return Promise.resolve(null); | |
} else { | |
return new Promise((resolve, reject) => { | |
waitQueue.push({resolve, reject}); | |
}); | |
} | |
}; | |
req.disposeRemainingFiles = () => { | |
if (!hasDisposed) { | |
hasDisposed = true; | |
bufferedFiles.forEach(({file}) => file.resume()); | |
waitQueue.forEach(({reject}) => reject(new Error('The incoming form has already been disposed'))); | |
waitQueue = null; | |
} | |
}; | |
req.pipe(new BusBoy({headers: req.headers})).once('field', function (name, value) { | |
if (!hasDisposed) { | |
if (!seenFirstPart) { | |
seenFirstPart = true; | |
try { | |
req.body = JSON.parse(value); | |
} catch (e) { | |
return next(e); | |
} | |
next(); | |
} else { | |
// Ignore. Only the first part can be form-data, the remaining ones must be file uploads. | |
// We should probably see if we could respond 400, but the mutation has already taken | |
// over at this point. | |
} | |
} | |
}) | |
.on('file', function fileListener(name, file, fileName, encoding, contentType) { | |
if (hasDisposed) { | |
file.resume(); | |
} else if (!seenFirstPart) { | |
seenFirstPart = true; | |
if (contentType === 'application/graphql') { | |
consumeReadableStream(file).then(body => { | |
req.body = JSON.parse(body.toString('utf-8')); | |
next(); | |
}); | |
} else { | |
next(new httpErrors.BadRequest('First part of a multipart/form-data request must be application/graphql')); | |
} | |
} else { | |
newFileArrived({name, file, fileName, encoding, contentType}); | |
} | |
}) | |
.once('finish', formEnded) | |
.once('error', formErrored); | |
req.once('error', formErrored); | |
} else { | |
req.getNextFileAsync = function () { | |
return Promise.resolve(null); | |
}; | |
req.disposeRemainingFiles = () => {}; | |
next(); | |
} | |
}; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
how it should be defined in schema?