@apollographql/apollo-upload-server
Version:
Enhances Apollo GraphQL Server for intuitive file uploads via GraphQL mutations.
112 lines (101 loc) • 4.14 kB
JavaScript
import _asyncToGenerator from "@babel/runtime-corejs2/helpers/asyncToGenerator";
import _Map from "@babel/runtime-corejs2/core-js/map";
import _Object$entries from "@babel/runtime-corejs2/core-js/object/entries";
import _Promise from "@babel/runtime-corejs2/core-js/promise";
import Busboy from 'busboy';
import objectPath from 'object-path';
import { SPEC_URL, MaxFileSizeUploadError, MaxFilesUploadError, MapBeforeOperationsUploadError, FilesBeforeMapUploadError, FileMissingUploadError, UploadPromiseDisconnectUploadError, FileStreamDisconnectUploadError } from './errors';
class Upload {
constructor() {
this.promise = new _Promise((resolve, reject) => {
this.reject = reject;
this.resolve = file => {
this.file = file;
file.stream.once('end', () => {
this.done = true;
});
file.stream.once('limit', () => file.stream.emit('error', new MaxFileSizeUploadError('File truncated as it exceeds the size limit.')));
resolve(file);
};
});
}
}
export const processRequest = (request, {
maxFieldSize,
maxFileSize,
maxFiles
} = {}) => new _Promise((resolve, reject) => {
const parser = new Busboy({
headers: request.headers,
limits: {
fieldSize: maxFieldSize,
fields: 2,
fileSize: maxFileSize,
files: maxFiles
}
});
let operations;
let operationsPath;
let map;
parser.on('field', (fieldName, value) => {
switch (fieldName) {
case 'operations':
operations = JSON.parse(value);
operationsPath = objectPath(operations);
break;
case 'map':
{
if (!operations) return reject(new MapBeforeOperationsUploadError(`Misordered multipart fields; “map” should follow “operations” (${SPEC_URL}).`, 400));
const mapEntries = _Object$entries(JSON.parse(value));
if (mapEntries.length > maxFiles) return reject(new MaxFilesUploadError(`${maxFiles} max file uploads exceeded.`, 413));
map = new _Map();
for (const [fieldName, paths] of mapEntries) {
map.set(fieldName, new Upload());
for (const path of paths) operationsPath.set(path, map.get(fieldName).promise);
}
resolve(operations);
}
}
});
parser.on('file', (fieldName, stream, filename, encoding, mimetype) => {
if (!map) return reject(new FilesBeforeMapUploadError(`Misordered multipart fields; files should follow “map” (${SPEC_URL}).`, 400));
if (map.has(fieldName)) map.get(fieldName).resolve({
stream,
filename,
mimetype,
encoding
});else stream.resume();
});
parser.once('filesLimit', () => {
if (map) for (const upload of map.values()) if (!upload.file) upload.reject(new MaxFilesUploadError(`${maxFiles} max file uploads exceeded.`));
});
parser.once('finish', () => {
if (map) for (const upload of map.values()) if (!upload.file) upload.reject(new FileMissingUploadError('File missing in the request.'));
});
request.on('close', () => {
if (map) for (const upload of map.values()) if (!upload.file) upload.reject(new UploadPromiseDisconnectUploadError('Request disconnected before file upload stream parsing.'));else if (!upload.done) {
upload.file.stream.truncated = true;
upload.file.stream.emit('error', new FileStreamDisconnectUploadError('Request disconnected during file upload stream parsing.'));
}
});
request.pipe(parser);
});
export const apolloUploadKoa = options => function () {
var _ref = _asyncToGenerator(function* (ctx, next) {
if (ctx.request.is('multipart/form-data')) ctx.request.body = yield processRequest(ctx.req, options);
yield next();
});
return function (_x, _x2) {
return _ref.apply(this, arguments);
};
}();
export const apolloUploadExpress = options => (request, response, next) => {
if (!request.is('multipart/form-data')) return next();
processRequest(request, options).then(body => {
request.body = body;
next();
}).catch(error => {
if (error.status && error.expose) response.status(error.status);
next(error);
});
};