diff --git a/services/api/package.json b/services/api/package.json index 8b804f45e..649adfede 100644 --- a/services/api/package.json +++ b/services/api/package.json @@ -35,6 +35,7 @@ "jszip": "^3.10.1", "koa": "^2.14.2", "koa-body": "^6.0.1", + "koa-compose": "^4.1.0", "lodash": "^4.17.21", "marked": "^5.1.0", "mongoose": "^6.9.1", diff --git a/services/api/src/app.js b/services/api/src/app.js index ab5620df5..2eea86952 100644 --- a/services/api/src/app.js +++ b/services/api/src/app.js @@ -1,9 +1,9 @@ const Router = require('@koa/router'); const Koa = require('koa'); const { version } = require('../package.json'); -const { koaBody: bodyParser } = require('koa-body'); const errorHandler = require('./utils/middleware/error-handler'); const corsMiddleware = require('./utils/middleware/cors'); +const bodyMiddleware = require('./utils/middleware/body'); const recordMiddleware = require('./utils/middleware/record'); const serializeMiddleware = require('./utils/middleware/serialize'); const { applicationMiddleware } = require('./utils/middleware/application'); @@ -48,7 +48,7 @@ if (['development'].includes(ENV_NAME)) { app.use(errorHandler); -app.use(logger.middleware()).use(bodyParser({ multipart: true })); +app.use(logger.middleware()).use(bodyMiddleware()); app.on('error', (err, ctx) => { if (err.code === 'EPIPE' || err.code === 'ECONNRESET') { diff --git a/services/api/src/utils/middleware/__fixtures__/test.png b/services/api/src/utils/middleware/__fixtures__/test.png new file mode 100644 index 000000000..be9bf72ac Binary files /dev/null and b/services/api/src/utils/middleware/__fixtures__/test.png differ diff --git a/services/api/src/utils/middleware/__tests__/body.js b/services/api/src/utils/middleware/__tests__/body.js new file mode 100644 index 000000000..5b52c587d --- /dev/null +++ b/services/api/src/utils/middleware/__tests__/body.js @@ -0,0 +1,39 @@ +const Koa = require('koa'); +const Router = require('@koa/router'); + +const bodyMiddleware = require('../body'); +const { request } = require('../../testing'); + +const file = __dirname + '/../__fixtures__/test.png'; + +describe('bodyMiddleware', () => { + it('should be able to send files and data in the same request', async () => { + // Leaning on supertest and koa here as multipart form + // data is difficult to send through a node stream. + const router = new Router(); + router.post('/', (ctx) => { + const { originalFilename } = ctx.request.files.file; + ctx.body = { + ...ctx.request.body, + originalFilename, + }; + }); + + const app = new Koa(); + app.use(bodyMiddleware()); + app.use(router.routes()); + + const response = await request( + 'POST', + '/', + { + foo: 'bar', + }, + { app, file } + ); + expect(response.body).toEqual({ + foo: 'bar', + originalFilename: 'test.png', + }); + }); +}); diff --git a/services/api/src/utils/middleware/body.js b/services/api/src/utils/middleware/body.js new file mode 100644 index 000000000..6bf753911 --- /dev/null +++ b/services/api/src/utils/middleware/body.js @@ -0,0 +1,25 @@ +const compose = require('koa-compose'); +const { koaBody } = require('koa-body'); + +module.exports = function () { + return compose([ + koaBody({ + multipart: true, + }), + parseMultipartBody(), + ]); +}; + +// Multipart bodies are assumed to be simple key/value pairs. +// This middleware parses each field as JSON to allow the client +// to send files and JSON data in the same request. +function parseMultipartBody() { + return async (ctx, next) => { + if (ctx.request.files) { + for (let [key, value] of Object.entries(ctx.request.body)) { + ctx.request.body[key] = JSON.parse(value); + } + } + return next(); + }; +} diff --git a/services/api/src/utils/testing/request.js b/services/api/src/utils/testing/request.js index 664007bc0..7becf77c7 100644 --- a/services/api/src/utils/testing/request.js +++ b/services/api/src/utils/testing/request.js @@ -1,5 +1,4 @@ const request = require('supertest'); //eslint-disable-line -const app = require('../../app'); const qs = require('querystring'); const { createAuthTokenPayload, createAuthToken } = require('../tokens'); @@ -21,6 +20,8 @@ module.exports = async function handleRequest(httpMethod, url, bodyOrQuery = {}, headers.organization = options.organization.id; } + const app = options.app || require('../../app'); + let promise; if (options.file) { @@ -30,7 +31,7 @@ module.exports = async function handleRequest(httpMethod, url, bodyOrQuery = {}, promise = promise.attach('file', file); } for (let [key, value] of Object.entries(bodyOrQuery)) { - promise = promise.field(key, value); + promise = promise.field(key, JSON.stringify(value)); } } else { if (httpMethod === 'POST') { diff --git a/services/web/src/utils/api/request.js b/services/web/src/utils/api/request.js index d7c152662..07e3aa782 100644 --- a/services/web/src/utils/api/request.js +++ b/services/web/src/utils/api/request.js @@ -39,7 +39,7 @@ export default async function request(options) { data.append('file', file); }); for (let [key, value] of Object.entries(body || {})) { - data.append(key, value); + data.append(key, JSON.stringify(value)); } body = data; } else if (!(body instanceof FormData)) {