@scalar/postman-to-openapi 0.5.2 → 0.6.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 +12 -0
- package/dist/convert.d.ts +29 -1
- package/dist/convert.d.ts.map +1 -1
- package/dist/convert.js +375 -215
- 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/generate-unique-value.d.ts +23 -0
- package/dist/helpers/generate-unique-value.d.ts.map +1 -0
- package/dist/helpers/generate-unique-value.js +29 -0
- package/dist/helpers/get-operation-examples.d.ts +40 -0
- package/dist/helpers/get-operation-examples.d.ts.map +1 -0
- package/dist/helpers/get-operation-examples.js +76 -0
- package/dist/helpers/license.js +21 -17
- package/dist/helpers/logo.js +22 -21
- package/dist/helpers/markdown.js +33 -30
- package/dist/helpers/merge-operation.d.ts +55 -0
- package/dist/helpers/merge-operation.d.ts.map +1 -0
- package/dist/helpers/merge-operation.js +125 -0
- package/dist/helpers/merge-path-item.d.ts +5 -0
- package/dist/helpers/merge-path-item.d.ts.map +1 -0
- package/dist/helpers/merge-path-item.js +37 -0
- package/dist/helpers/parameters.d.ts +2 -2
- package/dist/helpers/parameters.d.ts.map +1 -1
- package/dist/helpers/parameters.js +124 -96
- package/dist/helpers/path-items.d.ts +1 -1
- package/dist/helpers/path-items.d.ts.map +1 -1
- package/dist/helpers/path-items.js +245 -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/rename-operation-example.d.ts +40 -0
- package/dist/helpers/rename-operation-example.d.ts.map +1 -0
- package/dist/helpers/rename-operation-example.js +61 -0
- package/dist/helpers/request-body.d.ts +1 -1
- package/dist/helpers/request-body.d.ts.map +1 -1
- package/dist/helpers/request-body.js +118 -94
- 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.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -5
- package/dist/types.js +1 -1
- package/package.json +7 -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,17 @@
|
|
|
1
1
|
# @scalar/postman-to-openapi
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#8511](https://github.com/scalar/scalar/pull/8511): feat: support duplicate operations
|
|
8
|
+
|
|
9
|
+
## 0.5.3
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [#8466](https://github.com/scalar/scalar/pull/8466): chore: new build pipeline
|
|
14
|
+
|
|
3
15
|
## 0.5.2
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/dist/convert.d.ts
CHANGED
|
@@ -1,9 +1,37 @@
|
|
|
1
1
|
import type { OpenAPIV3_1 } from '@scalar/openapi-types';
|
|
2
2
|
import type { PostmanCollection } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Indices from the collection root into nested `item` arrays.
|
|
5
|
+
* Example: `[0, 2, 1]` → `collection.item[0].item[2].item[1]`.
|
|
6
|
+
*/
|
|
7
|
+
export type PostmanRequestIndexPath = readonly number[];
|
|
8
|
+
export type ConvertOptions = {
|
|
9
|
+
/**
|
|
10
|
+
* Whether to merge operations with the same path and method.
|
|
11
|
+
* If true, the operations will be merged into a single operation.
|
|
12
|
+
* If false, the operations will be kept as separate operations.
|
|
13
|
+
* Default is true.
|
|
14
|
+
*/
|
|
15
|
+
mergeOperation?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* When set, only items at these paths are converted. Each path is a list of
|
|
18
|
+
* zero-based indices from `collection.item` through nested `item` arrays.
|
|
19
|
+
* The last index may point to a request or a folder; folders include every
|
|
20
|
+
* descendant request. Paths out of range or through non-folders are skipped.
|
|
21
|
+
* When omitted, the whole collection is converted (existing behavior).
|
|
22
|
+
*/
|
|
23
|
+
requestIndexPaths?: readonly PostmanRequestIndexPath[];
|
|
24
|
+
/**
|
|
25
|
+
* Existing OpenAPI document to merge into. The input is is updated with Postman paths,
|
|
26
|
+
* tags (union by name), security schemes, and servers.
|
|
27
|
+
* Root `info` and existing paths are preserved unless Postman adds or merges operations.
|
|
28
|
+
*/
|
|
29
|
+
document?: OpenAPIV3_1.Document;
|
|
30
|
+
};
|
|
3
31
|
/**
|
|
4
32
|
* Converts a Postman Collection to an OpenAPI 3.1.0 document.
|
|
5
33
|
* This function processes the collection's information, servers, authentication,
|
|
6
34
|
* and items to create a corresponding OpenAPI structure.
|
|
7
35
|
*/
|
|
8
|
-
export declare function convert(postmanCollection: PostmanCollection | string): OpenAPIV3_1.Document;
|
|
36
|
+
export declare function convert(postmanCollection: PostmanCollection | string, options?: ConvertOptions): OpenAPIV3_1.Document;
|
|
9
37
|
//# sourceMappingURL=convert.d.ts.map
|
package/dist/convert.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"convert.d.ts","sourceRoot":"","sources":["../src/convert.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"convert.d.ts","sourceRoot":"","sources":["../src/convert.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAaxD,OAAO,KAAK,EAAgC,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAE9E;;;GAGG;AACH,MAAM,MAAM,uBAAuB,GAAG,SAAS,MAAM,EAAE,CAAA;AA4SvD,MAAM,MAAM,cAAc,GAAG;IAC3B;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;;;;OAMG;IACH,iBAAiB,CAAC,EAAE,SAAS,uBAAuB,EAAE,CAAA;IACtD;;;;OAIG;IACH,QAAQ,CAAC,EAAE,WAAW,CAAC,QAAQ,CAAA;CAChC,CAAA;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CACrB,iBAAiB,EAAE,iBAAiB,GAAG,MAAM,EAC7C,OAAO,GAAE,cAA0C,GAClD,WAAW,CAAC,QAAQ,CAwLtB"}
|
package/dist/convert.js
CHANGED
|
@@ -1,233 +1,393 @@
|
|
|
1
|
-
import { processAuth } from
|
|
2
|
-
import { processContact } from
|
|
3
|
-
import { processExternalDocs } from
|
|
4
|
-
import { processLicense } from
|
|
5
|
-
import { processLogo } from
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
"get",
|
|
12
|
-
"put",
|
|
13
|
-
"post",
|
|
14
|
-
"delete",
|
|
15
|
-
"options",
|
|
16
|
-
"head",
|
|
17
|
-
"patch",
|
|
18
|
-
"trace"
|
|
19
|
-
];
|
|
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 { DEFAULT_EXAMPLE_NAME, OPERATION_KEYS, mergePathItem } from './helpers/merge-path-item.js';
|
|
7
|
+
import { processItem } from './helpers/path-items.js';
|
|
8
|
+
import { pruneDocument } from './helpers/prune-document.js';
|
|
9
|
+
import { analyzeServerDistribution } from './helpers/servers.js';
|
|
10
|
+
import { normalizePath } from './helpers/urls.js';
|
|
20
11
|
const normalizeDescription = (description) => {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
12
|
+
if (typeof description === 'string') {
|
|
13
|
+
return description;
|
|
14
|
+
}
|
|
15
|
+
return description?.content;
|
|
25
16
|
};
|
|
26
17
|
const parseCollectionInput = (postmanCollection) => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
18
|
+
if (typeof postmanCollection !== 'string') {
|
|
19
|
+
return postmanCollection;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(postmanCollection);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
const details = error instanceof Error ? error.message : 'Unknown parse error';
|
|
26
|
+
const parseError = new Error(`Invalid Postman collection JSON: ${details}`);
|
|
27
|
+
parseError.name = 'PostmanCollectionParseError';
|
|
28
|
+
throw parseError;
|
|
29
|
+
}
|
|
38
30
|
};
|
|
39
31
|
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
|
-
|
|
32
|
+
if (!collection || typeof collection !== 'object') {
|
|
33
|
+
throw new Error('Invalid Postman collection: expected an object');
|
|
34
|
+
}
|
|
35
|
+
const candidate = collection;
|
|
36
|
+
if (!candidate.info) {
|
|
37
|
+
throw new Error('Missing required info on Postman collection');
|
|
38
|
+
}
|
|
39
|
+
if (!candidate.item || !Array.isArray(candidate.item)) {
|
|
40
|
+
throw new Error('Invalid Postman collection: item must be an array');
|
|
41
|
+
}
|
|
42
|
+
if (typeof candidate.info !== 'object') {
|
|
43
|
+
throw new Error('Invalid Postman collection: info must be an object');
|
|
44
|
+
}
|
|
45
|
+
if (!candidate.info.name) {
|
|
46
|
+
throw new Error('Missing required info.name on Postman collection');
|
|
47
|
+
}
|
|
48
|
+
if (!candidate.info.schema) {
|
|
49
|
+
throw new Error('Invalid Postman collection: missing info.schema');
|
|
50
|
+
}
|
|
51
|
+
if (candidate.variable && !Array.isArray(candidate.variable)) {
|
|
52
|
+
throw new Error('Invalid Postman collection: variable must be an array when provided');
|
|
53
|
+
}
|
|
54
|
+
return candidate;
|
|
63
55
|
};
|
|
64
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Extracts tags from Postman collection folders.
|
|
58
|
+
* We keep folder nesting using " > " so tag names stay readable while preserving hierarchy.
|
|
59
|
+
* Requests do not produce tags; only folders are reflected as tags.
|
|
60
|
+
*/
|
|
61
|
+
const isItemGroup = (item) => 'item' in item && Array.isArray(item.item);
|
|
65
62
|
const extractTags = (items) => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
63
|
+
const collectTags = (item, parentPath = '') => {
|
|
64
|
+
if (!isItemGroup(item)) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
const nextPath = item.name ? (parentPath ? `${parentPath} > ${item.name}` : item.name) : parentPath;
|
|
68
|
+
const description = normalizeDescription(item.description);
|
|
69
|
+
const currentTag = item.name?.length
|
|
70
|
+
? [
|
|
71
|
+
{
|
|
72
|
+
name: nextPath,
|
|
73
|
+
...(description && { description }),
|
|
74
|
+
},
|
|
75
|
+
]
|
|
76
|
+
: [];
|
|
77
|
+
return [...currentTag, ...item.item.flatMap((subItem) => collectTags(subItem, nextPath))];
|
|
78
|
+
};
|
|
79
|
+
return items.flatMap((item) => collectTags(item));
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Folder tags for ancestors of each selected path only (same shape as {@link extractTags}).
|
|
83
|
+
*/
|
|
84
|
+
const extractTagsForSelectedPaths = (items, paths) => {
|
|
85
|
+
const seen = new Set();
|
|
86
|
+
const result = [];
|
|
87
|
+
for (const path of paths) {
|
|
88
|
+
if (path.length === 0) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
let list = items;
|
|
92
|
+
let parentPath = '';
|
|
93
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
94
|
+
const idx = path[i];
|
|
95
|
+
if (idx === undefined || idx < 0 || idx >= list.length) {
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
const node = list[idx];
|
|
99
|
+
if (node === undefined || !isItemGroup(node)) {
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
const nextPath = node.name ? (parentPath ? `${parentPath} > ${node.name}` : node.name) : parentPath;
|
|
103
|
+
const description = normalizeDescription(node.description);
|
|
104
|
+
if (node.name?.length && !seen.has(nextPath)) {
|
|
105
|
+
seen.add(nextPath);
|
|
106
|
+
result.push({
|
|
107
|
+
name: nextPath,
|
|
108
|
+
...(description && { description }),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
parentPath = nextPath;
|
|
112
|
+
list = node.item;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
116
|
+
};
|
|
117
|
+
const getNodeAtPath = (items, path) => {
|
|
118
|
+
if (path.length === 0) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
let list = items;
|
|
122
|
+
let node;
|
|
123
|
+
for (let i = 0; i < path.length; i++) {
|
|
124
|
+
const idx = path[i];
|
|
125
|
+
if (idx === undefined || idx < 0 || idx >= list.length) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
node = list[idx];
|
|
129
|
+
if (node === undefined) {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
if (i < path.length - 1) {
|
|
133
|
+
if (!isItemGroup(node)) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
list = node.item;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return node;
|
|
140
|
+
};
|
|
141
|
+
const collectParentTagSegments = (items, path) => {
|
|
142
|
+
const segments = [];
|
|
143
|
+
if (path.length <= 1) {
|
|
144
|
+
return segments;
|
|
145
|
+
}
|
|
146
|
+
let list = items;
|
|
147
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
148
|
+
const idx = path[i];
|
|
149
|
+
if (idx === undefined || idx < 0 || idx >= list.length) {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
const node = list[idx];
|
|
153
|
+
if (node === undefined || !isItemGroup(node)) {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
if (node.name) {
|
|
157
|
+
segments.push(node.name);
|
|
158
|
+
}
|
|
159
|
+
list = node.item;
|
|
160
|
+
}
|
|
161
|
+
return segments;
|
|
162
|
+
};
|
|
163
|
+
const dedupeIndexPaths = (paths) => {
|
|
164
|
+
const map = new Map();
|
|
165
|
+
for (const path of paths) {
|
|
166
|
+
map.set(JSON.stringify([...path]), path);
|
|
167
|
+
}
|
|
168
|
+
return [...map.values()];
|
|
81
169
|
};
|
|
82
170
|
const mergeSecuritySchemes = (openapi, securitySchemes) => {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
171
|
+
if (!securitySchemes || Object.keys(securitySchemes).length === 0) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
openapi.components = openapi.components || {};
|
|
175
|
+
openapi.components.securitySchemes = {
|
|
176
|
+
...(openapi.components.securitySchemes ?? {}),
|
|
177
|
+
...securitySchemes,
|
|
178
|
+
};
|
|
91
179
|
};
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
`Duplicate operation detected for ${operationName} ${normalizedPathKey}. Last operation will overwrite previous.`
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
targetPath[key] = value;
|
|
106
|
-
}
|
|
107
|
-
paths[normalizedPathKey] = targetPath;
|
|
180
|
+
const mergeServerLists = (existing, incoming) => {
|
|
181
|
+
const seen = new Set((existing ?? []).map((s) => s.url));
|
|
182
|
+
const out = [...(existing ?? [])];
|
|
183
|
+
for (const server of incoming) {
|
|
184
|
+
if (!seen.has(server.url)) {
|
|
185
|
+
seen.add(server.url);
|
|
186
|
+
out.push(server);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
108
190
|
};
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return;
|
|
191
|
+
const mergeTagsIntoDocument = (openapi, incoming) => {
|
|
192
|
+
if (incoming.length === 0) {
|
|
193
|
+
return;
|
|
113
194
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
195
|
+
const existing = openapi.tags ?? [];
|
|
196
|
+
if (existing.length === 0) {
|
|
197
|
+
openapi.tags = incoming;
|
|
117
198
|
return;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
if (!operation.description) {
|
|
132
|
-
delete operation.description;
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
});
|
|
199
|
+
}
|
|
200
|
+
const names = new Set(existing.map((t) => t.name));
|
|
201
|
+
const additions = incoming.filter((t) => t.name && !names.has(t.name));
|
|
202
|
+
openapi.tags = additions.length > 0 ? [...existing, ...additions] : existing;
|
|
136
203
|
};
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
...description && { description },
|
|
151
|
-
...license && { license },
|
|
152
|
-
...contact && { contact },
|
|
153
|
-
...logo && { "x-logo": logo }
|
|
154
|
-
},
|
|
155
|
-
paths: {}
|
|
156
|
-
};
|
|
157
|
-
const externalDocs = processExternalDocs(collection);
|
|
158
|
-
if (externalDocs) {
|
|
159
|
-
openapi.externalDocs = externalDocs;
|
|
160
|
-
}
|
|
161
|
-
if (collection.auth) {
|
|
162
|
-
const { securitySchemes, security } = processAuth(collection.auth);
|
|
163
|
-
mergeSecuritySchemes(openapi, securitySchemes);
|
|
164
|
-
openapi.security = security;
|
|
165
|
-
}
|
|
166
|
-
const allServerUsage = [];
|
|
167
|
-
if (collection.item) {
|
|
168
|
-
const tags = extractTags(collection.item);
|
|
169
|
-
if (tags.length > 0) {
|
|
170
|
-
openapi.tags = tags;
|
|
171
|
-
}
|
|
172
|
-
collection.item.forEach((item) => {
|
|
173
|
-
const { paths: itemPaths, components: itemComponents, serverUsage } = processItem(item);
|
|
174
|
-
allServerUsage.push(...serverUsage);
|
|
175
|
-
openapi.paths = openapi.paths || {};
|
|
176
|
-
for (const [pathKey, pathItem] of Object.entries(itemPaths)) {
|
|
177
|
-
const normalizedPathKey = normalizePath(pathKey);
|
|
204
|
+
const assignTagsFromPostman = (openapi, tags, isMergingIntoBase) => {
|
|
205
|
+
if (tags.length === 0) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (isMergingIntoBase) {
|
|
209
|
+
mergeTagsIntoDocument(openapi, tags);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
openapi.tags = tags;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
const cleanupOperations = (paths) => {
|
|
216
|
+
Object.values(paths).forEach((pathItem) => {
|
|
178
217
|
if (!pathItem) {
|
|
179
|
-
|
|
218
|
+
return;
|
|
180
219
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
220
|
+
OPERATION_KEYS.forEach((operationKey) => {
|
|
221
|
+
const operation = pathItem[operationKey];
|
|
222
|
+
if (!operation) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if ('parameters' in operation && operation.parameters?.length === 0) {
|
|
226
|
+
delete operation.parameters;
|
|
227
|
+
}
|
|
228
|
+
if ('requestBody' in operation && operation.requestBody && 'content' in operation.requestBody) {
|
|
229
|
+
const content = operation.requestBody.content;
|
|
230
|
+
if (content && 'text/plain' in content) {
|
|
231
|
+
const text = content['text/plain'];
|
|
232
|
+
if (!text?.schema || (text.schema && Object.keys(text.schema).length === 0)) {
|
|
233
|
+
content['text/plain'] = {};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (!operation.description) {
|
|
238
|
+
delete operation.description;
|
|
239
|
+
}
|
|
240
|
+
});
|
|
186
241
|
});
|
|
187
|
-
}
|
|
188
|
-
const allUniquePaths = /* @__PURE__ */ new Set();
|
|
189
|
-
if (openapi.paths) {
|
|
190
|
-
for (const pathKey of Object.keys(openapi.paths)) {
|
|
191
|
-
allUniquePaths.add(pathKey);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
const serverPlacement = analyzeServerDistribution(allServerUsage, allUniquePaths);
|
|
195
|
-
if (serverPlacement.document.length > 0) {
|
|
196
|
-
openapi.servers = serverPlacement.document;
|
|
197
|
-
}
|
|
198
|
-
if (openapi.paths) {
|
|
199
|
-
for (const [path, servers] of serverPlacement.pathItems.entries()) {
|
|
200
|
-
const normalizedPathKey = normalizePath(path);
|
|
201
|
-
const pathItem = openapi.paths[normalizedPathKey];
|
|
202
|
-
if (pathItem) {
|
|
203
|
-
pathItem.servers = servers;
|
|
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
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
if (openapi.paths) {
|
|
223
|
-
cleanupOperations(openapi.paths);
|
|
224
|
-
}
|
|
225
|
-
if (Object.keys(openapi.components || {}).length === 0) {
|
|
226
|
-
delete openapi.components;
|
|
227
|
-
}
|
|
228
|
-
return pruneDocument(openapi);
|
|
229
|
-
}
|
|
230
|
-
export {
|
|
231
|
-
convert
|
|
232
242
|
};
|
|
233
|
-
|
|
243
|
+
/**
|
|
244
|
+
* Converts a Postman Collection to an OpenAPI 3.1.0 document.
|
|
245
|
+
* This function processes the collection's information, servers, authentication,
|
|
246
|
+
* and items to create a corresponding OpenAPI structure.
|
|
247
|
+
*/
|
|
248
|
+
export function convert(postmanCollection, options = { mergeOperation: false }) {
|
|
249
|
+
const { requestIndexPaths, mergeOperation = false, document: baseDocument } = options;
|
|
250
|
+
const isMergingIntoBase = baseDocument !== undefined;
|
|
251
|
+
const collection = validateCollectionShape(parseCollectionInput(postmanCollection));
|
|
252
|
+
// Extract title from collection info, fallback to 'API' if not provided
|
|
253
|
+
const title = collection.info.name || 'API';
|
|
254
|
+
// Look for version in collection variables, default to '1.0.0'
|
|
255
|
+
const version = collection.variable?.find((v) => v.key === 'version')?.value || '1.0.0';
|
|
256
|
+
// Handle different description formats in Postman
|
|
257
|
+
const description = normalizeDescription(collection.info.description) || '';
|
|
258
|
+
// Process license and contact information
|
|
259
|
+
const license = processLicense(collection);
|
|
260
|
+
const contact = processContact(collection);
|
|
261
|
+
// Process logo information
|
|
262
|
+
const logo = processLogo(collection);
|
|
263
|
+
// Initialize the OpenAPI document with required fields (or clone a base document to merge into)
|
|
264
|
+
const openapi = baseDocument ?? {
|
|
265
|
+
openapi: '3.1.0',
|
|
266
|
+
info: {
|
|
267
|
+
title,
|
|
268
|
+
version,
|
|
269
|
+
...(description && { description }),
|
|
270
|
+
...(license && { license }),
|
|
271
|
+
...(contact && { contact }),
|
|
272
|
+
...(logo && { 'x-logo': logo }),
|
|
273
|
+
},
|
|
274
|
+
paths: {},
|
|
275
|
+
};
|
|
276
|
+
openapi.paths = openapi.paths ?? {};
|
|
277
|
+
// Process external docs
|
|
278
|
+
const externalDocs = processExternalDocs(collection);
|
|
279
|
+
if (externalDocs && (!isMergingIntoBase || !openapi.externalDocs)) {
|
|
280
|
+
openapi.externalDocs = externalDocs;
|
|
281
|
+
}
|
|
282
|
+
// Process authentication if present in the collection
|
|
283
|
+
if (collection.auth) {
|
|
284
|
+
const { securitySchemes, security } = processAuth(collection.auth);
|
|
285
|
+
mergeSecuritySchemes(openapi, securitySchemes);
|
|
286
|
+
if (security?.length) {
|
|
287
|
+
if (isMergingIntoBase && openapi.security?.length) {
|
|
288
|
+
openapi.security = [...openapi.security, ...security];
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
openapi.security = security;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// Process each item in the collection and merge into OpenAPI spec
|
|
296
|
+
const allServerUsage = [];
|
|
297
|
+
if (collection.item) {
|
|
298
|
+
const usePathFilter = requestIndexPaths !== undefined;
|
|
299
|
+
if (usePathFilter) {
|
|
300
|
+
const uniquePaths = dedupeIndexPaths(requestIndexPaths);
|
|
301
|
+
const tags = extractTagsForSelectedPaths(collection.item, uniquePaths);
|
|
302
|
+
assignTagsFromPostman(openapi, tags, isMergingIntoBase);
|
|
303
|
+
for (const path of uniquePaths) {
|
|
304
|
+
const node = getNodeAtPath(collection.item, path);
|
|
305
|
+
if (!node) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const parentTags = collectParentTagSegments(collection.item, path);
|
|
309
|
+
const { paths: itemPaths, components: itemComponents, serverUsage, } = processItem(node, DEFAULT_EXAMPLE_NAME, parentTags, '');
|
|
310
|
+
allServerUsage.push(...serverUsage);
|
|
311
|
+
for (const [pathKey, pathItem] of Object.entries(itemPaths)) {
|
|
312
|
+
const normalizedPathKey = normalizePath(pathKey);
|
|
313
|
+
if (!pathItem) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
mergePathItem(openapi.paths, normalizedPathKey, pathItem, mergeOperation);
|
|
317
|
+
}
|
|
318
|
+
if (itemComponents?.securitySchemes) {
|
|
319
|
+
mergeSecuritySchemes(openapi, itemComponents.securitySchemes);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
const tags = extractTags(collection.item);
|
|
325
|
+
assignTagsFromPostman(openapi, tags, isMergingIntoBase);
|
|
326
|
+
collection.item.forEach((item) => {
|
|
327
|
+
const { paths: itemPaths, components: itemComponents, serverUsage } = processItem(item, DEFAULT_EXAMPLE_NAME);
|
|
328
|
+
allServerUsage.push(...serverUsage);
|
|
329
|
+
openapi.paths = openapi.paths || {};
|
|
330
|
+
for (const [pathKey, pathItem] of Object.entries(itemPaths)) {
|
|
331
|
+
const normalizedPathKey = normalizePath(pathKey);
|
|
332
|
+
if (!pathItem) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
mergePathItem(openapi.paths, normalizedPathKey, pathItem, mergeOperation);
|
|
336
|
+
}
|
|
337
|
+
if (itemComponents?.securitySchemes) {
|
|
338
|
+
mergeSecuritySchemes(openapi, itemComponents.securitySchemes);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// Extract all unique paths from the document
|
|
344
|
+
const allUniquePaths = new Set();
|
|
345
|
+
if (openapi.paths) {
|
|
346
|
+
for (const pathKey of Object.keys(openapi.paths)) {
|
|
347
|
+
allUniquePaths.add(pathKey);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// Analyze server distribution and place servers at appropriate levels
|
|
351
|
+
const serverPlacement = analyzeServerDistribution(allServerUsage, allUniquePaths);
|
|
352
|
+
// Add servers to document level
|
|
353
|
+
if (serverPlacement.document.length > 0) {
|
|
354
|
+
openapi.servers = isMergingIntoBase
|
|
355
|
+
? mergeServerLists(openapi.servers, serverPlacement.document)
|
|
356
|
+
: serverPlacement.document;
|
|
357
|
+
}
|
|
358
|
+
// Add servers to path items
|
|
359
|
+
if (openapi.paths) {
|
|
360
|
+
for (const [path, servers] of serverPlacement.pathItems.entries()) {
|
|
361
|
+
const normalizedPathKey = normalizePath(path);
|
|
362
|
+
const pathItem = openapi.paths[normalizedPathKey];
|
|
363
|
+
if (pathItem) {
|
|
364
|
+
pathItem.servers = isMergingIntoBase ? mergeServerLists(pathItem.servers, servers) : servers;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
// Add servers to operations
|
|
368
|
+
for (const [path, methods] of serverPlacement.operations.entries()) {
|
|
369
|
+
const normalizedPathKey = normalizePath(path);
|
|
370
|
+
const pathItem = openapi.paths[normalizedPathKey];
|
|
371
|
+
if (!pathItem) {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
for (const [method, servers] of methods.entries()) {
|
|
375
|
+
if (method in pathItem) {
|
|
376
|
+
const operation = pathItem[method];
|
|
377
|
+
if (operation && typeof operation === 'object' && 'responses' in operation) {
|
|
378
|
+
operation.servers = isMergingIntoBase ? mergeServerLists(operation.servers, servers) : servers;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// Clean up the generated paths
|
|
385
|
+
if (openapi.paths) {
|
|
386
|
+
cleanupOperations(openapi.paths);
|
|
387
|
+
}
|
|
388
|
+
// Remove empty components object
|
|
389
|
+
if (Object.keys(openapi.components || {}).length === 0) {
|
|
390
|
+
delete openapi.components;
|
|
391
|
+
}
|
|
392
|
+
return pruneDocument(openapi);
|
|
393
|
+
}
|