@apollographql/apollo-upload-server
Version:
Enhances Apollo GraphQL Server for intuitive file uploads via GraphQL mutations.
133 lines (110 loc) • 4.63 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.apolloUploadExpress = exports.apolloUploadKoa = exports.processRequest = void 0;
var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/asyncToGenerator"));
var _map = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/map"));
var _entries = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/object/entries"));
var _promise = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/promise"));
var _busboy = _interopRequireDefault(require("busboy"));
var _objectPath = _interopRequireDefault(require("object-path"));
var _errors = require("./errors");
class Upload {
constructor() {
this.promise = new _promise.default((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 _errors.MaxFileSizeUploadError('File truncated as it exceeds the size limit.')));
resolve(file);
};
});
}
}
const processRequest = (request, {
maxFieldSize,
maxFileSize,
maxFiles
} = {}) => new _promise.default((resolve, reject) => {
const parser = new _busboy.default({
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 = (0, _objectPath.default)(operations);
break;
case 'map':
{
if (!operations) return reject(new _errors.MapBeforeOperationsUploadError(`Misordered multipart fields; “map” should follow “operations” (${_errors.SPEC_URL}).`, 400));
const mapEntries = (0, _entries.default)(JSON.parse(value));
if (mapEntries.length > maxFiles) return reject(new _errors.MaxFilesUploadError(`${maxFiles} max file uploads exceeded.`, 413));
map = new _map.default();
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 _errors.FilesBeforeMapUploadError(`Misordered multipart fields; files should follow “map” (${_errors.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 _errors.MaxFilesUploadError(`${maxFiles} max file uploads exceeded.`));
});
parser.once('finish', () => {
if (map) for (const upload of map.values()) if (!upload.file) upload.reject(new _errors.FileMissingUploadError('File missing in the request.'));
});
request.on('close', () => {
if (map) for (const upload of map.values()) if (!upload.file) upload.reject(new _errors.UploadPromiseDisconnectUploadError('Request disconnected before file upload stream parsing.'));else if (!upload.done) {
upload.file.stream.truncated = true;
upload.file.stream.emit('error', new _errors.FileStreamDisconnectUploadError('Request disconnected during file upload stream parsing.'));
}
});
request.pipe(parser);
});
exports.processRequest = processRequest;
const apolloUploadKoa = options => function () {
var _ref = (0, _asyncToGenerator2.default)(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);
};
}();
exports.apolloUploadKoa = apolloUploadKoa;
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);
});
};
exports.apolloUploadExpress = apolloUploadExpress;