Skip to content

Commit f16645f

Browse files
committed
Fix text form field support
refs: #30
1 parent 3cf37fc commit f16645f

5 files changed

Lines changed: 75 additions & 25 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ Options currently supports:
7878
- `ajvConfigBody` - Object that will be passed as config to new Ajv instance which will be used for validating request body. Can be useful to e. g. enable type coercion (to automatically convert strings to numbers etc). See Ajv documentation for supported values.
7979
- `ajvConfigParams` - Object that will be passed as config to new Ajv instance which will be used for validating request body. See Ajv documentation for supported values.
8080
- `contentTypeValidation` - Boolean that indicates if to perform content type validation in case `consume` field is specified and the request body is not empty.
81+
- `expectFormFieldsInBody` - Boolean that indicates whether fields of non-file type that are specified in the schema should be validated against request body (e. g. Multer is copying text fields to body)
8182

8283
```js
8384
formats: [

src/middleware.js

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ var SwaggerParser = require('swagger-parser'),
66
filesKeyword = require('./customKeywords/files'),
77
contentKeyword = require('./customKeywords/contentTypeValidation'),
88
InputValidationError = require('./inputValidationError'),
9-
schemaPreprocessor = require('./utils/schema-preprocessor');
9+
schemaPreprocessor = require('./utils/schema-preprocessor'),
10+
sourceResolver = require('./utils/sourceResolver');
1011

1112
var schemas = {};
1213
var middlewareOptions;
@@ -16,7 +17,7 @@ var ajvConfigParams;
1617
/**
1718
* Initialize the input validation middleware
1819
* @param {string} swaggerPath - the path for the swagger file
19-
* @param {Object} options - options.formats to add formats to ajv, options.beautifyErrors, options.firstError, options.fileNameField (default is 'fieldname' - multer package), options.ajvConfigBody and options.ajvConfigParams for config object that will be passed for creation of Ajv instance used for validation of body and parameters appropriately
20+
* @param {Object} options - options.formats to add formats to ajv, options.beautifyErrors, options.firstError, options.expectFormFieldsInBody, options.fileNameField (default is 'fieldname' - multer package), options.ajvConfigBody and options.ajvConfigParams for config object that will be passed for creation of Ajv instance used for validation of body and parameters appropriately
2021
*/
2122
function init(swaggerPath, options) {
2223
middlewareOptions = options || {};
@@ -38,22 +39,41 @@ function init(swaggerPath, options) {
3839
schemas[parsedPath][currentMethod.toLowerCase()] = {};
3940

4041
const parameters = dereferenced.paths[currentPath][currentMethod].parameters || [];
41-
let bodySchema = parameters.filter(function (parameter) { return parameter.in === 'body' });
42+
43+
let bodySchema = middlewareOptions.expectFormFieldsInBody
44+
? parameters.filter(function (parameter) { return (parameter.in === 'body' || (parameter.in === 'formData' && parameter.type !== 'file')) })
45+
: parameters.filter(function (parameter) { return parameter.in === 'body' });
46+
4247
if (makeOptionalAttributesNullable) {
4348
schemaPreprocessor.makeOptionalAttributesNullable(bodySchema);
4449
}
4550
if (bodySchema.length > 0) {
46-
schemas[parsedPath][currentMethod].body = buildBodyValidation(bodySchema[0].schema, dereferenced.definitions, swaggers[1], currentPath, currentMethod, parsedPath);
51+
let validatedBodySchema;
52+
if (bodySchema[0].in === 'body') {
53+
validatedBodySchema = bodySchema[0].schema;
54+
} else if (bodySchema[0].in === 'formData') {
55+
validatedBodySchema = {
56+
required: [],
57+
properties: {}
58+
};
59+
bodySchema.forEach((formField) => {
60+
if (formField.type !== 'file') {
61+
validatedBodySchema.properties[formField.name] = {
62+
type: formField.type
63+
};
64+
if (formField.required) {
65+
validatedBodySchema.required.push(formField.name);
66+
}
67+
}
68+
});
69+
}
70+
schemas[parsedPath][currentMethod].body = buildBodyValidation(validatedBodySchema, dereferenced.definitions, swaggers[1], currentPath, currentMethod, parsedPath);
4771
}
4872

4973
let localParameters = parameters.filter(function (parameter) {
5074
return parameter.in !== 'body';
5175
}).concat(pathParameters);
5276

53-
if (bodySchema.length > 0) {
54-
schemas[parsedPath][currentMethod].body = buildBodyValidation(bodySchema[0].schema, dereferenced.definitions, swaggers[1], currentPath, currentMethod, parsedPath);
55-
}
56-
5777
if (localParameters.length > 0 || middlewareOptions.contentTypeValidation) {
5878
schemas[parsedPath][currentMethod].parameters = buildParametersValidation(localParameters,
5979
dereferenced.paths[currentPath][currentMethod].consumes || dereferenced.paths[currentPath].consumes || dereferenced.consumes);
@@ -90,7 +110,7 @@ function validate(req, res, next) {
90110
firstError: middlewareOptions.firstError });
91111
return next(error);
92112
});
93-
};
113+
}
94114

95115
function _validateBody(body, path, method) {
96116
return new Promise(function (resolve, reject) {
@@ -209,7 +229,7 @@ function buildParametersValidation(parameters, contentTypes) {
209229
additionalProperties: false
210230
},
211231
files: {
212-
title: 'HTTP form',
232+
title: 'HTTP form files',
213233
files: {
214234
required: [],
215235
optional: []
@@ -222,7 +242,7 @@ function buildParametersValidation(parameters, contentTypes) {
222242
var data = Object.assign({}, parameter);
223243

224244
const required = parameter.required;
225-
const source = typeNameConversion[parameter.in] || parameter.in;
245+
const source = sourceResolver.resolveParameterSource(parameter);
226246
const key = parameter.in === 'header' ? parameter.name.toLowerCase() : parameter.name;
227247

228248
var destination = ajvParametersSchema.properties[source];
@@ -233,7 +253,7 @@ function buildParametersValidation(parameters, contentTypes) {
233253

234254
if (data.type === 'file') {
235255
required ? destination.files.required.push(key) : destination.files.optional.push(key);
236-
} else {
256+
} else if (source !== 'fields') {
237257
if (required) {
238258
destination.required = destination.required || [];
239259
destination.required.push(key);
@@ -252,8 +272,3 @@ module.exports = {
252272
validate: validate,
253273
InputValidationError: InputValidationError
254274
};
255-
256-
var typeNameConversion = {
257-
header: 'headers',
258-
formData: 'files'
259-
};

src/utils/sourceResolver.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Resolve value source for a given schema parameter
3+
* @param {Object} parameter from Swagger schema
4+
* @returns {string}
5+
*/
6+
function resolveParameterSource(parameter) {
7+
if (parameter.in === 'formData') {
8+
if (parameter.type === 'file') {
9+
return 'files';
10+
} else {
11+
return 'fields';
12+
}
13+
} else if (parameter.in === 'header') {
14+
return 'headers';
15+
}
16+
17+
return parameter.in;
18+
}
19+
20+
module.exports = {
21+
resolveParameterSource
22+
};

test/middleware-test.js

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2279,10 +2279,8 @@ describe('input-validation middleware tests', function () {
22792279
request(app)
22802280
.post('/login')
22812281
.set('api-version', '1.0')
2282-
.send({
2283-
username: 'user',
2284-
password: 'pass'
2285-
})
2282+
.field('username', 'user')
2283+
.field('password', 'pass')
22862284
.expect(200, function (err, res) {
22872285
if (err) {
22882286
throw err;
@@ -2291,5 +2289,18 @@ describe('input-validation middleware tests', function () {
22912289
done();
22922290
});
22932291
});
2292+
it('validates string formData', function (done) {
2293+
request(app)
2294+
.post('/login')
2295+
.set('api-version', '1.0')
2296+
.field('username', 'user')
2297+
.expect(400, function (err, res) {
2298+
if (err) {
2299+
throw err;
2300+
}
2301+
expect(res.body.more_info).to.includes('body should have required property \'password\'');
2302+
done();
2303+
});
2304+
});
22942305
});
22952306
});

test/test-server-formdata.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ var inputValidationOptions = {
1414
{ name: 'file', validate: () => { return true } }
1515
],
1616
beautifyErrors: true,
17-
firstError: true
17+
firstError: true,
18+
expectFormFieldsInBody: true
1819
};
1920

2021
module.exports = inputValidation.init('test/form-data-swagger.yaml', inputValidationOptions)
@@ -24,9 +25,9 @@ module.exports = inputValidation.init('test/form-data-swagger.yaml', inputValida
2425
app.post('/pets/import', upload.any(), inputValidation.validate, function (req, res, next) {
2526
res.json({ result: 'OK' });
2627
});
27-
app.post('/login', upload.any(), inputValidation.validate, function (req, res, next) {
28-
res.json({ result: 'OK' });
29-
});
28+
app.post('/login', upload.any(), inputValidation.validate, function (req, res, next) {
29+
res.json({ result: 'OK' });
30+
});
3031
app.use(function (err, req, res, next) {
3132
if (err instanceof inputValidation.InputValidationError) {
3233
res.status(400).json({ more_info: err.errors });

0 commit comments

Comments
 (0)