@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.
- package/CHANGELOG.md +25 -0
- package/README.md +15 -0
- package/dist/convert.d.ts +14 -0
- package/dist/convert.d.ts.map +1 -1
- package/dist/convert.js +392 -44
- package/dist/helpers/header-utils.d.ts +21 -0
- package/dist/helpers/header-utils.d.ts.map +1 -0
- package/dist/helpers/header-utils.js +42 -0
- package/dist/helpers/merge-operation.d.ts.map +1 -1
- package/dist/helpers/merge-operation.js +120 -5
- package/dist/helpers/merge-path-item.d.ts.map +1 -1
- package/dist/helpers/merge-path-item.js +28 -4
- package/dist/helpers/parameters.d.ts +4 -1
- package/dist/helpers/parameters.d.ts.map +1 -1
- package/dist/helpers/parameters.js +40 -1
- package/dist/helpers/path-items.d.ts +11 -1
- package/dist/helpers/path-items.d.ts.map +1 -1
- package/dist/helpers/path-items.js +111 -11
- package/dist/helpers/request-body.d.ts +5 -1
- package/dist/helpers/request-body.d.ts.map +1 -1
- package/dist/helpers/request-body.js +21 -28
- package/dist/helpers/responses.d.ts +8 -1
- package/dist/helpers/responses.d.ts.map +1 -1
- package/dist/helpers/responses.js +99 -11
- package/dist/helpers/servers.d.ts.map +1 -1
- package/dist/helpers/servers.js +10 -6
- package/dist/helpers/urls.d.ts +20 -0
- package/dist/helpers/urls.d.ts.map +1 -1
- package/dist/helpers/urls.js +101 -3
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/is-postman-collection.d.ts +9 -0
- package/dist/is-postman-collection.d.ts.map +1 -0
- package/dist/is-postman-collection.js +23 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- 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;
|
|
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
|
|
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
|
-
|
|
109
|
-
|
|
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;
|
|
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(
|
|
28
|
-
// Rename operation examples from the new path item
|
|
29
|
-
renameOperationExamples(
|
|
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],
|
|
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;
|
|
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;
|
|
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,
|
|
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
|
|
73
|
-
if (
|
|
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:
|
|
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,
|
|
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,
|
|
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
|
|
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"}
|