Skip to content

Commit

Permalink
Merge pull request #205 from cdimascio/optional-file-upload
Browse files Browse the repository at this point in the history
Optional file upload
  • Loading branch information
cdimascio authored Dec 30, 2019
2 parents f917bba + 8e1561a commit 72428c2
Show file tree
Hide file tree
Showing 16 changed files with 374 additions and 50 deletions.
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ new OpenApiValidator(options).install({
},
ignorePaths: /.*\/pets$/,
unknownFormats: ['phone-number', 'uuid'],
multerOpts: { ... },
fileUploader: { ... } | true | false,
$refParser: {
mode: 'bundle'
}
Expand Down Expand Up @@ -454,10 +454,22 @@ Defines how the validator should behave if an unknown or custom format is encoun

- `"ignore"` - to log warning during schema compilation and always pass validation. This option is not recommended, as it allows to mistype format name and it won't be validated without any error message.

### ▪️ multerOpts (optional)
### ▪️ fileUploader (optional)

Specifies the options to passthrough to multer. express-openapi-validator uses multer to handle file uploads. see [multer opts](https://github.com/expressjs/multer)

- `true` (**default**) - enables multer and provides simple file(s) upload capabilities
- `false` - disables file upload capability. Upload capabilities may be provided by the user
- `{...}` - multer options to be passed-through to multer. see [multer opts](https://github.com/expressjs/multer) for possible options

e.g.

```javascript
fileUploader: {
dest: 'uploads/';
}
```

### ▪️ coerceTypes (optional)

Determines whether the validator should coerce value types to match the type defined in the OpenAPI spec.
Expand Down Expand Up @@ -773,6 +785,10 @@ module.exports = app;

**A:** In v3, `securityHandlers` have been replaced by `validateSecurity.handlers`. To use v3 security handlers, move your existing security handlers to the new property. No other change is required. Note that the v2 `securityHandlers` property is supported in v3, but deprecated

Q: What happened to the `multerOpts` property?

A: In v3, `multerOpts` have been replaced by `fileUploader`. In order to use the v3 `fileUploader`, move your multer options to `fileUploader` No other change is required. Note that the v2 `multerOpts` property is supported in v3, but deprecated

**Q:** Can I use a top level await?

**A:** Top-level await is currently a stage 3 proposal, however it can be used today with [babel](https://babeljs.io/docs/en/babel-plugin-syntax-top-level-await)
Expand Down
21 changes: 18 additions & 3 deletions example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,10 @@ new OpenApiValidator({
.then(app => {
// 5. Define routes using Express
app.get('/v1/pets', function(req, res, next) {
res.json([{ id: 1, name: 'max' }, { id: 2, name: 'mini' }]);
res.json([
{ id: 1, name: 'max' },
{ id: 2, name: 'mini' },
]);
});

app.post('/v1/pets', function(req, res, next) {
Expand Down Expand Up @@ -364,7 +367,7 @@ new OpenApiValidator(options).install({
validateResponses: true,
ignorePaths: /.*\/pets$/
unknownFormats: ['phone-number', 'uuid'],
multerOpts: { ... },
fileUploader: { ... },
securityHandlers: {
ApiKeyAuth: (req, scopes, schema) => {
throw { status: 401, message: 'sorry' }
Expand Down Expand Up @@ -461,10 +464,22 @@ Defines how the validator should behave if an unknown or custom format is encoun

- `"ignore"` - to log warning during schema compilation and always pass validation. This option is not recommended, as it allows to mistype format name and it won't be validated without any error message.

### ▪️ multerOpts (optional)
### ▪️ fileUploader (optional)

Specifies the options to passthrough to multer. express-openapi-validator uses multer to handle file uploads. see [multer opts](https://github.com/expressjs/multer)

- `true` (**default**) - enables multer and provides simple file(s) upload capabilities
- `false` - disables file upload capability. Upload capabilities may be provided by the user
- `{...}` - multer options to be passed-through to multer. see [multer opts](https://github.com/expressjs/multer) for possible options

e.g.

```javascript
fileUploader: {
dest: 'uploads/';
}
```

### ▪️ coerceTypes (optional)

Determines whether the validator should coerce value types to match the type defined in the OpenAPI spec.
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "express-openapi-validator",
"version": "3.3.1",
"version": "3.4.1",
"description": "Automatically validate API requests and responses with OpenAPI 3 and Express.",
"main": "dist/index.js",
"scripts": {
Expand Down
4 changes: 3 additions & 1 deletion src/framework/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as ajv from 'ajv';
import * as multer from 'multer';
import { Request, Response, NextFunction } from 'express';
export { OpenAPIFrameworkArgs };

Expand Down Expand Up @@ -55,7 +56,8 @@ export interface OpenApiValidatorOpts {
securityHandlers?: SecurityHandlers;
coerceTypes?: boolean | 'array';
unknownFormats?: true | string[] | 'ignore';
multerOpts?: {};
fileUploader?: boolean | multer.Options;
multerOpts?: multer.Options;
$refParser?: {
mode: 'bundle' | 'dereference';
};
Expand Down
23 changes: 22 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,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.$refParser == null) options.$refParser = { mode: 'bundle' };

if (options.validateResponses === true) {
Expand Down Expand Up @@ -83,7 +84,9 @@ export class OpenApiValidator {

this.installPathParams(app, context);
this.installMetadataMiddleware(app, context);
this.installMultipartMiddleware(app, context);
if (this.options.fileUploader) {
this.installMultipartMiddleware(app, context);
}

const components = context.apiDoc.components;
if (this.options.validateSecurity && components?.securitySchemes) {
Expand Down Expand Up @@ -218,6 +221,20 @@ export class OpenApiValidator {
);
}

const multerOpts = options.multerOpts;
if (securityHandlers != null) {
if (typeof multerOpts !== 'object' || Array.isArray(securityHandlers)) {
throw ono('multerOpts must be an object or undefined');
}
deprecationWarning('multerOpts is deprecated. Use fileUploader instead.');
}

if (options.multerOpts && options.fileUploader) {
throw ono(
'multerOpts and fileUploader may not be used together. Use fileUploader to specify upload options.',
);
}

const unknownFormats = options.unknownFormats;
if (typeof unknownFormats === 'boolean') {
if (!unknownFormats) {
Expand All @@ -243,5 +260,9 @@ export class OpenApiValidator {
};
delete options.securityHandlers;
}
if (options.multerOpts) {
options.fileUploader = options.multerOpts;
delete options.multerOpts;
}
}
}
30 changes: 15 additions & 15 deletions src/middlewares/openapi.multipart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ const multer = require('multer');

export function multipart(
OpenApiContext: OpenApiContext,
multerOpts: {} = {},
multerOpts: {},
): OpenApiRequestHandler {
const mult = multer(multerOpts);
return (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 (isMultipart(req) && isValidContentType(req)) {
mult.any()(req, res, err => {
if (err) {
Expand All @@ -35,7 +37,6 @@ export function multipart(
// case we must follow the $ref to check the type.

if (req.files) {

// to handle single and multiple file upload at the same time, let us this initialize this count variable
// for example { "files": 5 }
const count_by_fieldname = (<Express.Multer.File[]>req.files)
Expand All @@ -46,18 +47,15 @@ export function multipart(
}, {});

// add file(s) to body
Object
.entries(count_by_fieldname)
.forEach(
([fieldname, count]: [string, number]) => {
// TODO maybe also check in the api doc if it is a single upload or multiple
const is_multiple = count > 1;
req.body[fieldname] = (is_multiple)
? new Array(count).fill('')
: '';
},
);

Object.entries(count_by_fieldname).forEach(
([fieldname, count]: [string, number]) => {
// TODO maybe also check in the api doc if it is a single upload or multiple
const is_multiple = count > 1;
req.body[fieldname] = is_multiple
? new Array(count).fill('')
: '';
},
);
}
next();
}
Expand All @@ -74,7 +72,9 @@ function isValidContentType(req: Request): boolean {
}

function isMultipart(req: OpenApiRequest): boolean {
return (<any>req?.openapi)?.schema?.requestBody?.content?.['multipart/form-data'];
return (<any>req?.openapi)?.schema?.requestBody?.content?.[
'multipart/form-data'
];
}

function error(req: OpenApiRequest, err: Error): ValidationError {
Expand Down
4 changes: 2 additions & 2 deletions src/middlewares/parsers/body.parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ export class BodySchemaParser {
requestBody: OpenAPIV3.RequestBodyObject,
): BodySchema {
const bodyContentSchema =
requestBody.content[contentType.contentType] &&
requestBody.content[contentType.contentType].schema;
requestBody.content[contentType.withoutBoundary] &&
requestBody.content[contentType.withoutBoundary].schema;

let bodyContentRefSchema = null;
if (bodyContentSchema && '$ref' in bodyContentSchema) {
Expand Down
2 changes: 1 addition & 1 deletion src/middlewares/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export class ContentType {
public contentType: string = null;
public mediaType: string = null;
public charSet: string = null;
private withoutBoundary: string = null;
public withoutBoundary: string = null;
private constructor(contentType: string | null) {
this.contentType = contentType;
if (contentType) {
Expand Down
18 changes: 9 additions & 9 deletions test/common/app.common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,15 +98,15 @@ export function routes(app) {
});
});

app.post('/v1/pets/:id/photos', function(req: Request, res: Response): void {
// req.file is the `avatar` file
// req.body will hold the text fields, if there were any
const files = req.files;
res.status(200).json({
files,
metadata: req.body.metadata,
});
});
// app.post('/v1/pets/:id/photos', function(req: Request, res: Response): void {
// // req.file is the `avatar` file
// // req.body will hold the text fields, if there were any
// const files = req.files;
// res.status(200).json({
// files,
// metadata: req.body.metadata,
// });
// });
app.post('/v1/pets_charset', function(req: Request, res: Response): void {
// req.file is the `avatar` file
// req.body will hold the text fields, if there were any
Expand Down
1 change: 0 additions & 1 deletion test/common/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ export async function createApp(
app.use(bodyParser.json({ type: 'application/hal+json' }));
app.use(bodyParser.text());
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
Expand Down
13 changes: 10 additions & 3 deletions test/common/myapp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import { OpenApiValidator } from '../../src';

const app = express();

app.use(bodyParser.urlencoded());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.text());
app.use(bodyParser.json());
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
// app.use(express.json());
// app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
const spec = path.join(__dirname, 'openapi.yaml');
Expand Down Expand Up @@ -44,6 +44,13 @@ app.get('/v1/pets/:id', function(req: Request, res: Response): void {
res.json({ id: req.params.id, name: 'sparky' });
});

app.get('/v1/pets/:id/form_urlencoded', function(
req: Request,
res: Response,
): void {
res.json(req.body);
});

// 2a. Add a route upload file(s)
app.post('/v1/pets/:id/photos', function(req: Request, res: Response): void {
// DO something with the file
Expand Down
Loading

0 comments on commit 72428c2

Please sign in to comment.