Skip to content

Instantly share code, notes, and snippets.

@papandreou
Last active October 28, 2020 03:06
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save papandreou/abd7b8f00f7bff699a06e21330a2004a to your computer and use it in GitHub Desktop.
Save papandreou/abd7b8f00f7bff699a06e21330a2004a to your computer and use it in GitHub Desktop.
graphqlInMultipartFormDataMiddleware.js
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);
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 });
}
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();
}
};
};
@dokazhi
Copy link

dokazhi commented Aug 10, 2018

how it should be defined in schema?

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