@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.
Files changed (69) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/convert.d.ts +29 -1
  3. package/dist/convert.d.ts.map +1 -1
  4. package/dist/convert.js +375 -215
  5. package/dist/helpers/auth.js +116 -92
  6. package/dist/helpers/contact.js +24 -20
  7. package/dist/helpers/external-docs.js +33 -25
  8. package/dist/helpers/form-data.js +42 -36
  9. package/dist/helpers/generate-unique-value.d.ts +23 -0
  10. package/dist/helpers/generate-unique-value.d.ts.map +1 -0
  11. package/dist/helpers/generate-unique-value.js +29 -0
  12. package/dist/helpers/get-operation-examples.d.ts +40 -0
  13. package/dist/helpers/get-operation-examples.d.ts.map +1 -0
  14. package/dist/helpers/get-operation-examples.js +76 -0
  15. package/dist/helpers/license.js +21 -17
  16. package/dist/helpers/logo.js +22 -21
  17. package/dist/helpers/markdown.js +33 -30
  18. package/dist/helpers/merge-operation.d.ts +55 -0
  19. package/dist/helpers/merge-operation.d.ts.map +1 -0
  20. package/dist/helpers/merge-operation.js +125 -0
  21. package/dist/helpers/merge-path-item.d.ts +5 -0
  22. package/dist/helpers/merge-path-item.d.ts.map +1 -0
  23. package/dist/helpers/merge-path-item.js +37 -0
  24. package/dist/helpers/parameters.d.ts +2 -2
  25. package/dist/helpers/parameters.d.ts.map +1 -1
  26. package/dist/helpers/parameters.js +124 -96
  27. package/dist/helpers/path-items.d.ts +1 -1
  28. package/dist/helpers/path-items.d.ts.map +1 -1
  29. package/dist/helpers/path-items.js +245 -202
  30. package/dist/helpers/post-response-scripts.js +12 -12
  31. package/dist/helpers/pre-request-scripts.js +12 -12
  32. package/dist/helpers/prune-document.js +42 -35
  33. package/dist/helpers/rename-operation-example.d.ts +40 -0
  34. package/dist/helpers/rename-operation-example.d.ts.map +1 -0
  35. package/dist/helpers/rename-operation-example.js +61 -0
  36. package/dist/helpers/request-body.d.ts +1 -1
  37. package/dist/helpers/request-body.d.ts.map +1 -1
  38. package/dist/helpers/request-body.js +118 -94
  39. package/dist/helpers/responses.js +62 -57
  40. package/dist/helpers/schemas.js +43 -37
  41. package/dist/helpers/servers.js +83 -57
  42. package/dist/helpers/status-codes.js +40 -30
  43. package/dist/helpers/urls.js +74 -51
  44. package/dist/index.d.ts +2 -1
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +2 -5
  47. package/dist/types.js +1 -1
  48. package/package.json +7 -11
  49. package/dist/convert.js.map +0 -7
  50. package/dist/helpers/auth.js.map +0 -7
  51. package/dist/helpers/contact.js.map +0 -7
  52. package/dist/helpers/external-docs.js.map +0 -7
  53. package/dist/helpers/form-data.js.map +0 -7
  54. package/dist/helpers/license.js.map +0 -7
  55. package/dist/helpers/logo.js.map +0 -7
  56. package/dist/helpers/markdown.js.map +0 -7
  57. package/dist/helpers/parameters.js.map +0 -7
  58. package/dist/helpers/path-items.js.map +0 -7
  59. package/dist/helpers/post-response-scripts.js.map +0 -7
  60. package/dist/helpers/pre-request-scripts.js.map +0 -7
  61. package/dist/helpers/prune-document.js.map +0 -7
  62. package/dist/helpers/request-body.js.map +0 -7
  63. package/dist/helpers/responses.js.map +0 -7
  64. package/dist/helpers/schemas.js.map +0 -7
  65. package/dist/helpers/servers.js.map +0 -7
  66. package/dist/helpers/status-codes.js.map +0 -7
  67. package/dist/helpers/urls.js.map +0 -7
  68. package/dist/index.js.map +0 -7
  69. 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
@@ -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;AAYxD,OAAO,KAAK,EAAgC,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAkL9E;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,iBAAiB,EAAE,iBAAiB,GAAG,MAAM,GAAG,WAAW,CAAC,QAAQ,CA6I3F"}
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 "./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
- const OPERATION_KEYS = [
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
- if (typeof description === "string") {
22
- return description;
23
- }
24
- return description?.content;
12
+ if (typeof description === 'string') {
13
+ return description;
14
+ }
15
+ return description?.content;
25
16
  };
26
17
  const parseCollectionInput = (postmanCollection) => {
27
- if (typeof postmanCollection !== "string") {
28
- return postmanCollection;
29
- }
30
- try {
31
- return JSON.parse(postmanCollection);
32
- } catch (error) {
33
- const details = error instanceof Error ? error.message : "Unknown parse error";
34
- const parseError = new Error(`Invalid Postman collection JSON: ${details}`);
35
- parseError.name = "PostmanCollectionParseError";
36
- throw parseError;
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
- if (!collection || typeof collection !== "object") {
41
- throw new Error("Invalid Postman collection: expected an object");
42
- }
43
- const candidate = collection;
44
- if (!candidate.info) {
45
- throw new Error("Missing required info on Postman collection");
46
- }
47
- if (!candidate.item || !Array.isArray(candidate.item)) {
48
- throw new Error("Invalid Postman collection: item must be an array");
49
- }
50
- if (typeof candidate.info !== "object") {
51
- throw new Error("Invalid Postman collection: info must be an object");
52
- }
53
- if (!candidate.info.name) {
54
- throw new Error("Missing required info.name on Postman collection");
55
- }
56
- if (!candidate.info.schema) {
57
- throw new Error("Invalid Postman collection: missing info.schema");
58
- }
59
- if (candidate.variable && !Array.isArray(candidate.variable)) {
60
- throw new Error("Invalid Postman collection: variable must be an array when provided");
61
- }
62
- return candidate;
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
- const isItemGroup = (item) => "item" in item && Array.isArray(item.item);
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
- const collectTags = (item, parentPath = "") => {
67
- if (!isItemGroup(item)) {
68
- return [];
69
- }
70
- const nextPath = item.name ? parentPath ? `${parentPath} > ${item.name}` : item.name : parentPath;
71
- const description = normalizeDescription(item.description);
72
- const currentTag = item.name?.length ? [
73
- {
74
- name: nextPath,
75
- ...description && { description }
76
- }
77
- ] : [];
78
- return [...currentTag, ...item.item.flatMap((subItem) => collectTags(subItem, nextPath))];
79
- };
80
- return items.flatMap((item) => collectTags(item));
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
- if (!securitySchemes || Object.keys(securitySchemes).length === 0) {
84
- return;
85
- }
86
- openapi.components = openapi.components || {};
87
- openapi.components.securitySchemes = {
88
- ...openapi.components.securitySchemes ?? {},
89
- ...securitySchemes
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 mergePathItem = (paths, normalizedPathKey, pathItem) => {
93
- const targetPath = paths[normalizedPathKey] ?? {};
94
- for (const [key, value] of Object.entries(pathItem)) {
95
- if (value === void 0) {
96
- continue;
97
- }
98
- const isOperationKey = OPERATION_KEYS.includes(key);
99
- if (isOperationKey && targetPath[key]) {
100
- const operationName = typeof key === "string" ? key.toUpperCase() : String(key);
101
- console.warn(
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 cleanupOperations = (paths) => {
110
- Object.values(paths).forEach((pathItem) => {
111
- if (!pathItem) {
112
- return;
191
+ const mergeTagsIntoDocument = (openapi, incoming) => {
192
+ if (incoming.length === 0) {
193
+ return;
113
194
  }
114
- OPERATION_KEYS.forEach((operationKey) => {
115
- const operation = pathItem[operationKey];
116
- if (!operation) {
195
+ const existing = openapi.tags ?? [];
196
+ if (existing.length === 0) {
197
+ openapi.tags = incoming;
117
198
  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
- }
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
- function convert(postmanCollection) {
138
- const collection = validateCollectionShape(parseCollectionInput(postmanCollection));
139
- const title = collection.info.name || "API";
140
- const version = collection.variable?.find((v) => v.key === "version")?.value || "1.0.0";
141
- const description = normalizeDescription(collection.info.description) || "";
142
- const license = processLicense(collection);
143
- const contact = processContact(collection);
144
- const logo = processLogo(collection);
145
- const openapi = {
146
- openapi: "3.1.0",
147
- info: {
148
- title,
149
- version,
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
- continue;
218
+ return;
180
219
  }
181
- mergePathItem(openapi.paths, normalizedPathKey, pathItem);
182
- }
183
- if (itemComponents?.securitySchemes) {
184
- mergeSecuritySchemes(openapi, itemComponents.securitySchemes);
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
- //# sourceMappingURL=convert.js.map
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
+ }