From 8fc7226e831083123839e08af7f1d928c37fe2bd Mon Sep 17 00:00:00 2001 From: Pierre Le Roux Date: Fri, 24 Dec 2021 12:08:34 +0100 Subject: [PATCH] ResponseValidator's Ajv can be useful too. So we return an object that contains both request ajv and response ajv : ```javascript ajvs = { req : 'Ajv object' res : 'Ajv object' } ``` #683 --- README.md | 29 ++++- src/framework/types.ts | 6 + src/middlewares/openapi.response.validator.ts | 5 + src/openapi.validator.ts | 13 ++- test/ajv.return.spec.ts | 106 ++++++++++++++---- 5 files changed, 133 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index b4f89df2..66aacd16 100644 --- a/README.md +++ b/README.md @@ -1156,17 +1156,35 @@ Instead of initialize OpenApiValidator with middleware, you can get a configured ```javascript -const ajv = await OpenApiValidator.ajv({ +const ajvs = await OpenApiValidator.ajv({ apiSpec: './openapi.yaml', validateRequests: true, // (default) validateResponses: true, // false by default }); -const req : ReqClass = { +const customObj = { id : '507f191e810c19729de860ea', } -ajv.validate( +const isReqValid = ajvs.req.validate( + { + type: 'object', + properties: { + id: { + $ref: '#/components/schemas/ObjectId', + }, + }, + required: ['token'], + additionalProperties: false, + }, + customObj +); + +// isReqValid = true +// No error in ajvs.req.errors + + +const isResValid = ajvs.res.validate( { type: 'object', properties: { @@ -1177,8 +1195,11 @@ ajv.validate( required: ['token'], additionalProperties: false, }, - req + customObj ); + +// isResValid = true +// No error in ajvs.res.errors ``` ## FAQ diff --git a/src/framework/types.ts b/src/framework/types.ts index cc16a520..f4a77d1a 100644 --- a/src/framework/types.ts +++ b/src/framework/types.ts @@ -1,6 +1,7 @@ import * as ajv from 'ajv'; import * as multer from 'multer'; import { Request, Response, NextFunction } from 'express'; +import { Ajv } from 'ajv'; export { OpenAPIFrameworkArgs }; export type BodySchema = @@ -155,6 +156,11 @@ export namespace OpenAPIV3 { version: string; } + export interface Ajvs { + req?: Ajv, + res?: Ajv + } + export interface ContactObject { name?: string; url?: string; diff --git a/src/middlewares/openapi.response.validator.ts b/src/middlewares/openapi.response.validator.ts index 28494375..4983aa53 100644 --- a/src/middlewares/openapi.response.validator.ts +++ b/src/middlewares/openapi.response.validator.ts @@ -17,6 +17,7 @@ import { } from '../framework/types'; import * as mediaTypeParser from 'media-typer'; import * as contentTypeParser from 'content-type'; +import { Ajv } from 'ajv'; interface ValidateResult { validators: { [key: string]: ajv.ValidateFunction }; @@ -318,4 +319,8 @@ export class ResponseValidator { mediaTypeParsed.subtype === 'json' || mediaTypeParsed.suffix === 'json' ); } + + public getAJV() : Ajv { + return this.ajvBody; + } } diff --git a/src/openapi.validator.ts b/src/openapi.validator.ts index 215f14b7..8c384c97 100644 --- a/src/openapi.validator.ts +++ b/src/openapi.validator.ts @@ -21,6 +21,7 @@ import { defaultSerDes } from './framework/base.serdes'; import { SchemaPreprocessor } from './middlewares/parsers/schema.preprocessor'; import { AjvOptions } from './framework/ajv/options'; import { Ajv } from 'ajv'; +import Ajvs = OpenAPIV3.Ajvs; export { OpenApiValidatorOpts, @@ -91,7 +92,7 @@ export class OpenApiValidator { this.ajvOpts = new AjvOptions(options); } - installAjv(spec: Promise): Promise { + installAjv(spec: Promise): Promise { return spec .then((spec) => { const apiDoc = spec.apiDoc; @@ -107,7 +108,15 @@ export class OpenApiValidator { responseApiDoc: sp.apiDocRes, error: null, };*/ - return new middlewares.RequestValidator(apiDoc, this.ajvOpts.request).getAJV(); + return { + req : new middlewares.RequestValidator(apiDoc, this.ajvOpts.request).getAJV(), + res : new middlewares.ResponseValidator( + apiDoc, + this.ajvOpts.response, + // This has already been converted from boolean if required + this.options.validateResponses as ValidateResponseOpts,) + .getAJV(), + }; }); } diff --git a/test/ajv.return.spec.ts b/test/ajv.return.spec.ts index 16a0a7e8..0ae3d4c8 100644 --- a/test/ajv.return.spec.ts +++ b/test/ajv.return.spec.ts @@ -3,7 +3,8 @@ import { expect } from 'chai'; import { date, dateTime } from '../src/framework/base.serdes'; import * as OpenApiValidator from '../src'; -import { Ajv } from 'ajv'; +import { OpenAPIV3 } from '../src/framework/types'; +import Ajvs = OpenAPIV3.Ajvs; const apiSpecPath = path.join('test', 'resources', 'serdes.yaml'); @@ -20,10 +21,25 @@ class ObjectID { } describe('ajv.return', () => { - let ajv : Ajv = null; + let ajvs : Ajvs = null; + + class ReqRes { + id?: string|ObjectID + } + + const customSchema = { + type: 'object', + properties: { + id: { + $ref: '#/components/schemas/ObjectId', + }, + }, + required: ['id'], + additionalProperties: false, + }; before(async () => { - ajv = await OpenApiValidator.ajv({ + ajvs = await OpenApiValidator.ajv({ apiSpec: apiSpecPath, validateRequests: { coerceTypes: true @@ -44,30 +60,80 @@ describe('ajv.return', () => { }); }); - it('should control BAD id format and throw an error', async () => { - class ReqClass { - id: string|ObjectID - } - - const req : ReqClass = { + it('should control request and deserialize string to object', async () => { + const req : ReqRes = { id : '507f191e810c19729de860ea', } - ajv.validate( - { - type: 'object', - properties: { - id: { - $ref: '#/components/schemas/ObjectId', - }, - }, - required: ['token'], - additionalProperties: false, - }, + const isValid = ajvs.req.validate( + customSchema, req ); + expect(isValid).to.be.equal(true); + expect(ajvs.req.errors).to.be.equal(null); expect(req.id instanceof ObjectID).to.be.true; }); + + it('should control request and return error if id is not set', async () => { + const req : ReqRes = { + // No id but it is required + // id : '507f191e810c19729de860ea', + } + + const isValid = ajvs.req.validate( + customSchema, + req + ); + expect(isValid).to.be.equal(false); + expect(ajvs.req.errors.length).to.be.equal(1); + expect(ajvs.req.errors[0].message).to.be.equal('should have required property \'id\''); + }); + + it('should control request and return error if id is in bad format', async () => { + const req : ReqRes = { + id : 'notAnObjectId', + } + + const isValid = ajvs.req.validate( + customSchema, + req + ); + expect(isValid).to.be.equal(false); + expect(ajvs.req.errors.length).to.be.equal(1); + expect(ajvs.req.errors[0].message).to.be.equal('should match pattern "^[0-9a-fA-F]{24}$"'); + }); + + + it('should control response and serialize object to string', async () => { + const res : ReqRes = { + id : new ObjectID('507f191e810c19729de860ea'), + } + + const isValid = ajvs.res.validate( + customSchema, + res + ); + expect(res.id).to.be.equal('507f191e810c19729de860ea'); + expect(isValid).to.be.equal(true); + expect(ajvs.res.errors).to.be.equal(null); + }); + + it('should control response and return an error if id is not set', async () => { + + const res : ReqRes = { + // No id but it is required + // id : '507f191e810c19729de860ea', + //id : new ObjectID('507f191e810c19729de860ea'), + } + + const isValid = ajvs.res.validate( + customSchema, + res + ); + expect(isValid).to.be.equal(false); + expect(ajvs.res.errors.length).to.be.equal(1); + expect(ajvs.res.errors[0].message).to.be.equal('should have required property \'id\''); + }); });