@@ -100,7 +100,6 @@ export class RequestParameterMutator {
100100 this . handleContent ( req , name , parameter ) ;
101101 } else if ( parameter . in === 'query' && this . isObjectOrXOf ( schema ) ) {
102102 // handle bracket notation and mutates query param
103-
104103
105104 if ( style === 'form' && explode ) {
106105 this . parseJsonAndMutateRequest ( req , parameter . in , name ) ;
@@ -119,7 +118,7 @@ export class RequestParameterMutator {
119118 this . parseJsonAndMutateRequest ( req , parameter . in , name ) ;
120119 } else {
121120 this . parseJsonAndMutateRequest ( req , parameter . in , name ) ;
122- }
121+ }
123122 } else if ( type === 'array' && ! explode ) {
124123 const delimiter = ARRAY_DELIMITER [ parameter . style ] ;
125124 this . validateArrayDelimiter ( delimiter , parameter ) ;
@@ -428,78 +427,153 @@ export class RequestParameterMutator {
428427 } , new Map < string , string [ ] > ( ) ) ;
429428 }
430429
431- private csvToKeyValuePairs ( csvString : string ) : Record < string , string > | undefined {
430+ private csvToKeyValuePairs (
431+ csvString : string ,
432+ ) : Record < string , string > | undefined {
432433 const hasBrace = csvString . split ( '{' ) . length > 1 ;
433434 const items = csvString . split ( ',' ) ;
434-
435+
435436 if ( hasBrace ) {
436437 // if it has a brace, we assume its JSON and skip creating k v pairs
437438 // TODO improve json check, but ensure its cheap
438439 return ;
439440 }
440-
441+
441442 if ( items . length % 2 !== 0 ) {
442- // if the number of elements is not event,
443+ // if the number of elements is not event,
443444 // then we do not have k v pairs, so return undefined
444445 return ;
445446 }
446447
447448 const result = { } ;
448-
449+
449450 for ( let i = 0 ; i < items . length - 1 ; i += 2 ) {
450451 result [ items [ i ] ] = items [ i + 1 ] ;
451452 }
452-
453+
453454 return result ;
454455 }
455456
456457 /**
457458 * Handles query parameters with bracket notation.
458- * - If the parameter in the OpenAPI spec has literal brackets in its name (e.g., 'filter[name]'),
459- * it will be treated as a literal parameter name.
460- * - Otherwise, it will be parsed as a nested object using qs.
461459 * @param query The query parameters object to process
462460 * @returns The processed query parameters object
463461 */
464462 private handleBracketNotationQueryFields ( query : { [ key : string ] : any } ) : {
465463 [ key : string ] : any ;
466464 } {
467- // Get the OpenAPI parameters for the current request
468- const openApiParams = ( query . _openapi ?. schema ?. parameters || [ ] ) as ParameterObject [ ] ;
469-
470- // Create a Set of parameter names that have literal brackets in the spec
471- const literalBracketParams = new Set < string > (
472- openApiParams
473- . filter ( p => p . in === 'query' && p . name . includes ( '[' ) && p . name . endsWith ( ']' ) )
474- . map ( p => p . name )
475- ) ;
465+ const handler = new BracketNotationHandler ( query ) ;
466+ return handler . process ( ) ;
467+ }
468+ }
469+
470+ /**
471+ * Handles parsing of query parameters with bracket notation.
472+ * - If a parameter in the OpenAPI spec has literal brackets in its name (e.g., 'filter[name]'),
473+ * it will be treated as a literal parameter name.
474+ * - Otherwise, it will be parsed as a nested object using qs.
475+ */
476+ class BracketNotationHandler {
477+ // Cache for literal bracket parameters per endpoint
478+ private static readonly literalBracketParamsCache = new Map <
479+ string ,
480+ Set < string >
481+ > ( ) ;
482+
483+ constructor ( private readonly query : { [ key : string ] : any } ) { }
484+
485+ /**
486+ * Process the query parameters to handle bracket notation
487+ */
488+ public process ( ) : { [ key : string ] : any } {
489+ const literalBracketParams = this . getLiteralBracketParams ( ) ;
476490
477- // Create a new object to avoid mutating the original during iteration
478- const result : { [ key : string ] : any } = { ...query } ;
479-
491+ const query = this . query ;
480492 Object . keys ( query ) . forEach ( ( key ) => {
481493 // Only process keys that contain brackets
482- if ( key . includes ( '[' ) && key . endsWith ( ']' ) ) {
483- if ( literalBracketParams . has ( key ) ) {
484- // If the parameter is defined with literal brackets in the spec, preserve it as-is
485- result [ key ] = query [ key ] ;
486- } else {
487- // Otherwise, use qs.parse to handle it as a nested object
494+ if ( key . includes ( '[' ) ) {
495+ // Only process keys that do not contain literal bracket notation
496+ if ( ! literalBracketParams . has ( key ) ) {
497+ // Use qs.parse to handle it as a nested object
488498 const normalizedKey = key . split ( '[' ) [ 0 ] ;
489499 const parsed = parse ( `${ key } =${ query [ key ] } ` ) ;
490-
500+
491501 // Use the parsed value for the normalized key
492502 if ( parsed [ normalizedKey ] !== undefined ) {
493- result [ normalizedKey ] = parsed [ normalizedKey ] ;
503+ // If we already have a value for this key, merge the objects
504+ if (
505+ query [ normalizedKey ] &&
506+ typeof query [ normalizedKey ] === 'object' &&
507+ typeof parsed [ normalizedKey ] === 'object' &&
508+ ! Array . isArray ( parsed [ normalizedKey ] )
509+ ) {
510+ query [ normalizedKey ] = {
511+ ...query [ normalizedKey ] ,
512+ ...parsed [ normalizedKey ] ,
513+ } ;
514+ } else {
515+ query [ normalizedKey ] = parsed [ normalizedKey ] ;
516+ }
494517 }
495-
496- // Remove the original bracketed key
497- delete result [ key ] ;
518+
519+ // Remove the original bracketed key from the query
520+ delete query [ key ] ;
498521 }
499522 }
500523 } ) ;
501-
502- return result ;
524+
525+ return query ;
526+ }
527+
528+ /**
529+ * Gets a cache key for the current request's OpenAPI schema
530+ * Combines path, method, and operation ID to create a key
531+ */
532+ private getCacheKey ( ) : string | null {
533+ const schema = this . query . _openapi ?. schema ;
534+ if ( ! schema ) return null ;
535+
536+ // Use all available identifiers to ensure uniqueness
537+ const path = schema . path ?? '' ;
538+ const method = schema . method ?? '' ;
539+ const operationId = schema . operationId ?? '' ;
540+
541+ // Combine all parts with a consistent separator
542+ return `${ path } |${ method } |${ operationId } ` ;
543+ }
544+
545+ /**
546+ * Gets the set of parameter names that should be treated as literal bracket notation
547+ */
548+ private getLiteralBracketParams ( ) : Set < string > {
549+ const cacheKey = this . getCacheKey ( ) ;
550+
551+ if (
552+ cacheKey &&
553+ BracketNotationHandler . literalBracketParamsCache . has ( cacheKey )
554+ ) {
555+ return BracketNotationHandler . literalBracketParamsCache . get ( cacheKey ) ! ;
556+ }
557+
558+ // Get the OpenAPI parameters for the current request
559+ const openApiParams = ( this . query . _openapi ?. schema ?. parameters ||
560+ [ ] ) as ParameterObject [ ] ;
561+
562+ // Create a Set of parameter names that have literal brackets in the spec
563+ const literalBracketParams = new Set < string > (
564+ openApiParams
565+ . filter ( ( p ) => p . in === 'query' && p . name . includes ( '[' ) )
566+ . map ( ( p ) => p . name ) ,
567+ ) ;
568+
569+ // Cache the result for future requests to this endpoint
570+ if ( cacheKey ) {
571+ BracketNotationHandler . literalBracketParamsCache . set (
572+ cacheKey ,
573+ literalBracketParams ,
574+ ) ;
575+ }
576+
577+ return literalBracketParams ;
503578 }
504-
505579}
0 commit comments