diff --git a/.gitignore b/.gitignore index c8460a23..b21036c4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .vscode +.idea node_modules *.orig dist diff --git a/openapi.yaml b/openapi.yaml index 7e0c5fd3..f94aec56 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -66,6 +66,32 @@ paths: enum: - one - two + - name: testJson + in: query + description: JSON in query params + content: + application/json: + schema: + type: object + properties: + foo: + type: string + enum: + - bar + - baz + - name: testArray + in: query + description: Array in query param + schema: + type: array + items: + type: string + enum: + - foo + - bar + - baz + style: form + explode: false responses: '200': description: pet response @@ -110,7 +136,7 @@ paths: operationId: find pet by id parameters: - $ref: '#/components/parameters/id' - + # - name: id # in: path # description: ID of pet to fetch diff --git a/src/middlewares/openapi.request.validator.ts b/src/middlewares/openapi.request.validator.ts index 1d98717b..23418ec7 100644 --- a/src/middlewares/openapi.request.validator.ts +++ b/src/middlewares/openapi.request.validator.ts @@ -45,6 +45,7 @@ export class RequestValidator { private _middlewareCache; private _apiDocs; private ajv; + constructor(apiDocs, options = {}) { this._middlewareCache = {}; this._apiDocs = apiDocs; @@ -147,10 +148,10 @@ export class RequestValidator { private extractContentType(req) { let contentType = req.headers['content-type'] || 'not_provided'; - let end = contentType.indexOf(';') + let end = contentType.indexOf(';'); end = end === -1 ? contentType.length : end; if (contentType) { - return contentType.substring(0, end); + return contentType.substring(0, end); } return contentType; } @@ -196,6 +197,18 @@ export class RequestValidator { } }); + /** + * array deserialization + * filter=foo,bar,baz + * filter=foo|bar|baz + * filter=foo%20bar%20baz + */ + parameters.parseArray.forEach(item => { + if (req[item.reqField] && req[item.reqField][item.name]) { + req[item.reqField][item.name] = req[item.reqField][item.name].split(item.delimiter); + } + }); + const reqToValidate = { ...req, cookies: req.cookies @@ -249,7 +262,13 @@ export class RequestValidator { path: 'params', cookie: 'cookies', }; + const arrayDelimiter = { + form: ',', + spaceDelimited: ' ', + pipeDelimited: '|', + }; const parseJson = []; + const parseArray = []; parameters.forEach(parameter => { if (parameter.hasOwnProperty('$ref')) { @@ -280,6 +299,17 @@ export class RequestValidator { throw ono(err, message); } + if (parameter.schema && parameter.schema.type === 'array' && !parameter.explode) { + const delimiter = arrayDelimiter[parameter.style]; + if (!delimiter) { + const message = `Parameter 'style' has incorrect value '${parameter.style}' for [${parameter.name}]`; + const err = validationError(400, path, message); + throw ono(err, message); + } + + parseArray.push({ name, reqField, delimiter }); + } + if (!schema[reqField].properties) { schema[reqField] = { type: 'object', @@ -296,6 +326,6 @@ export class RequestValidator { } }); - return { schema, parseJson }; + return { schema, parseJson, parseArray }; } } diff --git a/test/resources/openapi.json b/test/resources/openapi.json index e0c4c621..10d261bb 100644 --- a/test/resources/openapi.json +++ b/test/resources/openapi.json @@ -88,7 +88,46 @@ "two" ] } - } + }, + { + "name": "testJson", + "in": "query", + "description": "JSON in query params", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "foo": { + "type": "string", + "enum": [ + "bar", + "baz" + ] + } + } + } + } + } + }, + { + "name": "testArray", + "in": "query", + "description": "Array in query param", + "schema": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "foo", + "bar", + "baz" + ] + } + }, + "style": "form", + "explode": false + } ], "responses": { "200": { diff --git a/test/routes.spec.ts b/test/routes.spec.ts index 5830de82..62d35ad5 100644 --- a/test/routes.spec.ts +++ b/test/routes.spec.ts @@ -66,6 +66,50 @@ const basePath = (app).basePath; expect(e[0].path).to.contain('limit'); expect(e[0].message).to.equal('should be >= 5'); })); + + it('should return 200 when JSON in query param', async () => + request(app) + .get(`${basePath}/pets`) + .query(`limit=10&test=one&testJson={"foo": "bar"}`) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200)); + + it('should return 400 when improper JSON in query param', async () => + request(app) + .get(`${basePath}/pets`) + .query(`limit=10&test=one&testJson={"foo": "test"}`) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(400) + .then(r => { + const e = r.body.errors; + expect(e).to.have.length(1); + expect(e[0].path).to.contain('testJson'); + expect(e[0].message).to.equal('should be equal to one of the allowed values'); + })); + + it('should return 200 when separated array in query param', async () => + request(app) + .get(`${basePath}/pets`) + .query(`limit=10&test=one&testArray=foo,bar,baz`) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200)); + + it('should return 400 when improper separated array in query param', async () => + request(app) + .get(`${basePath}/pets`) + .query(`limit=10&test=one&testArray=foo,bar,test`) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(400) + .then(r => { + const e = r.body.errors; + expect(e).to.have.length(1); + expect(e[0].path).to.contain('testArray'); + expect(e[0].message).to.equal('should be equal to one of the allowed values'); + })); }); describe('POST /pets', () => {