@@ -4,6 +4,7 @@ import Ajv, { ErrorObject, ValidateFunction } from 'ajv'
44import OpenapiRequestCoercer from 'openapi-request-coercer'
55import { Logger , dummyLogger } from 'ts-log'
66import { OpenAPIV3 } from 'openapi-types'
7+ import { merge , openApiMergeRules } from 'allof-merge'
78import {
89 convertDatesToISOString ,
910 ErrorObj ,
@@ -19,7 +20,6 @@ import {
1920 unserializeParameters ,
2021 STRICT_COERCION_STRATEGY ,
2122} from './openapi-validator'
22- import { merge , openApiMergeRules } from 'allof-merge'
2323
2424const REQ_BODY_COMPONENT_PREFIX_LENGTH = 27 // #/components/requestBodies/PetBody
2525const PARAMS_COMPONENT_PREFIX_LENGH = 24 // #/components/parameters/offsetParam
@@ -99,18 +99,30 @@ export class AjvOpenApiValidator {
9999 validatorOpts ?: ValidatorOpts
100100 ) {
101101 this . validatorOpts = validatorOpts ? { ...DEFAULT_VALIDATOR_OPTS , ...validatorOpts } : DEFAULT_VALIDATOR_OPTS
102- if ( this . validatorOpts . logger == undefined ) {
102+ if ( this . validatorOpts . logger === undefined ) {
103103 this . validatorOpts . logger = dummyLogger
104104 }
105105
106106 this . initialize ( spec , this . validatorOpts . coerceTypes )
107107 }
108108
109+ /**
110+ * Validates query parameters against the specification. Unless otherwise configured, parameters are coerced to the schema's type.
111+ *
112+ * @param path - The path of the request
113+ * @param method - The HTTP method of the request
114+ * @param origParams - The query parameters to validate
115+ * @param strict - If true, parameters not defined in the specification will cause a validation error
116+ * @param strictExclusions - An array of query parameters to exclude from strict mode
117+ * @param logger - A logger instance
118+ * @returns An object containing the normalized query parameters and an array of validation errors
119+ */
109120 validateQueryParams (
110121 path : string ,
111122 method : string ,
112123 origParams : Record < string , Primitive > | URLSearchParams ,
113124 strict = true ,
125+ strictExclusions : string [ ] = [ ] ,
114126 logger ?: Logger
115127 ) : { normalizedParams : Record < string , Primitive > ; errors : ErrorObj [ ] | undefined } {
116128 const parameterDefinitions = this . paramsValidators . filter ( ( p ) => p . path === path ?. toLowerCase ( ) && p . method === method ?. toLowerCase ( ) )
@@ -138,45 +150,47 @@ export class AjvOpenApiValidator {
138150 }
139151
140152 for ( const key in params ) {
141- const value = params [ key ]
142- const paramDefinitionIndex = parameterDefinitions . findIndex ( ( p ) => p . param . name === key ?. toLowerCase ( ) )
143- if ( paramDefinitionIndex < 0 ) {
144- if ( strict ) {
145- errResponse . push ( {
146- status : HttpStatus . BAD_REQUEST ,
147- code : `${ EC_VALIDATION } -invalid-query-parameter` ,
148- title : 'The query parameter is not supported.' ,
149- source : {
150- parameter : key ,
151- } ,
152- } )
153+ if ( Object . prototype . hasOwnProperty . call ( params , key ) ) {
154+ const value = params [ key ]
155+ const paramDefinitionIndex = parameterDefinitions . findIndex ( ( p ) => p . param . name === key ?. toLowerCase ( ) )
156+ if ( paramDefinitionIndex < 0 ) {
157+ if ( strict && ( ! Array . isArray ( strictExclusions ) || ! strictExclusions . includes ( key ) ) ) {
158+ errResponse . push ( {
159+ status : HttpStatus . BAD_REQUEST ,
160+ code : `${ EC_VALIDATION } -invalid-query-parameter` ,
161+ title : 'The query parameter is not supported.' ,
162+ source : {
163+ parameter : key ,
164+ } ,
165+ } )
166+ } else {
167+ log . debug ( `Query parameter '${ key } ' not specified and strict mode is disabled -> ignoring it (${ method } ${ path } )` )
168+ }
153169 } else {
154- log . debug ( `Query parameter '${ key } ' not specified and strict mode is disabled -> ignoring it (${ method } ${ path } )` )
155- }
156- } else {
157- const paramDefinition = parameterDefinitions . splice ( paramDefinitionIndex , 1 ) [ 0 ]
170+ const paramDefinition = parameterDefinitions . splice ( paramDefinitionIndex , 1 ) [ 0 ]
158171
159- const rejectEmptyValues = ! ( paramDefinition . param . allowEmptyValue === true )
160- if ( rejectEmptyValues && ( value === undefined || value === null || String ( value ) . trim ( ) === '' ) ) {
161- errResponse . push ( {
162- status : HttpStatus . BAD_REQUEST ,
163- code : `${ EC_VALIDATION } -query-parameter` ,
164- title : 'The query parameter must not be empty.' ,
165- source : {
166- parameter : key ,
167- } ,
168- } )
169- } else {
170- const validator = paramDefinition . validator
171- if ( ! validator ) {
172- throw new Error ( 'The validator needs to be iniatialized first' )
173- }
172+ const rejectEmptyValues = ! ( paramDefinition . param . allowEmptyValue === true )
173+ if ( rejectEmptyValues && ( value === undefined || value === null || String ( value ) . trim ( ) === '' ) ) {
174+ errResponse . push ( {
175+ status : HttpStatus . BAD_REQUEST ,
176+ code : `${ EC_VALIDATION } -query-parameter` ,
177+ title : 'The query parameter must not be empty.' ,
178+ source : {
179+ parameter : key ,
180+ } ,
181+ } )
182+ } else {
183+ const validator = paramDefinition . validator
184+ if ( ! validator ) {
185+ throw new Error ( 'The validator needs to be iniatialized first' )
186+ }
174187
175- const res = validator ( value )
188+ const res = validator ( value )
176189
177- if ( ! res ) {
178- const validationErrors = mapValidatorErrors ( validator . errors , HttpStatus . BAD_REQUEST )
179- errResponse = errResponse . concat ( validationErrors )
190+ if ( ! res ) {
191+ const validationErrors = mapValidatorErrors ( validator . errors , HttpStatus . BAD_REQUEST )
192+ errResponse = errResponse . concat ( validationErrors )
193+ }
180194 }
181195 }
182196 }
@@ -200,6 +214,16 @@ export class AjvOpenApiValidator {
200214 return { normalizedParams : params , errors : errResponse . length ? errResponse : undefined }
201215 }
202216
217+ /**
218+ * Validates the request body against the specification.
219+ *
220+ * @param path - The path of the request
221+ * @param method - The HTTP method of the request
222+ * @param data - The request body to validate
223+ * @param strict - If true and a request body is present, then there must be a request body defined in the specification for validation to continue
224+ * @param logger - A logger
225+ * @returns - An array of validation errors
226+ */
203227 validateRequestBody ( path : string , method : string , data : unknown , strict = true , logger ?: Logger ) : ErrorObj [ ] | undefined {
204228 const validator = this . requestBodyValidators . find ( ( v ) => v . path === path ?. toLowerCase ( ) && v . method === method ?. toLowerCase ( ) )
205229 const log = logger ? logger : this . validatorOpts . logger
@@ -233,6 +257,17 @@ export class AjvOpenApiValidator {
233257 return undefined
234258 }
235259
260+ /**
261+ * Validates the response body against the specification.
262+ *
263+ * @param path - The path of the request
264+ * @param method - The HTTP method of the request
265+ * @param status - The HTTP status code of the response
266+ * @param data - The response body to validate
267+ * @param strict - If true and a response body is present, then there must be a response body defined in the specification for validation to continue
268+ * @param logger - A logger
269+ * @returns - An array of validation errors
270+ */
236271 validateResponseBody (
237272 path : string ,
238273 method : string ,
@@ -282,7 +317,7 @@ export class AjvOpenApiValidator {
282317 if ( hasComponentSchemas ( spec ) ) {
283318 Object . keys ( spec . components . schemas ) . forEach ( ( key ) => {
284319 const schema = spec . components . schemas [ key ]
285- if ( this . validatorOpts . setAdditionalPropertiesToFalse === true ) {
320+ if ( this . validatorOpts . setAdditionalPropertiesToFalse ) {
286321 if ( ! isValidReferenceObject ( schema ) && schema . additionalProperties === undefined && schema . discriminator === undefined ) {
287322 schema . additionalProperties = false
288323 }
@@ -359,22 +394,22 @@ export class AjvOpenApiValidator {
359394 path : path . toLowerCase ( ) ,
360395 method : method . toLowerCase ( ) as string ,
361396 validator,
362- required : required ,
397+ required,
363398 } )
364399 }
365400 }
366401
367402 if ( operation . responses ) {
368403 Object . keys ( operation . responses ) . forEach ( ( key ) => {
369- const response = operation . responses [ key ]
404+ const opResponse = operation . responses [ key ]
370405
371406 let schema : OpenAPIV3 . SchemaObject | OpenAPIV3 . ReferenceObject | undefined
372407
373- if ( isValidReferenceObject ( response ) ) {
408+ if ( isValidReferenceObject ( opResponse ) ) {
374409 let errorStr : string | undefined
375410
376- if ( response . $ref . length > RESPONSE_COMPONENT_PREFIX_LENGTH ) {
377- const respName = response . $ref . substring ( RESPONSE_COMPONENT_PREFIX_LENGTH )
411+ if ( opResponse . $ref . length > RESPONSE_COMPONENT_PREFIX_LENGTH ) {
412+ const respName = opResponse . $ref . substring ( RESPONSE_COMPONENT_PREFIX_LENGTH )
378413 if ( spec . components ?. responses && spec . components ?. responses [ respName ] ) {
379414 const response = spec . components ?. responses [ respName ]
380415 if ( ! isValidReferenceObject ( response ) ) {
@@ -384,10 +419,10 @@ export class AjvOpenApiValidator {
384419 errorStr = `A reference was not expected here: '${ response . $ref } '`
385420 }
386421 } else {
387- errorStr = `Unable to find response reference '${ response . $ref } '`
422+ errorStr = `Unable to find response reference '${ opResponse . $ref } '`
388423 }
389424 } else {
390- errorStr = `Unable to follow response reference '${ response . $ref } '`
425+ errorStr = `Unable to follow response reference '${ opResponse . $ref } '`
391426 }
392427 if ( errorStr ) {
393428 if ( this . validatorOpts . strict ) {
@@ -396,8 +431,8 @@ export class AjvOpenApiValidator {
396431 this . validatorOpts . logger . warn ( errorStr )
397432 }
398433 }
399- } else if ( response . content ) {
400- schema = this . getJsonContent ( response . content ) ?. schema
434+ } else if ( opResponse . content ) {
435+ schema = this . getJsonContent ( opResponse . content ) ?. schema
401436 }
402437
403438 if ( schema ) {
@@ -509,7 +544,7 @@ export class AjvOpenApiValidator {
509544 } else if ( content [ 'application/json; charset=utf-8' ] ) {
510545 return content [ 'application/json; charset=utf-8' ]
511546 } else {
512- const key = Object . keys ( content ) . find ( ( key ) => key . toLowerCase ( ) . startsWith ( 'application/json;' ) )
547+ const key = Object . keys ( content ) . find ( ( k ) => k . toLowerCase ( ) . startsWith ( 'application/json;' ) )
513548 return key ? content [ key ] : undefined
514549 }
515550 }
@@ -519,7 +554,9 @@ export class AjvOpenApiValidator {
519554
520555 // eslint-disable-next-line @typescript-eslint/no-explicit-any
521556 return pathParts . reduce ( ( current : any , part ) => {
522- if ( part === '#' || part === '' ) return current
557+ if ( part === '#' || part === '' ) {
558+ return current
559+ }
523560 return current ? current [ part ] : undefined
524561 } , document )
525562 }
0 commit comments