Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optional file upload #205

Merged
merged 6 commits into from
Dec 30, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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