Skip to content

Instantly share code, notes, and snippets.

@Minasokoni
Forked from papandreou/app.js
Created July 22, 2019 17:51
Show Gist options
  • Save Minasokoni/44a48ddf957273a45fa0a661a3043534 to your computer and use it in GitHub Desktop.
Save Minasokoni/44a48ddf957273a45fa0a661a3043534 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();
}
};
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment