express-openapi-validator
Version:
Automatically validate API requests and responses with OpenAPI 3 and Express.
138 lines • 7.02 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.multipart = multipart;
const ajv_1 = require("../framework/ajv");
const types_1 = require("../framework/types");
const multer = require('multer');
function multipart(apiDoc, options) {
const mult = multer(options.multerOpts);
const Ajv = (0, ajv_1.createRequestAjv)(apiDoc, Object.assign({}, options.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)) {
try {
await new Promise((resolve, reject) => {
mult.any()(req, res, (err) => {
if (err) {
reject(error(req, err));
}
else {
// TODO:
// If a form parameter 'file' is defined to take file value, but the user provides a string value instead
// req.files will be empty and req.body.file will be populated with a string
// This will incorrectly PASS validation.
// Instead, we should return a 400 with an invalid type e.g. file expects a file, but found string.
//
// In order to support this, we likely need to inspect the schema directly to find the type.
// For example, if param with type: 'string', format: 'binary' is defined, we expect to see it in
// req.files. If it's not present we should throw a 400
//
// This is a bit complex because the schema may be defined inline (easy) or via a $ref (complex) in which
// 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 = req.files
.map((file) => file.fieldname)
.reduce((acc, curr) => {
acc[curr] = (acc[curr] || 0) + 1;
return acc;
}, {});
// add file(s) to body
Object.entries(count_by_fieldname).forEach(([fieldname, count]) => {
// 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('')
: '';
});
}
resolve();
}
});
});
next();
}
catch (error) {
next(error);
}
}
else {
next();
}
};
}
function shouldHandle(Ajv, req) {
var _a, _b, _c, _d;
const reqContentType = req.headers['content-type'];
if (isMultipart(req) && (reqContentType === null || reqContentType === void 0 ? void 0 : reqContentType.includes('multipart/form-data'))) {
return true;
}
const bodyRef = (_b = (_a = req === null || req === void 0 ? void 0 : req.openapi) === null || _a === void 0 ? void 0 : _a.schema) === null || _b === void 0 ? void 0 : _b.$ref;
const requestBody = bodyRef
? Ajv.getSchema(bodyRef)
: (_d = (_c = req === null || req === void 0 ? void 0 : req.openapi) === null || _c === void 0 ? void 0 : _c.schema) === null || _d === void 0 ? void 0 : _d.requestBody;
const bodyContent = requestBody === null || requestBody === void 0 ? void 0 : requestBody.content;
if (!bodyContent)
return false;
const content = bodyContent;
const contentTypes = Object.entries(content);
for (const [contentType, mediaType] of contentTypes) {
if (!contentType.includes(reqContentType))
continue;
const mediaTypeSchema = mediaType === null || mediaType === void 0 ? void 0 : mediaType.schema;
const schema = (mediaTypeSchema === null || mediaTypeSchema === void 0 ? void 0 : mediaTypeSchema.$ref)
? Ajv.getSchema(mediaTypeSchema.$ref)
: mediaTypeSchema;
const format = schema === null || schema === void 0 ? void 0 : schema.format;
if (format === 'binary') {
return true;
}
}
}
function isMultipart(req) {
var _a, _b, _c, _d;
return (_d = (_c = (_b = (_a = req === null || req === void 0 ? void 0 : req.openapi) === null || _a === void 0 ? void 0 : _a.schema) === null || _b === void 0 ? void 0 : _b.requestBody) === null || _c === void 0 ? void 0 : _c.content) === null || _d === void 0 ? void 0 : _d['multipart/form-data'];
}
function error(req, err) {
var _a;
if (err instanceof multer.MulterError) {
// distinguish common errors :
// - 413 ( Request Entity Too Large ) : Too many parts / File too large / Too many files
// - 400 ( Bad Request ) : Field * too long / Too many fields
// - 500 ( Internal Server Error ) : Unexpected field
const multerError = err;
const payload_too_big = /LIMIT_(FILE|PART)_(SIZE|COUNT)/.test(multerError.code);
const unexpected = /LIMIT_UNEXPECTED_FILE/.test(multerError.code);
const status = payload_too_big ? 413 : !unexpected ? 400 : 500;
return types_1.HttpError.create({
status: status,
path: req.path,
message: err.message,
});
/*return payload_too_big
? new RequestEntityTooLarge({ path: req.path, message: err.message })
: !unexpected
? new BadRequest({ path: req.path, message: err.message })
: new InternalServerError({ path: req.path, message: err.message });*/
}
else if (err instanceof types_1.HttpError) {
return err;
}
else {
// HACK
// TODO improve multer error handling
const missingField = /Multipart: Boundary not found/i.test((_a = err.message) !== null && _a !== void 0 ? _a : '');
if (missingField) {
return new types_1.BadRequest({
path: req.path,
message: 'multipart file(s) required',
});
}
else {
return new types_1.InternalServerError({ path: req.path, message: err.message });
}
}
}
//# sourceMappingURL=openapi.multipart.js.map