import { RequestHandler } from 'express' ; import * as ajv from 'ajv' ; import mung from '../framework/modded.express.mung' ; import { createResponseAjv } from '../framework/ajv' ; import { augmentAjvErrors , ContentType , ajvErrorsToValidatorError , findResponseContent , } from './util' ; import { OpenAPIV3 , OpenApiRequest , OpenApiRequestMetadata , InternalServerError , ValidateResponseOpts , } from '../framework/types' ; import * as mediaTypeParser from 'media-typer' ; import * as contentTypeParser from 'content-type' ; interface ValidateResult { validators : { [ key ] : ajv . ValidateFunction ; } ; body : object ; statusCode : number ; path : string ; accepts : string [ ] ; } export class ResponseValidator { private ajvBody : ajv . Ajv ; private spec : OpenAPIV3 . Document ; private validatorsCache : { [ key ] : { [ key ] : ajv . ValidateFunction ; } ; } = { } ; private eovOptions : ValidateResponseOpts ; constructor ( openApiSpec , options = { } , eovOptions = { } ) { this . spec = openApiSpec ; this . ajvBody = createResponseAjv ( openApiSpec , options ) ; this . eovOptions = eovOptions ; ( < any > mung ) . onError = ( err , req , res , next ) => { return next ( err ) ; } ; } public validate ( ) { return mung . json ( ( body , req , res ) => { if ( req . openapi ) { const openapi = < OpenApiRequestMetadata > req . openapi ; const responses = ( < any > openapi ) . _responseSchema ?. responses ; const validators = this . _getOrBuildValidator ( req , responses ) ; const path = req . originalUrl ; const statusCode = res . statusCode ; const contentType = res . getHeaders ( ) [ 'content-type' ] ; const accept = req . headers [ 'accept' ] ; const accepts = contentType ? [ contentType ] : accept ? accept . split ( ',' ) . map ( ( h ) => h . trim ( ) ) : [ ] ; try { return this . _validate ( { validators , body , statusCode , path , accepts , } ) ; } catch ( err ) { if ( err instanceof InternalServerError && this . eovOptions . onError ) { this . eovOptions . onError ( err , body , req ) ; } else { throw err ; } } } return body ; } ) ; } public _getOrBuildValidator ( req , responses ) { const contentTypeMeta = ContentType . from ( req ) ; const contentType = ( contentTypeMeta . contentType ?. indexOf ( 'multipart' ) > - 1 ? contentTypeMeta . equivalents ( ) [ 0 ] : contentTypeMeta . contentType ) ?? 'not_provided' ; const openapi = < OpenApiRequestMetadata > req . openapi ; const key = `${ req . method }-${ openapi . expressRoute }-${ contentType }` ; let validators = this . validatorsCache [ key ] ; if ( ! validators ) { validators = this . buildValidators ( responses ) ; this . validatorsCache [ key ] = validators ; } return validators ; } public _validate ( { validators , body , statusCode , path , accepts , } ) { const status = statusCode ?? 'default' ; const statusXX = status . toString ( ) [ 0 ] + 'XX' ; let svalidator ; if ( status in validators ) { svalidator = validators [ status ] ; } else if ( statusXX in validators ) { svalidator = validators [ statusXX ] ; } else if ( validators . default ) { svalidator = validators . default ; } else { throw new InternalServerError ( { path : path , message : `no schema defined for status code '${ status }' in the openapi spec` , } ) ; } const validatorContentTypes = Object . keys ( svalidator ) ; const contentType = findResponseContent ( accepts , validatorContentTypes ) || validatorContentTypes [ 0 ] ; if ( validatorContentTypes . length === 0 ) { if ( body !== undefined ) { throw new InternalServerError ( { path : '.response' , message : 'response should NOT have a body' , } ) ; } return ; } if ( ! contentType ) { console . warn ( 'no contentType found' ) ; return ; } const validator = svalidator [ contentType ] ; if ( ! validator ) { console . warn ( 'no validator found' ) ; return ; } if ( body === undefined || body === null ) { throw new InternalServerError ( { path : '.response' , message : 'response body required.' , } ) ; } try { if ( ! this . canValidateContentType ( contentType ) ) { console . warn ( 'Cannot validate content type' , contentType ) ; return ; } } catch ( e ) { } const valid = validator ( { response : body , } ) ; if ( ! valid ) { const errors = augmentAjvErrors ( validator . errors ) ; const message = this . ajvBody . errorsText ( errors , { dataVar : '' , } ) ; throw new InternalServerError ( { path : path , errors : ajvErrorsToValidatorError ( 500 , errors ) . errors , message : message , } ) ; } } param responses returns private buildValidators ( responses ) { const validationTypes = ( response ) => { if ( ! response . content ) { return [ 'no_content' ] ; } if ( typeof response . content !== 'object' ) { return [ ] ; } const types = [ ] ; for ( let contentType of Object . keys ( response . content ) ) { try { if ( this . canValidateContentType ( contentType ) ) { if ( response . content [ contentType ] && response . content [ contentType ] . schema ) { types . push ( contentType ) ; } } } catch ( e ) { if ( response . content [ contentType ] . schema && ( contentType === '*/*' || new RegExp ( /^[a-z]+\/\*$/ ) . test ( contentType ) ) ) { types . push ( contentType ) ; } } } return types ; } ; const responseSchemas = { } ; for ( const [ name , resp ] of < any [ ] > Object . entries ( responses ) ) { let tmpResponse = resp ; if ( tmpResponse . $ref ) { const id = tmpResponse . $ref . replace ( /^.+\//i , '' ) ; tmpResponse = this . spec . components ?. responses ?. [ id ] ; } const response = tmpResponse ; const types = validationTypes ( response ) ; for ( const mediaTypeToValidate of types ) { if ( ! mediaTypeToValidate ) { continue ; } else if ( mediaTypeToValidate === 'no_content' ) { responseSchemas [ name ] = { } ; continue ; } const schema = response . content [ mediaTypeToValidate ] . schema ; responseSchemas [ name ] = { ... responseSchemas [ name ] , [ mediaTypeToValidate ] : { type : 'object' , properties : { response : schema , } , components : this . spec . components ?? { } , } , } ; } } const validators = { } ; for ( const [ code , contentTypeSchemas ] of Object . entries ( responseSchemas ) ) { if ( Object . keys ( contentTypeSchemas ) . length === 0 ) { validators [ code ] = { } ; } for ( const contentType of Object . keys ( contentTypeSchemas ) ) { const schema = contentTypeSchemas [ contentType ] ; schema . paths = this . spec . paths ; schema . components = this . spec . components ; validators [ code ] = { ... validators [ code ] , [ contentType ] : this . ajvBody . compile ( < object > schema ) , } ; } } return validators ; } param contentType returns throws private canValidateContentType ( contentType ) { const contentTypeParsed = contentTypeParser . parse ( contentType ) ; const mediaTypeParsed = mediaTypeParser . parse ( contentTypeParsed . type ) ; return ( mediaTypeParsed . subtype === 'json' || mediaTypeParsed . suffix === 'json' ) ; } }