@scalar/postman-to-openapi 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +15 -0
  3. package/dist/convert.d.ts +14 -0
  4. package/dist/convert.d.ts.map +1 -1
  5. package/dist/convert.js +392 -44
  6. package/dist/helpers/header-utils.d.ts +21 -0
  7. package/dist/helpers/header-utils.d.ts.map +1 -0
  8. package/dist/helpers/header-utils.js +42 -0
  9. package/dist/helpers/merge-operation.d.ts.map +1 -1
  10. package/dist/helpers/merge-operation.js +120 -5
  11. package/dist/helpers/merge-path-item.d.ts.map +1 -1
  12. package/dist/helpers/merge-path-item.js +28 -4
  13. package/dist/helpers/parameters.d.ts +4 -1
  14. package/dist/helpers/parameters.d.ts.map +1 -1
  15. package/dist/helpers/parameters.js +40 -1
  16. package/dist/helpers/path-items.d.ts +11 -1
  17. package/dist/helpers/path-items.d.ts.map +1 -1
  18. package/dist/helpers/path-items.js +111 -11
  19. package/dist/helpers/request-body.d.ts +5 -1
  20. package/dist/helpers/request-body.d.ts.map +1 -1
  21. package/dist/helpers/request-body.js +21 -28
  22. package/dist/helpers/responses.d.ts +8 -1
  23. package/dist/helpers/responses.d.ts.map +1 -1
  24. package/dist/helpers/responses.js +99 -11
  25. package/dist/helpers/servers.d.ts.map +1 -1
  26. package/dist/helpers/servers.js +10 -6
  27. package/dist/helpers/urls.d.ts +20 -0
  28. package/dist/helpers/urls.d.ts.map +1 -1
  29. package/dist/helpers/urls.js +101 -3
  30. package/dist/index.d.ts +2 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +1 -0
  33. package/dist/is-postman-collection.d.ts +9 -0
  34. package/dist/is-postman-collection.d.ts.map +1 -0
  35. package/dist/is-postman-collection.js +23 -0
  36. package/dist/types.d.ts +1 -0
  37. package/dist/types.d.ts.map +1 -1
  38. package/package.json +6 -5
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Looks up a header value case-insensitively. Returns the value of the first
3
+ * non-disabled match, or undefined. Silently ignores string-typed headers
4
+ * (Postman occasionally stores them that way).
5
+ */
6
+ export function readHeader(headers, name) {
7
+ if (!headers || typeof headers === 'string' || !Array.isArray(headers)) {
8
+ return undefined;
9
+ }
10
+ const target = name.toLowerCase();
11
+ const match = headers.find((h) => h.key?.toLowerCase() === target && !h.disabled);
12
+ return match?.value;
13
+ }
14
+ /**
15
+ * Normalises a raw Content-Type header value (`application/json; charset=utf-8`)
16
+ * to just the media type (`application/json`). Returns undefined for empty input.
17
+ */
18
+ export function parseMediaType(value) {
19
+ if (!value) {
20
+ return undefined;
21
+ }
22
+ const trimmed = value.split(';')[0]?.trim().toLowerCase();
23
+ return trimmed || undefined;
24
+ }
25
+ /**
26
+ * Picks a single media type from an Accept header value, preferring
27
+ * `application/json` if present, else the first non-wildcard type. Returns
28
+ * undefined for `*​/*` alone, empty input, or only wildcard types.
29
+ */
30
+ export function pickAcceptMediaType(value) {
31
+ if (!value) {
32
+ return undefined;
33
+ }
34
+ const types = value
35
+ .split(',')
36
+ .map((t) => parseMediaType(t))
37
+ .filter((t) => !!t && t !== '*/*');
38
+ if (types.length === 0) {
39
+ return undefined;
40
+ }
41
+ return types.find((t) => t === 'application/json') ?? types[0];
42
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"merge-operation.d.ts","sourceRoot":"","sources":["../../src/helpers/merge-operation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,eAAO,MAAM,eAAe,GAC1B,YAAY,WAAW,CAAC,eAAe,EACvC,YAAY,WAAW,CAAC,eAAe,KACtC,WAAW,CAAC,eAiFd,CAAA"}
1
+ {"version":3,"file":"merge-operation.d.ts","sourceRoot":"","sources":["../../src/helpers/merge-operation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAmBxD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,eAAO,MAAM,eAAe,GAC1B,YAAY,WAAW,CAAC,eAAe,EACvC,YAAY,WAAW,CAAC,eAAe,KACtC,WAAW,CAAC,eA0Gd,CAAA"}
@@ -1,3 +1,14 @@
1
+ import { POSTMAN_EXAMPLE_NAME_EXTENSION, POSTMAN_POST_RESPONSE_SCRIPTS_EXTENSION, POSTMAN_PRE_REQUEST_SCRIPTS_EXTENSION, } from './path-items.js';
2
+ const SCRIPT_MERGE_CONFIG = [
3
+ {
4
+ extensionKey: POSTMAN_PRE_REQUEST_SCRIPTS_EXTENSION,
5
+ outputKey: 'x-pre-request',
6
+ },
7
+ {
8
+ extensionKey: POSTMAN_POST_RESPONSE_SCRIPTS_EXTENSION,
9
+ outputKey: 'x-post-response',
10
+ },
11
+ ];
1
12
  /**
2
13
  * Merges two OpenAPI OperationObject instances.
3
14
  * Assumes that all example names (keys in the 'examples' objects) are unique across both operations.
@@ -102,12 +113,20 @@ export const mergeOperations = (operation1, operation2) => {
102
113
  const mediaTypeObj = mediaType;
103
114
  if (contentMediaTypeMap.has(contentType)) {
104
115
  const existingMediaType = contentMediaTypeMap.get(contentType);
105
- if (existingMediaType && (existingMediaType.examples || mediaTypeObj.examples)) {
116
+ if (existingMediaType) {
117
+ if (mediaTypeObj.schema) {
118
+ existingMediaType.schema = mediaTypeObj.schema;
119
+ }
120
+ if (mediaTypeObj.example !== undefined && existingMediaType.example === undefined) {
121
+ existingMediaType.example = mediaTypeObj.example;
122
+ }
106
123
  // Assumption: example names (keys) are unique, so this merge is safe
107
- existingMediaType.examples = {
108
- ...existingMediaType.examples,
109
- ...mediaTypeObj.examples,
110
- };
124
+ if (existingMediaType.examples || mediaTypeObj.examples) {
125
+ existingMediaType.examples = {
126
+ ...existingMediaType.examples,
127
+ ...mediaTypeObj.examples,
128
+ };
129
+ }
111
130
  }
112
131
  }
113
132
  else {
@@ -121,5 +140,101 @@ export const mergeOperations = (operation1, operation2) => {
121
140
  content: Object.fromEntries(contentMediaTypeMap),
122
141
  };
123
142
  }
143
+ operation.responses = {
144
+ ...(operation1.responses ?? {}),
145
+ ...(operation.responses ?? {}),
146
+ };
147
+ operation.summary = mergeSummary(operation1.summary, operation.summary);
148
+ operation.description = mergeDescription(operation1.description, operation.description);
149
+ for (const config of SCRIPT_MERGE_CONFIG) {
150
+ const mergedScripts = mergeScriptsBySource(operation1, operation, config.extensionKey, config.outputKey);
151
+ if (mergedScripts) {
152
+ operation[config.extensionKey] = mergedScripts;
153
+ }
154
+ else {
155
+ delete operation[config.extensionKey];
156
+ }
157
+ updateRenderedScript(operation, config.extensionKey, config.outputKey);
158
+ }
124
159
  return operation;
125
160
  };
161
+ const mergeSummary = (firstSummary, secondSummary) => {
162
+ const first = firstSummary?.trim();
163
+ const second = secondSummary?.trim();
164
+ if (!first && !second) {
165
+ return undefined;
166
+ }
167
+ if (!first) {
168
+ return second;
169
+ }
170
+ if (!second) {
171
+ return first;
172
+ }
173
+ if (first.length < second.length) {
174
+ return first;
175
+ }
176
+ return second;
177
+ };
178
+ const mergeDescription = (firstDescription, secondDescription) => {
179
+ const values = [firstDescription?.trim(), secondDescription?.trim()].filter((candidate) => Boolean(candidate && candidate.length > 0));
180
+ if (values.length === 0) {
181
+ return undefined;
182
+ }
183
+ const unique = Array.from(new Set(values));
184
+ return unique.join('\n\n');
185
+ };
186
+ const mergeScriptsBySource = (operation1, operation2, extensionKey, outputKey) => {
187
+ const scripts = new Map();
188
+ const operation1HasScriptMap = addScriptsToMap(scripts, operation1[extensionKey]);
189
+ if (!operation1HasScriptMap) {
190
+ addLegacyScriptToMap(scripts, operation1, outputKey);
191
+ }
192
+ const operation2HasScriptMap = addScriptsToMap(scripts, operation2[extensionKey]);
193
+ if (!operation2HasScriptMap) {
194
+ addLegacyScriptToMap(scripts, operation2, outputKey);
195
+ }
196
+ if (scripts.size === 0) {
197
+ return undefined;
198
+ }
199
+ return Object.fromEntries(scripts);
200
+ };
201
+ const addScriptsToMap = (target, source) => {
202
+ if (!source || typeof source !== 'object' || Array.isArray(source)) {
203
+ return false;
204
+ }
205
+ let hasScripts = false;
206
+ for (const [name, script] of Object.entries(source)) {
207
+ if (!name || typeof script !== 'string' || script.length === 0) {
208
+ continue;
209
+ }
210
+ target.set(name, script);
211
+ hasScripts = true;
212
+ }
213
+ return hasScripts;
214
+ };
215
+ const addLegacyScriptToMap = (target, operation, outputKey) => {
216
+ const legacyScript = operation[outputKey];
217
+ if (typeof legacyScript !== 'string' || legacyScript.length === 0) {
218
+ return;
219
+ }
220
+ const sourceName = typeof operation[POSTMAN_EXAMPLE_NAME_EXTENSION] === 'string' &&
221
+ operation[POSTMAN_EXAMPLE_NAME_EXTENSION].length > 0
222
+ ? operation[POSTMAN_EXAMPLE_NAME_EXTENSION]
223
+ : 'Default example';
224
+ target.set(sourceName, legacyScript);
225
+ };
226
+ const updateRenderedScript = (operation, extensionKey, scriptKey) => {
227
+ const scripts = operation[extensionKey];
228
+ if (!scripts || typeof scripts !== 'object' || Array.isArray(scripts)) {
229
+ delete operation[scriptKey];
230
+ return;
231
+ }
232
+ const sections = Object.entries(scripts)
233
+ .filter((entry) => typeof entry[0] === 'string' && typeof entry[1] === 'string')
234
+ .map(([sourceName, script]) => `// --- ${sourceName} ---\n${script}`);
235
+ if (sections.length === 0) {
236
+ delete operation[scriptKey];
237
+ return;
238
+ }
239
+ operation[scriptKey] = sections.join('\n\n');
240
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"merge-path-item.d.ts","sourceRoot":"","sources":["../../src/helpers/merge-path-item.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAOxD,eAAO,MAAM,oBAAoB,oBAAoB,CAAA;AAErD,eAAO,MAAM,cAAc,EAAE,SAAS,CAAC,MAAM,WAAW,CAAC,cAAc,CAAC,EASvE,CAAA;AAED,eAAO,MAAM,aAAa,GACxB,OAAO,WAAW,CAAC,WAAW,EAC9B,mBAAmB,MAAM,EACzB,UAAU,WAAW,CAAC,cAAc,EACpC,iBAAgB,OAAe,KAC9B,IA8BF,CAAA"}
1
+ {"version":3,"file":"merge-path-item.d.ts","sourceRoot":"","sources":["../../src/helpers/merge-path-item.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAYxD,eAAO,MAAM,oBAAoB,oBAAoB,CAAA;AAErD,eAAO,MAAM,cAAc,EAAE,SAAS,CAAC,MAAM,WAAW,CAAC,cAAc,CAAC,EASvE,CAAA;AAED,eAAO,MAAM,aAAa,GACxB,OAAO,WAAW,CAAC,WAAW,EAC9B,mBAAmB,MAAM,EACzB,UAAU,WAAW,CAAC,cAAc,EACpC,iBAAgB,OAAe,KAC9B,IAuCF,CAAA"}
@@ -1,6 +1,7 @@
1
1
  import { generateUniqueValue } from '../helpers/generate-unique-value.js';
2
2
  import { getOperationExamples } from '../helpers/get-operation-examples.js';
3
3
  import { mergeOperations } from '../helpers/merge-operation.js';
4
+ import { POSTMAN_EXAMPLE_NAME_EXTENSION, POSTMAN_POST_RESPONSE_SCRIPTS_EXTENSION, POSTMAN_PRE_REQUEST_SCRIPTS_EXTENSION, } from '../helpers/path-items.js';
4
5
  import { renameOperationExamples } from '../helpers/rename-operation-example.js';
5
6
  export const DEFAULT_EXAMPLE_NAME = 'Default example';
6
7
  export const OPERATION_KEYS = [
@@ -21,17 +22,40 @@ export const mergePathItem = (paths, normalizedPathKey, pathItem, mergeOperation
21
22
  }
22
23
  const isOperationKey = OPERATION_KEYS.includes(key);
23
24
  if (isOperationKey && targetPath[key] && mergeOperation) {
25
+ const incomingOperation = pathItem[key];
26
+ const sourceName = typeof incomingOperation[POSTMAN_EXAMPLE_NAME_EXTENSION] === 'string'
27
+ ? incomingOperation[POSTMAN_EXAMPLE_NAME_EXTENSION]
28
+ : DEFAULT_EXAMPLE_NAME;
24
29
  // Get all example names from the target path
25
30
  const exampleNames = getOperationExamples(targetPath);
26
31
  // Generate a unique example name
27
- const newExampleName = generateUniqueValue(DEFAULT_EXAMPLE_NAME, (value) => !exampleNames.has(value), '#');
28
- // Rename operation examples from the new path item (we know it's gonna have only the default example)
29
- renameOperationExamples(pathItem[key], DEFAULT_EXAMPLE_NAME, newExampleName);
32
+ const newExampleName = generateUniqueValue(sourceName, (value) => !exampleNames.has(value), '#');
33
+ // Rename operation examples from the new path item if this source name already exists
34
+ renameOperationExamples(incomingOperation, sourceName, newExampleName);
35
+ updateExtensionKey(incomingOperation, POSTMAN_PRE_REQUEST_SCRIPTS_EXTENSION, sourceName, newExampleName);
36
+ updateExtensionKey(incomingOperation, POSTMAN_POST_RESPONSE_SCRIPTS_EXTENSION, sourceName, newExampleName);
37
+ incomingOperation[POSTMAN_EXAMPLE_NAME_EXTENSION] = newExampleName;
30
38
  // Merge the operations
31
- targetPath[key] = mergeOperations(targetPath[key], pathItem[key]);
39
+ targetPath[key] = mergeOperations(targetPath[key], incomingOperation);
32
40
  continue;
33
41
  }
34
42
  targetPath[key] = value;
35
43
  }
36
44
  paths[normalizedPathKey] = targetPath;
37
45
  };
46
+ const updateExtensionKey = (operation, extensionKey, oldKey, newKey) => {
47
+ if (oldKey === newKey) {
48
+ return;
49
+ }
50
+ const map = operation[extensionKey];
51
+ if (!map || typeof map !== 'object' || Array.isArray(map)) {
52
+ return;
53
+ }
54
+ const castedMap = map;
55
+ const value = castedMap[oldKey];
56
+ if (value === undefined) {
57
+ return;
58
+ }
59
+ delete castedMap[oldKey];
60
+ castedMap[newKey] = value;
61
+ };
@@ -3,8 +3,11 @@ import type { Request } from '../types.js';
3
3
  /**
4
4
  * Extracts parameters from a Postman request and converts them to OpenAPI parameter objects.
5
5
  * Processes query, path, and header parameters from the request URL and headers.
6
+ *
7
+ * Headers on the built-in block-list (content negotiation, transport, auth) are
8
+ * dropped unless the caller passes them via `keepHeaders` (case-insensitive).
6
9
  */
7
- export declare function extractParameters(request: Request, exampleName: string): OpenAPIV3_1.ParameterObject[];
10
+ export declare function extractParameters(request: Request, exampleName: string, keepHeaders?: readonly string[]): OpenAPIV3_1.ParameterObject[];
8
11
  /**
9
12
  * Creates an OpenAPI parameter object from a Postman parameter.
10
13
  */
@@ -1 +1 @@
1
- {"version":3,"file":"parameters.d.ts","sourceRoot":"","sources":["../../src/helpers/parameters.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD,OAAO,KAAK,EAAU,OAAO,EAAE,MAAM,SAAS,CAAA;AAI9C;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,GAAG,WAAW,CAAC,eAAe,EAAE,CA2DtG;AAoBD;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,GAAG,EACV,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,EACpC,WAAW,EAAE,MAAM,GAClB,WAAW,CAAC,eAAe,CAwD7B"}
1
+ {"version":3,"file":"parameters.d.ts","sourceRoot":"","sources":["../../src/helpers/parameters.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD,OAAO,KAAK,EAAU,OAAO,EAAE,MAAM,SAAS,CAAA;AAuC9C;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,OAAO,EAChB,WAAW,EAAE,MAAM,EACnB,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,GAC9B,WAAW,CAAC,eAAe,EAAE,CA8D/B;AAoBD;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,GAAG,EACV,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,EACpC,WAAW,EAAE,MAAM,GAClB,WAAW,CAAC,eAAe,CAwD7B"}
@@ -1,9 +1,45 @@
1
1
  import { inferSchemaType } from './schemas.js';
2
+ /**
3
+ * Header keys (lower-case) that never become `parameters[in=header]` on an
4
+ * OpenAPI operation. They either describe the transport (Host, Connection),
5
+ * overlap with `requestBody.content` / `responses.content` (Accept,
6
+ * Content-Type), or belong in `components.securitySchemes` (Authorization,
7
+ * Cookie). Callers can opt a specific name back in via `ConvertOptions.keepHeaders`.
8
+ */
9
+ const BLOCKED_HEADERS = new Set([
10
+ // Content negotiation
11
+ 'accept',
12
+ 'accept-encoding',
13
+ 'accept-language',
14
+ 'content-type',
15
+ // Transport / hop-by-hop
16
+ 'connection',
17
+ 'content-length',
18
+ 'host',
19
+ 'transfer-encoding',
20
+ // Auth — belong in components.securitySchemes
21
+ 'authorization',
22
+ 'cookie',
23
+ 'proxy-authorization',
24
+ ]);
25
+ function isBlockedHeader(name, keepHeaders) {
26
+ if (!name) {
27
+ return false;
28
+ }
29
+ const lower = name.toLowerCase();
30
+ if (keepHeaders?.some((kept) => kept.toLowerCase() === lower)) {
31
+ return false;
32
+ }
33
+ return BLOCKED_HEADERS.has(lower);
34
+ }
2
35
  /**
3
36
  * Extracts parameters from a Postman request and converts them to OpenAPI parameter objects.
4
37
  * Processes query, path, and header parameters from the request URL and headers.
38
+ *
39
+ * Headers on the built-in block-list (content negotiation, transport, auth) are
40
+ * dropped unless the caller passes them via `keepHeaders` (case-insensitive).
5
41
  */
6
- export function extractParameters(request, exampleName) {
42
+ export function extractParameters(request, exampleName, keepHeaders) {
7
43
  const parameters = [];
8
44
  const parameterMap = new Map();
9
45
  if (typeof request === 'string' || !request.url) {
@@ -48,6 +84,9 @@ export function extractParameters(request, exampleName) {
48
84
  // Process header parameters
49
85
  if (request.header && Array.isArray(request.header)) {
50
86
  request.header.forEach((header) => {
87
+ if (isBlockedHeader(header.key, keepHeaders)) {
88
+ return;
89
+ }
51
90
  const paramObj = createParameterObject(header, 'header', exampleName);
52
91
  if (paramObj.name) {
53
92
  parameterMap.set(paramObj.name, paramObj);
@@ -1,24 +1,34 @@
1
1
  import type { OpenAPIV3_1 } from '@scalar/openapi-types';
2
2
  import type { Item, ItemGroup } from '../types.js';
3
3
  type HttpMethods = 'get' | 'put' | 'post' | 'delete' | 'options' | 'head' | 'patch' | 'trace';
4
+ export declare const POSTMAN_EXAMPLE_NAME_EXTENSION = "x-postman-example-name";
5
+ export declare const POSTMAN_PRE_REQUEST_SCRIPTS_EXTENSION = "x-postman-pre-request-scripts";
6
+ export declare const POSTMAN_POST_RESPONSE_SCRIPTS_EXTENSION = "x-postman-post-response-scripts";
7
+ export declare const POSTMAN_FOLDER_SEGMENTS_EXTENSION = "x-postman-folder-segments";
4
8
  /**
5
9
  * Information about server usage for an operation.
6
10
  */
7
11
  export type ServerUsage = {
8
12
  serverUrl: string;
13
+ server?: OpenAPIV3_1.ServerObject;
9
14
  path: string;
10
15
  method: HttpMethods;
11
16
  };
17
+ type CollectionVariableLookup = ReadonlyMap<string, string>;
12
18
  /**
13
19
  * Processes a Postman collection item or item group and returns
14
20
  * the corresponding OpenAPI paths and components.
15
21
  * Handles nested item groups, extracts request details, and generates corresponding
16
22
  * OpenAPI path items and operations.
17
23
  */
18
- export declare function processItem(item: Item | ItemGroup, exampleName?: string, parentTags?: string[], parentPath?: string): {
24
+ export declare function processItem(item: Item | ItemGroup, exampleName?: string, parentTags?: string[], parentPath?: string, preserveCollapsedVariants?: boolean, resolveTagName?: (segments: string[]) => string | undefined, collectionVariableLookup?: CollectionVariableLookup, keepHeaders?: readonly string[]): {
19
25
  paths: OpenAPIV3_1.PathsObject;
20
26
  components: OpenAPIV3_1.ComponentsObject;
21
27
  serverUsage: ServerUsage[];
22
28
  };
29
+ export declare function parseStatusCodeFromRequestName(requestName: string | undefined): {
30
+ statusCode: string;
31
+ description: string;
32
+ } | null;
23
33
  export {};
24
34
  //# sourceMappingURL=path-items.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"path-items.d.ts","sourceRoot":"","sources":["../../src/helpers/path-items.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD,OAAO,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAW9C,KAAK,WAAW,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,CAAA;AAE7F;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,WAAW,CAAA;CACpB,CAAA;AAoBD;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,IAAI,GAAG,SAAS,EACtB,WAAW,GAAE,MAAkB,EAC/B,UAAU,GAAE,MAAM,EAAO,EACzB,UAAU,GAAE,MAAW,GACtB;IACD,KAAK,EAAE,WAAW,CAAC,WAAW,CAAA;IAC9B,UAAU,EAAE,WAAW,CAAC,gBAAgB,CAAA;IACxC,WAAW,EAAE,WAAW,EAAE,CAAA;CAC3B,CA2KA"}
1
+ {"version":3,"file":"path-items.d.ts","sourceRoot":"","sources":["../../src/helpers/path-items.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD,OAAO,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAY9C,KAAK,WAAW,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,CAAA;AAE7F,eAAO,MAAM,8BAA8B,2BAA2B,CAAA;AACtE,eAAO,MAAM,qCAAqC,kCAAkC,CAAA;AACpF,eAAO,MAAM,uCAAuC,oCAAoC,CAAA;AAIxF,eAAO,MAAM,iCAAiC,8BAA8B,CAAA;AAE5E;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,WAAW,CAAC,YAAY,CAAA;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,WAAW,CAAA;CACpB,CAAA;AAED,KAAK,wBAAwB,GAAG,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAoB3D;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,IAAI,GAAG,SAAS,EACtB,WAAW,GAAE,MAAkB,EAC/B,UAAU,GAAE,MAAM,EAAO,EACzB,UAAU,GAAE,MAAW,EACvB,yBAAyB,GAAE,OAAe,EAC1C,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,MAAM,GAAG,SAAS,EAC3D,wBAAwB,GAAE,wBAAoC,EAC9D,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,GAC9B;IACD,KAAK,EAAE,WAAW,CAAC,WAAW,CAAA;IAC9B,UAAU,EAAE,WAAW,CAAC,gBAAgB,CAAA;IACxC,WAAW,EAAE,WAAW,EAAE,CAAA;CAC3B,CAoNA;AAED,wBAAgB,8BAA8B,CAC5C,WAAW,EAAE,MAAM,GAAG,SAAS,GAC9B;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAkCpD"}
@@ -1,11 +1,19 @@
1
1
  import { processAuth } from './auth.js';
2
+ import { parseMediaType, pickAcceptMediaType, readHeader } from './header-utils.js';
2
3
  import { parseMdTable } from './markdown.js';
3
4
  import { extractParameters } from './parameters.js';
4
5
  import { processPostResponseScripts } from './post-response-scripts.js';
5
6
  import { processPreRequestScripts } from './pre-request-scripts.js';
6
7
  import { extractRequestBody } from './request-body.js';
7
- import { extractResponses } from './responses.js';
8
- import { extractPathFromUrl, extractPathParameterNames, extractServerFromUrl, normalizePath } from './urls.js';
8
+ import { DEFAULT_RESPONSE_DESCRIPTIONS, extractResponses } from './responses.js';
9
+ import { extractPathFromUrl, extractPathParameterNames, extractServerObjectFromUrl, normalizePath } from './urls.js';
10
+ export const POSTMAN_EXAMPLE_NAME_EXTENSION = 'x-postman-example-name';
11
+ export const POSTMAN_PRE_REQUEST_SCRIPTS_EXTENSION = 'x-postman-pre-request-scripts';
12
+ export const POSTMAN_POST_RESPONSE_SCRIPTS_EXTENSION = 'x-postman-post-response-scripts';
13
+ // Raw Postman folder-name chain for the operation. Used internally to preserve
14
+ // template hints (for example `/applications/{id}`) even when tag names are
15
+ // simplified. Stripped before emitting the final OpenAPI document.
16
+ export const POSTMAN_FOLDER_SEGMENTS_EXTENSION = 'x-postman-folder-segments';
9
17
  function ensureRequestBodyContent(requestBody) {
10
18
  const content = requestBody.content ?? {};
11
19
  if (Object.keys(content).length === 0) {
@@ -27,14 +35,14 @@ function ensureRequestBodyContent(requestBody) {
27
35
  * Handles nested item groups, extracts request details, and generates corresponding
28
36
  * OpenAPI path items and operations.
29
37
  */
30
- export function processItem(item, exampleName = 'default', parentTags = [], parentPath = '') {
38
+ export function processItem(item, exampleName = 'default', parentTags = [], parentPath = '', preserveCollapsedVariants = false, resolveTagName, collectionVariableLookup = new Map(), keepHeaders) {
31
39
  const paths = {};
32
40
  const components = {};
33
41
  const serverUsage = [];
34
42
  if ('item' in item && Array.isArray(item.item)) {
35
43
  const newParentTags = item.name ? [...parentTags, item.name] : parentTags;
36
44
  item.item.forEach((childItem) => {
37
- const childResult = processItem(childItem, exampleName, newParentTags, `${parentPath}/${item.name || ''}`);
45
+ const childResult = processItem(childItem, exampleName, newParentTags, `${parentPath}/${item.name || ''}`, preserveCollapsedVariants, resolveTagName, collectionVariableLookup, keepHeaders);
38
46
  // Merge child paths and components
39
47
  for (const [pathKey, pathItem] of Object.entries(childResult.paths)) {
40
48
  if (!paths[pathKey]) {
@@ -63,16 +71,19 @@ export function processItem(item, exampleName = 'default', parentTags = [], pare
63
71
  return { paths, components, serverUsage };
64
72
  }
65
73
  const { request, name, response } = item;
74
+ const sourceRequestName = name?.trim() || exampleName;
75
+ const operationExampleName = preserveCollapsedVariants ? sourceRequestName : exampleName;
66
76
  const method = (typeof request === 'string' ? 'get' : request.method || 'get').toLowerCase();
67
77
  const requestUrl = typeof request === 'string' ? request : typeof request.url === 'string' ? request.url : (request.url?.raw ?? '');
68
78
  const path = extractPathFromUrl(requestUrl);
69
79
  // Normalize path parameters from ':param' to '{param}'
70
80
  const normalizedPath = normalizePath(path);
71
81
  // Extract server URL from request URL
72
- const serverUrl = extractServerFromUrl(requestUrl);
73
- if (serverUrl) {
82
+ const server = extractServerObjectFromUrl(requestUrl, collectionVariableLookup);
83
+ if (server?.url) {
74
84
  serverUsage.push({
75
- serverUrl,
85
+ serverUrl: server.url,
86
+ server,
76
87
  path: normalizedPath,
77
88
  method,
78
89
  });
@@ -86,22 +97,45 @@ export function processItem(item, exampleName = 'default', parentTags = [], pare
86
97
  : typeof request.description === 'string'
87
98
  ? request.description
88
99
  : (request.description?.content ?? '');
100
+ const tagName = parentTags.length > 0 ? (resolveTagName ? resolveTagName(parentTags) : parentTags.join(' > ')) : undefined;
101
+ // Derive media-type signals from request headers before they are filtered out
102
+ // of `parameters[in=header]`. Content-Type drives the request body media type;
103
+ // Accept drives responses when no saved-response Content-Type is available.
104
+ const requestHeaders = typeof request === 'string' ? undefined : request.header;
105
+ const contentType = parseMediaType(readHeader(requestHeaders, 'Content-Type'));
106
+ const acceptMediaType = pickAcceptMediaType(readHeader(requestHeaders, 'Accept'));
89
107
  const operationObject = {
90
- tags: parentTags.length > 0 ? [parentTags.join(' > ')] : undefined,
108
+ tags: tagName ? [tagName] : undefined,
91
109
  summary,
92
110
  description,
93
- responses: extractResponses(response || [], item),
111
+ responses: extractResponses(response || [], item, acceptMediaType),
94
112
  parameters: [],
95
113
  };
114
+ if (parentTags.length > 0) {
115
+ operationObject[POSTMAN_FOLDER_SEGMENTS_EXTENSION] = [...parentTags];
116
+ }
117
+ if (preserveCollapsedVariants) {
118
+ operationObject[POSTMAN_EXAMPLE_NAME_EXTENSION] = sourceRequestName;
119
+ }
96
120
  // Add pre-request scripts if present
97
121
  const preRequestScript = processPreRequestScripts(item.event);
98
122
  if (preRequestScript) {
99
123
  operationObject['x-pre-request'] = preRequestScript;
124
+ if (preserveCollapsedVariants) {
125
+ operationObject[POSTMAN_PRE_REQUEST_SCRIPTS_EXTENSION] = {
126
+ [sourceRequestName]: preRequestScript,
127
+ };
128
+ }
100
129
  }
101
130
  // Add post-response scripts if present
102
131
  const postResponseScript = processPostResponseScripts(item.event);
103
132
  if (postResponseScript) {
104
133
  operationObject['x-post-response'] = postResponseScript;
134
+ if (preserveCollapsedVariants) {
135
+ operationObject[POSTMAN_POST_RESPONSE_SCRIPTS_EXTENSION] = {
136
+ [sourceRequestName]: postResponseScript,
137
+ };
138
+ }
105
139
  }
106
140
  // Only add operationId if it was explicitly provided
107
141
  if (operationId) {
@@ -109,7 +143,7 @@ export function processItem(item, exampleName = 'default', parentTags = [], pare
109
143
  }
110
144
  // Extract parameters from the request (query, path, header)
111
145
  // This should always happen, regardless of whether a description exists
112
- const extractedParameters = extractParameters(request, exampleName);
146
+ const extractedParameters = extractParameters(request, operationExampleName, keepHeaders);
113
147
  // Merge parameters, giving priority to those from the Markdown table if description exists
114
148
  const mergedParameters = new Map();
115
149
  // Add extracted parameters, filtering out path parameters not in the path
@@ -156,7 +190,7 @@ export function processItem(item, exampleName = 'default', parentTags = [], pare
156
190
  }
157
191
  // Allow request bodies for all methods (including GET) if body is present
158
192
  if (typeof request !== 'string' && request.body) {
159
- const requestBody = extractRequestBody(request.body, exampleName);
193
+ const requestBody = extractRequestBody(request.body, operationExampleName, contentType);
160
194
  ensureRequestBodyContent(requestBody);
161
195
  // Only add requestBody if it has content
162
196
  if (requestBody.content && Object.keys(requestBody.content).length > 0) {
@@ -168,8 +202,40 @@ export function processItem(item, exampleName = 'default', parentTags = [], pare
168
202
  }
169
203
  const pathItem = paths[path];
170
204
  pathItem[method] = operationObject;
205
+ if (preserveCollapsedVariants) {
206
+ addResponseFromRequestName(pathItem[method], name);
207
+ }
171
208
  return { paths, components, serverUsage };
172
209
  }
210
+ export function parseStatusCodeFromRequestName(requestName) {
211
+ if (!requestName) {
212
+ return null;
213
+ }
214
+ const trimmedStart = requestName.trimStart();
215
+ if (trimmedStart.length < 4) {
216
+ return null;
217
+ }
218
+ const statusCode = trimmedStart.slice(0, 3);
219
+ if (!isThreeDigitStatusCode(statusCode)) {
220
+ return null;
221
+ }
222
+ let separatorIndex = 3;
223
+ while (trimmedStart[separatorIndex] === ' ') {
224
+ separatorIndex += 1;
225
+ }
226
+ const separator = trimmedStart[separatorIndex];
227
+ if (separator !== '-' && separator !== ':') {
228
+ return null;
229
+ }
230
+ let descriptionIndex = separatorIndex + 1;
231
+ while (trimmedStart[descriptionIndex] === ' ') {
232
+ descriptionIndex += 1;
233
+ }
234
+ return {
235
+ statusCode,
236
+ description: trimmedStart.slice(descriptionIndex).trim() || 'Response',
237
+ };
238
+ }
173
239
  /** OpenAPI 3.1 parameter schema types (ParameterObject uses OpenAPIV3_1). */
174
240
  const OPENAPI_PARAM_SCHEMA_TYPES = ['string', 'number', 'integer', 'boolean', 'object', 'array'];
175
241
  function toOpenApiParamSchemaType(s) {
@@ -259,3 +325,37 @@ function extractOperationInfo(name) {
259
325
  const summary = name.substring(0, lastBracketIndex).trim();
260
326
  return { operationId, summary };
261
327
  }
328
+ function addResponseFromRequestName(operationObject, requestName) {
329
+ const parsedStatus = parseStatusCodeFromRequestName(requestName);
330
+ if (!parsedStatus) {
331
+ return;
332
+ }
333
+ operationObject.responses = operationObject.responses ?? {};
334
+ const existingResponse = operationObject.responses[parsedStatus.statusCode];
335
+ if (!existingResponse) {
336
+ operationObject.responses[parsedStatus.statusCode] = { description: parsedStatus.description };
337
+ return;
338
+ }
339
+ const defaultDescriptions = new Set([
340
+ 'Successful response',
341
+ 'Response',
342
+ ...Object.values(DEFAULT_RESPONSE_DESCRIPTIONS),
343
+ ]);
344
+ if (typeof existingResponse === 'object' &&
345
+ !('$ref' in existingResponse) &&
346
+ defaultDescriptions.has(existingResponse.description ?? '')) {
347
+ existingResponse.description = parsedStatus.description;
348
+ }
349
+ }
350
+ function isThreeDigitStatusCode(value) {
351
+ if (value.length !== 3) {
352
+ return false;
353
+ }
354
+ for (let index = 0; index < value.length; index += 1) {
355
+ const charCode = value.charCodeAt(index);
356
+ if (charCode < 48 || charCode > 57) {
357
+ return false;
358
+ }
359
+ }
360
+ return true;
361
+ }
@@ -3,6 +3,10 @@ import type { RequestBody } from '../types.js';
3
3
  /**
4
4
  * Extracts and converts the request body from a Postman request to an OpenAPI RequestBodyObject.
5
5
  * Handles raw JSON, form-data, and URL-encoded body types, creating appropriate schemas and content types.
6
+ *
7
+ * When `contentType` is provided (already normalised via `parseMediaType`), it
8
+ * wins over the Postman `options.raw.language` hint for `raw` bodies.
9
+ * `formdata` and `urlencoded` modes keep their natural media types.
6
10
  */
7
- export declare function extractRequestBody(body: RequestBody, exampleName: string): OpenAPIV3_1.RequestBodyObject;
11
+ export declare function extractRequestBody(body: RequestBody, exampleName: string, contentType?: string): OpenAPIV3_1.RequestBodyObject;
8
12
  //# sourceMappingURL=request-body.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"request-body.d.ts","sourceRoot":"","sources":["../../src/helpers/request-body.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD,OAAO,KAAK,EAAiB,WAAW,EAAuB,MAAM,SAAS,CAAA;AAK9E;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,GAAG,WAAW,CAAC,iBAAiB,CAqBxG"}
1
+ {"version":3,"file":"request-body.d.ts","sourceRoot":"","sources":["../../src/helpers/request-body.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD,OAAO,KAAK,EAAiB,WAAW,EAAuB,MAAM,SAAS,CAAA;AAK9E;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,WAAW,EACjB,WAAW,EAAE,MAAM,EACnB,WAAW,CAAC,EAAE,MAAM,GACnB,WAAW,CAAC,iBAAiB,CAqB/B"}