1- import { Response , ResponseDefinition } from 'postman-collection' ;
1+ import { HeaderDefinition , Response , ResponseDefinition } from 'postman-collection' ;
22
33import { mergeRequestAndResponseBodyRaw } from './BodyMerger' ;
44import { mergeRequestAndResponseHeaders } from './HeaderMerger' ;
@@ -7,24 +7,78 @@ import { mergeRequestData } from './RequestMerger';
77import { SyncOptions } from '../../shared' ;
88import { attachImplicitHeaders } from '../header' ;
99
10+ /**
11+ * Preserve an existing saved response's header PARAMETER values while keeping any
12+ * implicit/generated headers (e.g. Content-Type, Accept) produced from the spec. Used when
13+ * preserving a non-first response's originalRequest on multi-example sync: for a shared key the
14+ * spec-generated header metadata (name casing, description, schema-derived flags) is kept and only
15+ * the existing header's `value`/`disabled` are copied over; generated target-only headers are
16+ * retained, and header params that only exist on the source are appended.
17+ *
18+ * Header keys are matched case-insensitively (HTTP header names are case-insensitive).
19+ * @param {HeaderDefinition[] | undefined } targetHeaders - Spec-generated headers (base)
20+ * @param {HeaderDefinition[] | undefined } sourceHeaders - Existing saved response headers to preserve
21+ * @returns {HeaderDefinition[] | undefined } Merged header list
22+ */
23+ function preserveHeaderParams (
24+ targetHeaders : HeaderDefinition [ ] | undefined ,
25+ sourceHeaders : HeaderDefinition [ ] | undefined
26+ ) : HeaderDefinition [ ] | undefined {
27+ if ( ! Array . isArray ( sourceHeaders ) ) {
28+ return targetHeaders ;
29+ }
30+
31+ const result : HeaderDefinition [ ] = Array . isArray ( targetHeaders ) ? [ ...targetHeaders ] : [ ] ,
32+ indexByLowerKey = new Map < string , number > ( ) ;
33+
34+ result . forEach ( ( header , index ) => {
35+ if ( typeof header ?. key === 'string' ) {
36+ indexByLowerKey . set ( header . key . toLowerCase ( ) , index ) ;
37+ }
38+ } ) ;
39+
40+ sourceHeaders . forEach ( ( sourceHeader ) => {
41+ if ( typeof sourceHeader ?. key !== 'string' ) {
42+ return ;
43+ }
44+
45+ const lowerKey = sourceHeader . key . toLowerCase ( ) ;
46+
47+ if ( indexByLowerKey . has ( lowerKey ) ) {
48+ // Keep the spec-generated header (name casing + metadata); copy only the existing header's
49+ // value/disabled state onto it.
50+ const index = indexByLowerKey . get ( lowerKey ) as number ;
51+
52+ result [ index ] = { ...result [ index ] , value : sourceHeader . value , disabled : sourceHeader . disabled } ;
53+ }
54+ else {
55+ indexByLowerKey . set ( lowerKey , result . length ) ;
56+ result . push ( sourceHeader ) ;
57+ }
58+ } ) ;
59+
60+ return result ;
61+ }
62+
1063/**
1164 * Merges a single response from target with a corresponding response from current.
1265 * Returns the merged response definition.
1366 * @param {Response } targetResponse - Response from the target request
1467 * @param {Response } sourceResponse - Response from the source request
1568 * @param {SyncOptions } syncOptions - Options to control what should be synced
16- * @param {boolean } preserveOriginalRequestBody - When true (and example syncing is enabled), keep
17- * the existing response's `originalRequest.body` instead of overwriting it with the spec's request.
18- * The spec carries a single request example (the live body) that maps to the first saved response;
19- * set this for every response after the first so a request change in the spec doesn't overwrite
20- * every response's originalRequest. Defaults to false (first response takes the spec request).
69+ * @param {boolean } preserveOriginalRequest - When true (and example syncing is enabled), keep the
70+ * existing response's `originalRequest` request-side data (body, url query/path variables and
71+ * headers) instead of overwriting it with the spec's request. The spec carries a single live
72+ * request (body + parameter values) that maps to the first saved response; set this for every
73+ * response after the first so a request/parameter change in the spec doesn't overwrite every
74+ * response's originalRequest. Defaults to false (first response takes the spec request).
2175 * @returns {ResponseDefinition } Merged response definition
2276 */
2377export function mergeResponseData (
2478 targetResponse : Response ,
2579 sourceResponse : Response ,
2680 syncOptions : SyncOptions ,
27- preserveOriginalRequestBody = false
81+ preserveOriginalRequest = false
2882) : ResponseDefinition {
2983 const targetRes : ResponseDefinition = targetResponse . toJSON ( ) ,
3084 sourceRes : ResponseDefinition = sourceResponse . toJSON ( ) ,
@@ -34,15 +88,32 @@ export function mergeResponseData(
3488 targetRes . originalRequest = mergeRequestData ( targetRes . originalRequest , sourceRes . originalRequest , syncOptions ) ;
3589
3690 /*
37- * The spec carries a single request example (the live request body), which maps to the first
38- * saved response. For the remaining responses, preserve their existing request body so that
39- * editing the request in the spec doesn't overwrite every response's originalRequest on sync-back.
91+ * The spec carries a single live request (its body plus the primary parameter values), which
92+ * maps to the first saved response. For the remaining responses, preserve their existing
93+ * request-side data — body, url (query params + path variables) and headers — so that editing
94+ * the request or a parameter in the spec doesn't overwrite every response's originalRequest on
95+ * sync-back. Each component is deleted when the source had none, mirroring the body rule.
4096 * Only applies when example syncing is enabled (the multi-example flow).
4197 */
42- if ( preserveOriginalRequestBody && shouldSyncExamples ) {
43- if ( sourceRes . originalRequest . body === undefined ) { delete targetRes . originalRequest . body ; }
44- else { targetRes . originalRequest . body = sourceRes . originalRequest . body ; }
45- }
98+ if ( preserveOriginalRequest && shouldSyncExamples ) {
99+ // Cast for the delete-when-source-had-none branches (url/body are not always optional in the type).
100+ const targetOriginalRequest = targetRes . originalRequest as unknown as Record < string , unknown > ;
101+
102+ // Body: preserve the existing example's body wholesale (delete when the source had none).
103+ if ( sourceRes . originalRequest . body === undefined ) { delete targetOriginalRequest . body ; }
104+ else { targetRes . originalRequest . body = sourceRes . originalRequest . body ; }
105+
106+ // URL: preserve the existing example's url (query params + path variable values) wholesale.
107+ if ( sourceRes . originalRequest . url === undefined ) { delete targetOriginalRequest . url ; }
108+ else { targetRes . originalRequest . url = sourceRes . originalRequest . url ; }
109+
110+ // Headers: preserve the existing example's header PARAMETER values while keeping any
111+ // implicit/generated headers (e.g. Content-Type, Accept) the spec produced.
112+ targetRes . originalRequest . header = preserveHeaderParams (
113+ targetRes . originalRequest . header as HeaderDefinition [ ] | undefined ,
114+ sourceRes . originalRequest . header as HeaderDefinition [ ] | undefined
115+ ) ;
116+ }
46117 }
47118 // Attach implicit headers from the source response to the target response
48119 // if they are not present in the target response
0 commit comments