UNPKG

@apollographql/apollo-upload-server

Version:

Enhances Apollo GraphQL Server for intuitive file uploads via GraphQL mutations.

112 lines (101 loc) 4.14 kB
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); }); };