@@ -100,7 +100,6 @@ export class RequestParameterMutator {
100
100
this . handleContent ( req , name , parameter ) ;
101
101
} else if ( parameter . in === 'query' && this . isObjectOrXOf ( schema ) ) {
102
102
// handle bracket notation and mutates query param
103
-
104
103
105
104
if ( style === 'form' && explode ) {
106
105
this . parseJsonAndMutateRequest ( req , parameter . in , name ) ;
@@ -119,7 +118,7 @@ export class RequestParameterMutator {
119
118
this . parseJsonAndMutateRequest ( req , parameter . in , name ) ;
120
119
} else {
121
120
this . parseJsonAndMutateRequest ( req , parameter . in , name ) ;
122
- }
121
+ }
123
122
} else if ( type === 'array' && ! explode ) {
124
123
const delimiter = ARRAY_DELIMITER [ parameter . style ] ;
125
124
this . validateArrayDelimiter ( delimiter , parameter ) ;
@@ -428,78 +427,153 @@ export class RequestParameterMutator {
428
427
} , new Map < string , string [ ] > ( ) ) ;
429
428
}
430
429
431
- private csvToKeyValuePairs ( csvString : string ) : Record < string , string > | undefined {
430
+ private csvToKeyValuePairs (
431
+ csvString : string ,
432
+ ) : Record < string , string > | undefined {
432
433
const hasBrace = csvString . split ( '{' ) . length > 1 ;
433
434
const items = csvString . split ( ',' ) ;
434
-
435
+
435
436
if ( hasBrace ) {
436
437
// if it has a brace, we assume its JSON and skip creating k v pairs
437
438
// TODO improve json check, but ensure its cheap
438
439
return ;
439
440
}
440
-
441
+
441
442
if ( items . length % 2 !== 0 ) {
442
- // if the number of elements is not event,
443
+ // if the number of elements is not event,
443
444
// then we do not have k v pairs, so return undefined
444
445
return ;
445
446
}
446
447
447
448
const result = { } ;
448
-
449
+
449
450
for ( let i = 0 ; i < items . length - 1 ; i += 2 ) {
450
451
result [ items [ i ] ] = items [ i + 1 ] ;
451
452
}
452
-
453
+
453
454
return result ;
454
455
}
455
456
456
457
/**
457
458
* 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.
461
459
* @param query The query parameters object to process
462
460
* @returns The processed query parameters object
463
461
*/
464
462
private handleBracketNotationQueryFields ( query : { [ key : string ] : any } ) : {
465
463
[ key : string ] : any ;
466
464
} {
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 ( ) ;
476
490
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 ;
480
492
Object . keys ( query ) . forEach ( ( key ) => {
481
493
// 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
488
498
const normalizedKey = key . split ( '[' ) [ 0 ] ;
489
499
const parsed = parse ( `${ key } =${ query [ key ] } ` ) ;
490
-
500
+
491
501
// Use the parsed value for the normalized key
492
502
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
+ }
494
517
}
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 ] ;
498
521
}
499
522
}
500
523
} ) ;
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 ;
503
578
}
504
-
505
579
}
0 commit comments