diff --git a/README.md b/README.md index 339a43fb..66aacd16 100644 --- a/README.md +++ b/README.md @@ -774,7 +774,7 @@ To create custom serializers and/or deserializers, define: e.g. ```javascript -serDes: [{ +serDes: [ // installs dateTime serializer and deserializer OpenApiValidator.serdes.dateTime, // installs date serializer and deserializer @@ -784,8 +784,8 @@ serDes: [{ format: 'mongo-objectid', deserialize: (s) => new ObjectID(s), serialize: (o) => o.toString(), - } -}], + }, +], ``` The mongo serializers will trigger on the following schema: @@ -1148,6 +1148,59 @@ function routesV2(app) { module.exports = app; ``` +## Use OpenAPIValidator AJV out of express usage + +You can get an AJV module as OpenAPIValidator generates it for express. +Then you can use it for other usages such as websocket request validation... +Instead of initialize OpenApiValidator with middleware, you can get a configured AJV object with all OpenApiValidator mecanisms (serdes...) and loaded schemas. + + +```javascript +const ajvs = await OpenApiValidator.ajv({ + apiSpec: './openapi.yaml', + validateRequests: true, // (default) + validateResponses: true, // false by default +}); + +const customObj = { + id : '507f191e810c19729de860ea', +} + +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: { + id: { + $ref: '#/components/schemas/ObjectId', + }, + }, + required: ['token'], + additionalProperties: false, + }, + customObj +); + +// isResValid = true +// No error in ajvs.res.errors +``` ## FAQ diff --git a/src/framework/ajv/index.ts b/src/framework/ajv/index.ts index 75660e19..ca879d40 100644 --- a/src/framework/ajv/index.ts +++ b/src/framework/ajv/index.ts @@ -55,7 +55,21 @@ function createAjv( ]; return false; } - obj[propName] = sch.deserialize(data); + try { + obj[propName] = sch.deserialize(data); + } + catch(e) { + (validate).errors = [ + { + keyword: 'serdes', + schemaPath: data, + dataPath: path, + message: `format is invalid`, + params: { 'x-eov-serdes': propName }, + }, + ]; + return false; + } } return true; }; @@ -99,7 +113,21 @@ function createAjv( return function validate(data, path, obj, propName) { if (typeof data === 'string') return true; if (!!sch.serialize) { - obj[propName] = sch.serialize(data); + try { + obj[propName] = sch.serialize(data); + } + catch(e) { + (validate).errors = [ + { + keyword: 'serdes', + schemaPath: data, + dataPath: path, + message: `format is invalid`, + params: { 'x-eov-serdes': propName }, + }, + ]; + return false; + } } return true; }; 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/index.ts b/src/index.ts index 60a8b949..74de62d2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,6 +17,7 @@ import { // export default openapiValidator; export const resolvers = res; export const middleware = openapiValidator; +export const ajv = ajvInstances; export const error = { InternalServerError, UnsupportedMediaType, @@ -43,3 +44,16 @@ function openapiValidator(options: OpenApiValidatorOpts) { }).load(), ); } + +function ajvInstances(options: OpenApiValidatorOpts) { + const oav = new OpenApiValidator(options); + exports.middleware._oav = oav; + + return oav.installAjv( + new OpenApiSpecLoader({ + apiDoc: cloneDeep(options.apiSpec), + validateApiSpec: options.validateApiSpec, + $refParser: options.$refParser, + }).load(), + ); +} diff --git a/src/middlewares/openapi.request.validator.ts b/src/middlewares/openapi.request.validator.ts index 5ce1c29c..95e94c8d 100644 --- a/src/middlewares/openapi.request.validator.ts +++ b/src/middlewares/openapi.request.validator.ts @@ -227,6 +227,10 @@ export class RequestValidator { } } } + + public getAJV () : Ajv { + return this.ajv; + } } class Validator { 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 db1a3f3d..665fb156 100644 --- a/src/openapi.validator.ts +++ b/src/openapi.validator.ts @@ -20,6 +20,8 @@ import { OperationHandlerOptions } from './framework/types'; 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, @@ -90,6 +92,29 @@ export class OpenApiValidator { this.ajvOpts = new AjvOptions(options); } + installAjv(spec: Promise): Promise { + return spec + .then((spec) => { + const apiDoc = spec.apiDoc; + const ajvOpts = this.ajvOpts.preprocessor; + const resOpts = this.options.validateResponses as ValidateRequestOpts; + const sp = new SchemaPreprocessor( + apiDoc, + ajvOpts, + resOpts, + ).preProcess(); + return { + req : new middlewares.RequestValidator(apiDoc, this.ajvOpts.request).getAJV(), + res : new middlewares.ResponseValidator( + apiDoc, + this.ajvOpts.response, + this.options.validateResponses as ValidateResponseOpts,) + .getAJV(), + }; + }); + } + + installMiddleware(spec: Promise): OpenApiRequestHandler[] { const middlewares: OpenApiRequestHandler[] = []; const pContext = spec diff --git a/test/ajv.return.spec.ts b/test/ajv.return.spec.ts new file mode 100644 index 00000000..0ae3d4c8 --- /dev/null +++ b/test/ajv.return.spec.ts @@ -0,0 +1,140 @@ +import * as path from 'path'; +import { expect } from 'chai'; + +import { date, dateTime } from '../src/framework/base.serdes'; +import * as OpenApiValidator from '../src'; +import { OpenAPIV3 } from '../src/framework/types'; +import Ajvs = OpenAPIV3.Ajvs; + +const apiSpecPath = path.join('test', 'resources', 'serdes.yaml'); + +class ObjectID { + id: string; + + constructor(id: string = "5fdefd13a6640bb5fb5fa925") { + this.id = id; + } + + toString() { + return this.id; + } +} + +describe('ajv.return', () => { + 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 () => { + ajvs = await OpenApiValidator.ajv({ + apiSpec: apiSpecPath, + validateRequests: { + coerceTypes: true + }, + validateResponses: { + coerceTypes: true + }, + serDes: [ + date, + dateTime, + { + format: "mongo-objectid", + deserialize: (s) => new ObjectID(s), + serialize: (o) => o.toString(), + }, + ], + unknownFormats: ['string-list'], + }); + }); + + it('should control request and deserialize string to object', async () => { + const req : ReqRes = { + id : '507f191e810c19729de860ea', + } + + 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\''); + }); +}); + + + diff --git a/test/serdes.spec.ts b/test/serdes.spec.ts index 3f822f8f..0260326d 100644 --- a/test/serdes.spec.ts +++ b/test/serdes.spec.ts @@ -307,13 +307,13 @@ describe('serdes serialize response components only', () => { .expect(500) .then((r) => { console.log(r); - expect(r.body.message).to.equal('d.toISOString is not a function'); + expect(r.body.message).to.equal('.response.creationDate format is invalid'); })); /* FIXME Manage format validation after serialize ? I can serialize using a working serialize method but that respond a bad format it('should throw error 500 on an object that serialize to a bad string format', async () => - + request(app) .get(`${app.basePath}/users/5fdefd13a6640bb5fb5fa925`) .query({baddateresponse : 'functionBadFormat'}) @@ -322,7 +322,7 @@ describe('serdes serialize response components only', () => { console.log(r.body); expect(r.body.message).to.equal('Something saying that date is not date-time format'); })); - + */ });