@scalar/postman-to-openapi 0.5.1 → 0.5.3
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 +15 -0
- package/dist/convert.js +240 -212
- package/dist/helpers/auth.js +116 -92
- package/dist/helpers/contact.js +24 -20
- package/dist/helpers/external-docs.js +33 -25
- package/dist/helpers/form-data.js +42 -36
- package/dist/helpers/license.js +21 -17
- package/dist/helpers/logo.js +22 -21
- package/dist/helpers/markdown.js +33 -30
- package/dist/helpers/parameters.js +119 -96
- package/dist/helpers/path-items.js +244 -202
- package/dist/helpers/post-response-scripts.js +12 -12
- package/dist/helpers/pre-request-scripts.js +12 -12
- package/dist/helpers/prune-document.js +42 -35
- package/dist/helpers/request-body.js +102 -92
- package/dist/helpers/responses.js +62 -57
- package/dist/helpers/schemas.js +43 -37
- package/dist/helpers/servers.js +83 -57
- package/dist/helpers/status-codes.js +40 -30
- package/dist/helpers/urls.js +74 -51
- package/dist/index.js +1 -5
- package/dist/types.js +1 -1
- package/package.json +6 -11
- package/dist/convert.js.map +0 -7
- package/dist/helpers/auth.js.map +0 -7
- package/dist/helpers/contact.js.map +0 -7
- package/dist/helpers/external-docs.js.map +0 -7
- package/dist/helpers/form-data.js.map +0 -7
- package/dist/helpers/license.js.map +0 -7
- package/dist/helpers/logo.js.map +0 -7
- package/dist/helpers/markdown.js.map +0 -7
- package/dist/helpers/parameters.js.map +0 -7
- package/dist/helpers/path-items.js.map +0 -7
- package/dist/helpers/post-response-scripts.js.map +0 -7
- package/dist/helpers/pre-request-scripts.js.map +0 -7
- package/dist/helpers/prune-document.js.map +0 -7
- package/dist/helpers/request-body.js.map +0 -7
- package/dist/helpers/responses.js.map +0 -7
- package/dist/helpers/schemas.js.map +0 -7
- package/dist/helpers/servers.js.map +0 -7
- package/dist/helpers/status-codes.js.map +0 -7
- package/dist/helpers/urls.js.map +0 -7
- package/dist/index.js.map +0 -7
- package/dist/types.js.map +0 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @scalar/postman-to-openapi
|
|
2
2
|
|
|
3
|
+
## 0.5.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#8466](https://github.com/scalar/scalar/pull/8466): chore: new build pipeline
|
|
8
|
+
|
|
9
|
+
## 0.5.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
#### Updated Dependencies
|
|
14
|
+
|
|
15
|
+
- **@scalar/helpers@0.4.1**
|
|
16
|
+
- [#8420](https://github.com/scalar/scalar/pull/8420): fix TypeScript access to navigator.userAgentData in isMacOS without ts-expect-error
|
|
17
|
+
|
|
3
18
|
## 0.5.1
|
|
4
19
|
|
|
5
20
|
### Patch Changes
|
package/dist/convert.js
CHANGED
|
@@ -1,233 +1,261 @@
|
|
|
1
|
-
import { processAuth } from
|
|
2
|
-
import { processContact } from
|
|
3
|
-
import { processExternalDocs } from
|
|
4
|
-
import { processLicense } from
|
|
5
|
-
import { processLogo } from
|
|
6
|
-
import { processItem } from
|
|
7
|
-
import { pruneDocument } from
|
|
8
|
-
import { analyzeServerDistribution } from
|
|
9
|
-
import { normalizePath } from
|
|
1
|
+
import { processAuth } from './helpers/auth.js';
|
|
2
|
+
import { processContact } from './helpers/contact.js';
|
|
3
|
+
import { processExternalDocs } from './helpers/external-docs.js';
|
|
4
|
+
import { processLicense } from './helpers/license.js';
|
|
5
|
+
import { processLogo } from './helpers/logo.js';
|
|
6
|
+
import { processItem } from './helpers/path-items.js';
|
|
7
|
+
import { pruneDocument } from './helpers/prune-document.js';
|
|
8
|
+
import { analyzeServerDistribution } from './helpers/servers.js';
|
|
9
|
+
import { normalizePath } from './helpers/urls.js';
|
|
10
10
|
const OPERATION_KEYS = [
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
'get',
|
|
12
|
+
'put',
|
|
13
|
+
'post',
|
|
14
|
+
'delete',
|
|
15
|
+
'options',
|
|
16
|
+
'head',
|
|
17
|
+
'patch',
|
|
18
|
+
'trace',
|
|
19
19
|
];
|
|
20
20
|
const normalizeDescription = (description) => {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
if (typeof description === 'string') {
|
|
22
|
+
return description;
|
|
23
|
+
}
|
|
24
|
+
return description?.content;
|
|
25
25
|
};
|
|
26
26
|
const parseCollectionInput = (postmanCollection) => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
27
|
+
if (typeof postmanCollection !== 'string') {
|
|
28
|
+
return postmanCollection;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(postmanCollection);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const details = error instanceof Error ? error.message : 'Unknown parse error';
|
|
35
|
+
const parseError = new Error(`Invalid Postman collection JSON: ${details}`);
|
|
36
|
+
parseError.name = 'PostmanCollectionParseError';
|
|
37
|
+
throw parseError;
|
|
38
|
+
}
|
|
38
39
|
};
|
|
39
40
|
const validateCollectionShape = (collection) => {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
41
|
+
if (!collection || typeof collection !== 'object') {
|
|
42
|
+
throw new Error('Invalid Postman collection: expected an object');
|
|
43
|
+
}
|
|
44
|
+
const candidate = collection;
|
|
45
|
+
if (!candidate.info) {
|
|
46
|
+
throw new Error('Missing required info on Postman collection');
|
|
47
|
+
}
|
|
48
|
+
if (!candidate.item || !Array.isArray(candidate.item)) {
|
|
49
|
+
throw new Error('Invalid Postman collection: item must be an array');
|
|
50
|
+
}
|
|
51
|
+
if (typeof candidate.info !== 'object') {
|
|
52
|
+
throw new Error('Invalid Postman collection: info must be an object');
|
|
53
|
+
}
|
|
54
|
+
if (!candidate.info.name) {
|
|
55
|
+
throw new Error('Missing required info.name on Postman collection');
|
|
56
|
+
}
|
|
57
|
+
if (!candidate.info.schema) {
|
|
58
|
+
throw new Error('Invalid Postman collection: missing info.schema');
|
|
59
|
+
}
|
|
60
|
+
if (candidate.variable && !Array.isArray(candidate.variable)) {
|
|
61
|
+
throw new Error('Invalid Postman collection: variable must be an array when provided');
|
|
62
|
+
}
|
|
63
|
+
return candidate;
|
|
63
64
|
};
|
|
64
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Extracts tags from Postman collection folders.
|
|
67
|
+
* We keep folder nesting using " > " so tag names stay readable while preserving hierarchy.
|
|
68
|
+
* Requests do not produce tags; only folders are reflected as tags.
|
|
69
|
+
*/
|
|
70
|
+
const isItemGroup = (item) => 'item' in item && Array.isArray(item.item);
|
|
65
71
|
const extractTags = (items) => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
72
|
+
const collectTags = (item, parentPath = '') => {
|
|
73
|
+
if (!isItemGroup(item)) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
const nextPath = item.name ? (parentPath ? `${parentPath} > ${item.name}` : item.name) : parentPath;
|
|
77
|
+
const description = normalizeDescription(item.description);
|
|
78
|
+
const currentTag = item.name?.length
|
|
79
|
+
? [
|
|
80
|
+
{
|
|
81
|
+
name: nextPath,
|
|
82
|
+
...(description && { description }),
|
|
83
|
+
},
|
|
84
|
+
]
|
|
85
|
+
: [];
|
|
86
|
+
return [...currentTag, ...item.item.flatMap((subItem) => collectTags(subItem, nextPath))];
|
|
87
|
+
};
|
|
88
|
+
return items.flatMap((item) => collectTags(item));
|
|
81
89
|
};
|
|
82
90
|
const mergeSecuritySchemes = (openapi, securitySchemes) => {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
+
if (!securitySchemes || Object.keys(securitySchemes).length === 0) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
openapi.components = openapi.components || {};
|
|
95
|
+
openapi.components.securitySchemes = {
|
|
96
|
+
...(openapi.components.securitySchemes ?? {}),
|
|
97
|
+
...securitySchemes,
|
|
98
|
+
};
|
|
91
99
|
};
|
|
92
100
|
const mergePathItem = (paths, normalizedPathKey, pathItem) => {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
paths[normalizedPathKey] = targetPath;
|
|
101
|
+
const targetPath = (paths[normalizedPathKey] ?? {});
|
|
102
|
+
for (const [key, value] of Object.entries(pathItem)) {
|
|
103
|
+
if (value === undefined) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const isOperationKey = OPERATION_KEYS.includes(key);
|
|
107
|
+
if (isOperationKey && targetPath[key]) {
|
|
108
|
+
const operationName = typeof key === 'string' ? key.toUpperCase() : String(key);
|
|
109
|
+
console.warn(`Duplicate operation detected for ${operationName} ${normalizedPathKey}. Last operation will overwrite previous.`);
|
|
110
|
+
}
|
|
111
|
+
targetPath[key] = value;
|
|
112
|
+
}
|
|
113
|
+
paths[normalizedPathKey] = targetPath;
|
|
108
114
|
};
|
|
109
115
|
const cleanupOperations = (paths) => {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
OPERATION_KEYS.forEach((operationKey) => {
|
|
115
|
-
const operation = pathItem[operationKey];
|
|
116
|
-
if (!operation) {
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
if ("parameters" in operation && operation.parameters?.length === 0) {
|
|
120
|
-
delete operation.parameters;
|
|
121
|
-
}
|
|
122
|
-
if ("requestBody" in operation && operation.requestBody && "content" in operation.requestBody) {
|
|
123
|
-
const content = operation.requestBody.content;
|
|
124
|
-
if (content && "text/plain" in content) {
|
|
125
|
-
const text = content["text/plain"];
|
|
126
|
-
if (!text?.schema || text.schema && Object.keys(text.schema).length === 0) {
|
|
127
|
-
content["text/plain"] = {};
|
|
128
|
-
}
|
|
116
|
+
Object.values(paths).forEach((pathItem) => {
|
|
117
|
+
if (!pathItem) {
|
|
118
|
+
return;
|
|
129
119
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
120
|
+
OPERATION_KEYS.forEach((operationKey) => {
|
|
121
|
+
const operation = pathItem[operationKey];
|
|
122
|
+
if (!operation) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if ('parameters' in operation && operation.parameters?.length === 0) {
|
|
126
|
+
delete operation.parameters;
|
|
127
|
+
}
|
|
128
|
+
if ('requestBody' in operation && operation.requestBody && 'content' in operation.requestBody) {
|
|
129
|
+
const content = operation.requestBody.content;
|
|
130
|
+
if (content && 'text/plain' in content) {
|
|
131
|
+
const text = content['text/plain'];
|
|
132
|
+
if (!text?.schema || (text.schema && Object.keys(text.schema).length === 0)) {
|
|
133
|
+
content['text/plain'] = {};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (!operation.description) {
|
|
138
|
+
delete operation.description;
|
|
139
|
+
}
|
|
140
|
+
});
|
|
134
141
|
});
|
|
135
|
-
});
|
|
136
142
|
};
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
143
|
+
/**
|
|
144
|
+
* Converts a Postman Collection to an OpenAPI 3.1.0 document.
|
|
145
|
+
* This function processes the collection's information, servers, authentication,
|
|
146
|
+
* and items to create a corresponding OpenAPI structure.
|
|
147
|
+
*/
|
|
148
|
+
export function convert(postmanCollection) {
|
|
149
|
+
const collection = validateCollectionShape(parseCollectionInput(postmanCollection));
|
|
150
|
+
// Extract title from collection info, fallback to 'API' if not provided
|
|
151
|
+
const title = collection.info.name || 'API';
|
|
152
|
+
// Look for version in collection variables, default to '1.0.0'
|
|
153
|
+
const version = collection.variable?.find((v) => v.key === 'version')?.value || '1.0.0';
|
|
154
|
+
// Handle different description formats in Postman
|
|
155
|
+
const description = normalizeDescription(collection.info.description) || '';
|
|
156
|
+
// Process license and contact information
|
|
157
|
+
const license = processLicense(collection);
|
|
158
|
+
const contact = processContact(collection);
|
|
159
|
+
// Process logo information
|
|
160
|
+
const logo = processLogo(collection);
|
|
161
|
+
// Initialize the OpenAPI document with required fields
|
|
162
|
+
const openapi = {
|
|
163
|
+
openapi: '3.1.0',
|
|
164
|
+
info: {
|
|
165
|
+
title,
|
|
166
|
+
version,
|
|
167
|
+
...(description && { description }),
|
|
168
|
+
...(license && { license }),
|
|
169
|
+
...(contact && { contact }),
|
|
170
|
+
...(logo && { 'x-logo': logo }),
|
|
171
|
+
},
|
|
172
|
+
paths: {},
|
|
173
|
+
};
|
|
174
|
+
// Process external docs
|
|
175
|
+
const externalDocs = processExternalDocs(collection);
|
|
176
|
+
if (externalDocs) {
|
|
177
|
+
openapi.externalDocs = externalDocs;
|
|
178
|
+
}
|
|
179
|
+
// Process authentication if present in the collection
|
|
180
|
+
if (collection.auth) {
|
|
181
|
+
const { securitySchemes, security } = processAuth(collection.auth);
|
|
182
|
+
mergeSecuritySchemes(openapi, securitySchemes);
|
|
183
|
+
openapi.security = security;
|
|
184
|
+
}
|
|
185
|
+
// Process each item in the collection and merge into OpenAPI spec
|
|
186
|
+
const allServerUsage = [];
|
|
187
|
+
if (collection.item) {
|
|
188
|
+
// Extract tags from folders
|
|
189
|
+
const tags = extractTags(collection.item);
|
|
190
|
+
if (tags.length > 0) {
|
|
191
|
+
openapi.tags = tags;
|
|
180
192
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
for (const [path, methods] of serverPlacement.operations.entries()) {
|
|
207
|
-
const normalizedPathKey = normalizePath(path);
|
|
208
|
-
const pathItem = openapi.paths[normalizedPathKey];
|
|
209
|
-
if (!pathItem) {
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
for (const [method, servers] of methods.entries()) {
|
|
213
|
-
if (method in pathItem) {
|
|
214
|
-
const operation = pathItem[method];
|
|
215
|
-
if (operation && typeof operation === "object" && "responses" in operation) {
|
|
216
|
-
operation.servers = servers;
|
|
217
|
-
}
|
|
193
|
+
collection.item.forEach((item) => {
|
|
194
|
+
const { paths: itemPaths, components: itemComponents, serverUsage } = processItem(item);
|
|
195
|
+
// Collect server usage information
|
|
196
|
+
allServerUsage.push(...serverUsage);
|
|
197
|
+
// Merge paths from the current item
|
|
198
|
+
openapi.paths = openapi.paths || {};
|
|
199
|
+
for (const [pathKey, pathItem] of Object.entries(itemPaths)) {
|
|
200
|
+
// Convert colon-style params to curly brace style
|
|
201
|
+
const normalizedPathKey = normalizePath(pathKey);
|
|
202
|
+
if (!pathItem) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
mergePathItem(openapi.paths, normalizedPathKey, pathItem);
|
|
206
|
+
}
|
|
207
|
+
// Merge security schemes from the current item
|
|
208
|
+
if (itemComponents?.securitySchemes) {
|
|
209
|
+
mergeSecuritySchemes(openapi, itemComponents.securitySchemes);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
// Extract all unique paths from the document
|
|
214
|
+
const allUniquePaths = new Set();
|
|
215
|
+
if (openapi.paths) {
|
|
216
|
+
for (const pathKey of Object.keys(openapi.paths)) {
|
|
217
|
+
allUniquePaths.add(pathKey);
|
|
218
218
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
219
|
+
}
|
|
220
|
+
// Analyze server distribution and place servers at appropriate levels
|
|
221
|
+
const serverPlacement = analyzeServerDistribution(allServerUsage, allUniquePaths);
|
|
222
|
+
// Add servers to document level
|
|
223
|
+
if (serverPlacement.document.length > 0) {
|
|
224
|
+
openapi.servers = serverPlacement.document;
|
|
225
|
+
}
|
|
226
|
+
// Add servers to path items
|
|
227
|
+
if (openapi.paths) {
|
|
228
|
+
for (const [path, servers] of serverPlacement.pathItems.entries()) {
|
|
229
|
+
const normalizedPathKey = normalizePath(path);
|
|
230
|
+
const pathItem = openapi.paths[normalizedPathKey];
|
|
231
|
+
if (pathItem) {
|
|
232
|
+
pathItem.servers = servers;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// Add servers to operations
|
|
236
|
+
for (const [path, methods] of serverPlacement.operations.entries()) {
|
|
237
|
+
const normalizedPathKey = normalizePath(path);
|
|
238
|
+
const pathItem = openapi.paths[normalizedPathKey];
|
|
239
|
+
if (!pathItem) {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
for (const [method, servers] of methods.entries()) {
|
|
243
|
+
if (method in pathItem) {
|
|
244
|
+
const operation = pathItem[method];
|
|
245
|
+
if (operation && typeof operation === 'object' && 'responses' in operation) {
|
|
246
|
+
operation.servers = servers;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// Clean up the generated paths
|
|
253
|
+
if (openapi.paths) {
|
|
254
|
+
cleanupOperations(openapi.paths);
|
|
255
|
+
}
|
|
256
|
+
// Remove empty components object
|
|
257
|
+
if (Object.keys(openapi.components || {}).length === 0) {
|
|
258
|
+
delete openapi.components;
|
|
259
|
+
}
|
|
260
|
+
return pruneDocument(openapi);
|
|
229
261
|
}
|
|
230
|
-
export {
|
|
231
|
-
convert
|
|
232
|
-
};
|
|
233
|
-
//# sourceMappingURL=convert.js.map
|