diff --git a/src/framework/types.ts b/src/framework/types.ts index b9391fea..e9bf6376 100644 --- a/src/framework/types.ts +++ b/src/framework/types.ts @@ -33,6 +33,8 @@ export type SecurityHandlers = { export interface MultipartOpts { multerOpts: boolean | multer.Options; + preMiddleware: OpenApiRequestHandler[]; + postMiddleware: OpenApiRequestHandler[]; ajvOpts: ajv.Options; } @@ -65,6 +67,12 @@ export type Format = { validate: (v: any) => boolean; }; +export type FileUploaderOptions = { + multer: multer.Options; + preMiddleware?: OpenApiRequestHandler[]; + postMiddleware?: OpenApiRequestHandler[]; +}; + export interface OpenApiValidatorOpts { apiSpec: OpenAPIV3.Document | string; validateResponses?: boolean | ValidateResponseOpts; @@ -75,8 +83,7 @@ export interface OpenApiValidatorOpts { coerceTypes?: boolean | 'array'; unknownFormats?: true | string[] | 'ignore'; formats?: Format[]; - fileUploader?: boolean | multer.Options; - multerOpts?: multer.Options; + fileUploader?: boolean | FileUploaderOptions; $refParser?: { mode: 'bundle' | 'dereference'; }; diff --git a/src/middlewares/openapi.metadata.ts b/src/middlewares/openapi.metadata.ts index 87d4fa1e..67817cd2 100644 --- a/src/middlewares/openapi.metadata.ts +++ b/src/middlewares/openapi.metadata.ts @@ -1,6 +1,5 @@ import * as _zipObject from 'lodash.zipobject'; import { pathToRegexp } from 'path-to-regexp'; -import * as deepCopy from 'lodash.clonedeep'; import { Response, NextFunction } from 'express'; import { OpenApiContext } from '../framework/openapi.context'; import { diff --git a/src/middlewares/openapi.multipart.ts b/src/middlewares/openapi.multipart.ts index 52a071bf..4b6e3ca5 100644 --- a/src/middlewares/openapi.multipart.ts +++ b/src/middlewares/openapi.multipart.ts @@ -1,4 +1,3 @@ -import { OpenApiContext } from '../framework/openapi.context'; import { createRequestAjv } from '../framework/ajv'; import { OpenAPIV3, @@ -11,22 +10,27 @@ import { MultipartOpts, } from '../framework/types'; import { MulterError } from 'multer'; - -const multer = require('multer'); +import * as multer from 'multer'; +import { Response } from 'express'; export function multipart( apiDoc: OpenAPIV3.Document, options: MultipartOpts, ): OpenApiRequestHandler { - const mult = multer(options.multerOpts); - const Ajv = createRequestAjv(apiDoc, { ...options.ajvOpts }); - return (req, res, next) => { + const { preMiddleware, postMiddleware, multerOpts, ajvOpts } = options; + const mult = multer(multerOpts); + const Ajv = createRequestAjv(apiDoc, { ...ajvOpts }); + return async (req, res, next) => { // TODO check that format: binary (for upload) else do not use multer.any() // use multer.none() if no binary parameters exist if (shouldHandle(Ajv, req)) { + const preError = await handleMiddleware(preMiddleware, req, res); + if (preError) return next(error(req, preError)); + + let multError: any; mult.any()(req, res, (err) => { if (err) { - next(error(req, err)); + return (multError = err); } else { // TODO: // If a form parameter 'file' is defined to take file value, but the user provides a string value instead @@ -62,15 +66,32 @@ export function multipart( }, ); } - next(); } }); - } else { - next(); + + if (multError) return next(error(req, multError)); + + const postError = await handleMiddleware(preMiddleware, req, res); + if (postError) return next(error(req, postError)); } + + next(); }; } +async function handleMiddleware( + middlewares: OpenApiRequestHandler[], + req: OpenApiRequest, + res: Response, +): Promise { + for (let mw of middlewares) { + let error: any; + const result = mw(req, res, (err: any) => (error = err)); + if (result instanceof Promise) await result; + if (error) return error; + } +} + function shouldHandle(Ajv, req: OpenApiRequest): boolean { const reqContentType = req.headers['content-type']; if (isMultipart(req) && reqContentType?.includes('multipart/form-data')) { diff --git a/src/openapi.validator.ts b/src/openapi.validator.ts index 61a833b6..90a4389b 100644 --- a/src/openapi.validator.ts +++ b/src/openapi.validator.ts @@ -19,7 +19,10 @@ import { RequestValidatorOptions, } from './framework/types'; import { defaultResolver } from './resolvers'; -import { OperationHandlerOptions } from './framework/types'; +import { + OperationHandlerOptions, + FileUploaderOptions, +} from './framework/types'; import { RequestSchemaPreprocessor } from './middlewares/parsers/request.schema.preprocessor'; export { @@ -46,7 +49,7 @@ export class OpenApiValidator { if (options.validateRequests == null) options.validateRequests = true; if (options.validateResponses == null) options.validateResponses = false; if (options.validateSecurity == null) options.validateSecurity = true; - if (options.fileUploader == null) options.fileUploader = {}; + if (options.fileUploader == null) options.fileUploader = { multer: {} }; if (options.$refParser == null) options.$refParser = { mode: 'bundle' }; if (options.unknownFormats == null) options.unknownFormats === true; if (options.validateFormats == null) options.validateFormats = 'fast'; @@ -71,7 +74,7 @@ export class OpenApiValidator { options.validateResponses = { removeAdditional: false, coerceTypes: false, - onError: null + onError: null, }; } @@ -249,7 +252,11 @@ export class OpenApiValidator { private multipartMiddleware(apiDoc: OpenAPIV3.Document) { return middlewares.multipart(apiDoc, { - multerOpts: this.options.fileUploader, + multerOpts: (this.options.fileUploader).multer, + preMiddleware: (this.options.fileUploader) + .preMiddleware, + postMiddleware: (this.options.fileUploader) + .postMiddleware, ajvOpts: this.ajvOpts.multipart, }); } @@ -274,7 +281,7 @@ export class OpenApiValidator { apiDoc, this.ajvOpts.response, // This has already been converted from boolean if required - this.options.validateResponses as ValidateResponseOpts + this.options.validateResponses as ValidateResponseOpts, ).validate(); } diff --git a/test/multipart.spec.ts b/test/multipart.spec.ts index 99b937ef..beb421f9 100644 --- a/test/multipart.spec.ts +++ b/test/multipart.spec.ts @@ -15,14 +15,16 @@ describe(packageJson.name, () => { { apiSpec, fileUploader: { - fileFilter: (req, file, cb) => { - fileNames.push(file.originalname); - cb(null, true); + multer: { + fileFilter: (req, file, cb) => { + fileNames.push(file.originalname); + cb(null, true); + }, }, }, }, 3003, - app => + (app) => app.use( `${app.basePath}`, express @@ -52,10 +54,8 @@ describe(packageJson.name, () => { .set('Content-Type', 'multipart/form-data') .set('Accept', 'application/json') .expect(400) - .then(e => { - expect(e.body) - .has.property('errors') - .with.length(1); + .then((e) => { + expect(e.body).has.property('errors').with.length(1); expect(e.body.errors[0]) .has.property('message') .equal('multipart file(s) required'); @@ -69,7 +69,7 @@ describe(packageJson.name, () => { .attach('file', 'package.json') .expect(400)); - it('should validate application/octet-stream file and metadata', done => { + it('should validate application/octet-stream file and metadata', (done) => { const testImage = `${__dirname}/assets/image.png`; const req = request(app) .post(`${app.basePath}/sample_3`) @@ -88,14 +88,10 @@ describe(packageJson.name, () => { .attach('file', 'package.json') .field('metadata', 'some-metadata') .expect(200) - .then(r => { + .then((r) => { const b = r.body; - expect(b.files) - .to.be.an('array') - .with.length(1); - expect(b.files[0]) - .to.have.property('fieldname') - .to.equal('file'); + expect(b.files).to.be.an('array').with.length(1); + expect(b.files[0]).to.have.property('fieldname').to.equal('file'); expect(b.metadata).to.equal('some-metadata'); }); expect(fileNames).to.deep.equal(['package.json']); @@ -117,10 +113,8 @@ describe(packageJson.name, () => { .set('Content-Type', 'application/json') .expect('Content-Type', /json/) .expect(415) - .then(r => { - expect(r.body) - .has.property('errors') - .with.length(1); + .then((r) => { + expect(r.body).has.property('errors').with.length(1); expect(r.body.errors[0]) .has.property('message') .equal('unsupported media type application/json');