@scalar/postman-to-openapi 0.5.3 → 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 CHANGED
@@ -1,5 +1,11 @@
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
+
3
9
  ## 0.5.3
4
10
 
5
11
  ### 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
@@ -3,20 +3,11 @@ import { processContact } from './helpers/contact.js';
3
3
  import { processExternalDocs } from './helpers/external-docs.js';
4
4
  import { processLicense } from './helpers/license.js';
5
5
  import { processLogo } from './helpers/logo.js';
6
+ import { DEFAULT_EXAMPLE_NAME, OPERATION_KEYS, mergePathItem } from './helpers/merge-path-item.js';
6
7
  import { processItem } from './helpers/path-items.js';
7
8
  import { pruneDocument } from './helpers/prune-document.js';
8
9
  import { analyzeServerDistribution } from './helpers/servers.js';
9
10
  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
- ];
20
11
  const normalizeDescription = (description) => {
21
12
  if (typeof description === 'string') {
22
13
  return description;
@@ -87,6 +78,95 @@ const extractTags = (items) => {
87
78
  };
88
79
  return items.flatMap((item) => collectTags(item));
89
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()];
169
+ };
90
170
  const mergeSecuritySchemes = (openapi, securitySchemes) => {
91
171
  if (!securitySchemes || Object.keys(securitySchemes).length === 0) {
92
172
  return;
@@ -97,20 +177,40 @@ const mergeSecuritySchemes = (openapi, securitySchemes) => {
97
177
  ...securitySchemes,
98
178
  };
99
179
  };
100
- const mergePathItem = (paths, normalizedPathKey, pathItem) => {
101
- const targetPath = (paths[normalizedPathKey] ?? {});
102
- for (const [key, value] of Object.entries(pathItem)) {
103
- if (value === undefined) {
104
- continue;
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);
105
187
  }
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
188
  }
113
- paths[normalizedPathKey] = targetPath;
189
+ return out;
190
+ };
191
+ const mergeTagsIntoDocument = (openapi, incoming) => {
192
+ if (incoming.length === 0) {
193
+ return;
194
+ }
195
+ const existing = openapi.tags ?? [];
196
+ if (existing.length === 0) {
197
+ openapi.tags = incoming;
198
+ return;
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;
203
+ };
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
+ }
114
214
  };
115
215
  const cleanupOperations = (paths) => {
116
216
  Object.values(paths).forEach((pathItem) => {
@@ -145,7 +245,9 @@ const cleanupOperations = (paths) => {
145
245
  * This function processes the collection's information, servers, authentication,
146
246
  * and items to create a corresponding OpenAPI structure.
147
247
  */
148
- export function convert(postmanCollection) {
248
+ export function convert(postmanCollection, options = { mergeOperation: false }) {
249
+ const { requestIndexPaths, mergeOperation = false, document: baseDocument } = options;
250
+ const isMergingIntoBase = baseDocument !== undefined;
149
251
  const collection = validateCollectionShape(parseCollectionInput(postmanCollection));
150
252
  // Extract title from collection info, fallback to 'API' if not provided
151
253
  const title = collection.info.name || 'API';
@@ -158,8 +260,8 @@ export function convert(postmanCollection) {
158
260
  const contact = processContact(collection);
159
261
  // Process logo information
160
262
  const logo = processLogo(collection);
161
- // Initialize the OpenAPI document with required fields
162
- const openapi = {
263
+ // Initialize the OpenAPI document with required fields (or clone a base document to merge into)
264
+ const openapi = baseDocument ?? {
163
265
  openapi: '3.1.0',
164
266
  info: {
165
267
  title,
@@ -171,44 +273,72 @@ export function convert(postmanCollection) {
171
273
  },
172
274
  paths: {},
173
275
  };
276
+ openapi.paths = openapi.paths ?? {};
174
277
  // Process external docs
175
278
  const externalDocs = processExternalDocs(collection);
176
- if (externalDocs) {
279
+ if (externalDocs && (!isMergingIntoBase || !openapi.externalDocs)) {
177
280
  openapi.externalDocs = externalDocs;
178
281
  }
179
282
  // Process authentication if present in the collection
180
283
  if (collection.auth) {
181
284
  const { securitySchemes, security } = processAuth(collection.auth);
182
285
  mergeSecuritySchemes(openapi, securitySchemes);
183
- openapi.security = security;
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
+ }
184
294
  }
185
295
  // Process each item in the collection and merge into OpenAPI spec
186
296
  const allServerUsage = [];
187
297
  if (collection.item) {
188
- // Extract tags from folders
189
- const tags = extractTags(collection.item);
190
- if (tags.length > 0) {
191
- openapi.tags = tags;
192
- }
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) {
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) {
203
306
  continue;
204
307
  }
205
- mergePathItem(openapi.paths, normalizedPathKey, pathItem);
206
- }
207
- // Merge security schemes from the current item
208
- if (itemComponents?.securitySchemes) {
209
- mergeSecuritySchemes(openapi, itemComponents.securitySchemes);
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
+ }
210
321
  }
211
- });
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
+ }
212
342
  }
213
343
  // Extract all unique paths from the document
214
344
  const allUniquePaths = new Set();
@@ -221,7 +351,9 @@ export function convert(postmanCollection) {
221
351
  const serverPlacement = analyzeServerDistribution(allServerUsage, allUniquePaths);
222
352
  // Add servers to document level
223
353
  if (serverPlacement.document.length > 0) {
224
- openapi.servers = serverPlacement.document;
354
+ openapi.servers = isMergingIntoBase
355
+ ? mergeServerLists(openapi.servers, serverPlacement.document)
356
+ : serverPlacement.document;
225
357
  }
226
358
  // Add servers to path items
227
359
  if (openapi.paths) {
@@ -229,7 +361,7 @@ export function convert(postmanCollection) {
229
361
  const normalizedPathKey = normalizePath(path);
230
362
  const pathItem = openapi.paths[normalizedPathKey];
231
363
  if (pathItem) {
232
- pathItem.servers = servers;
364
+ pathItem.servers = isMergingIntoBase ? mergeServerLists(pathItem.servers, servers) : servers;
233
365
  }
234
366
  }
235
367
  // Add servers to operations
@@ -243,7 +375,7 @@ export function convert(postmanCollection) {
243
375
  if (method in pathItem) {
244
376
  const operation = pathItem[method];
245
377
  if (operation && typeof operation === 'object' && 'responses' in operation) {
246
- operation.servers = servers;
378
+ operation.servers = isMergingIntoBase ? mergeServerLists(operation.servers, servers) : servers;
247
379
  }
248
380
  }
249
381
  }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Generates a unique string value based on a default value and a validation function.
3
+ * The function tries the default value first. If validation fails, it appends
4
+ * an incrementing suffix (optionally with a prefix) until the validation passes.
5
+ *
6
+ * @param defaultValue - The initial base string value to try.
7
+ * @param validation - A function that receives a candidate value and returns true if it is valid.
8
+ * @param prefix - Optional prefix to add before the incrementing number (default is '').
9
+ * @returns The first value that passes the validation.
10
+ *
11
+ * @example
12
+ * // Case 1: Simple unused value
13
+ * generateUniqueValue('example', v => v !== 'example') // → "example 2"
14
+ *
15
+ * // Case 2: Avoids taken values in a set
16
+ * const taken = new Set(['foo', 'foo 1', 'foo 2']);
17
+ * generateUniqueValue('foo', v => !taken.has(v)) // → "foo 3"
18
+ *
19
+ * // Case 3: Using a prefix
20
+ * generateUniqueValue('base', v => v === 'base__3', '__') // → "base__3"
21
+ */
22
+ export declare const generateUniqueValue: (defaultValue: string, validation: (value: string) => boolean, prefix?: string) => string;
23
+ //# sourceMappingURL=generate-unique-value.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-unique-value.d.ts","sourceRoot":"","sources":["../../src/helpers/generate-unique-value.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,mBAAmB,GAC9B,cAAc,MAAM,EACpB,YAAY,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,EACtC,eAAW,KACV,MAOF,CAAA"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Generates a unique string value based on a default value and a validation function.
3
+ * The function tries the default value first. If validation fails, it appends
4
+ * an incrementing suffix (optionally with a prefix) until the validation passes.
5
+ *
6
+ * @param defaultValue - The initial base string value to try.
7
+ * @param validation - A function that receives a candidate value and returns true if it is valid.
8
+ * @param prefix - Optional prefix to add before the incrementing number (default is '').
9
+ * @returns The first value that passes the validation.
10
+ *
11
+ * @example
12
+ * // Case 1: Simple unused value
13
+ * generateUniqueValue('example', v => v !== 'example') // → "example 2"
14
+ *
15
+ * // Case 2: Avoids taken values in a set
16
+ * const taken = new Set(['foo', 'foo 1', 'foo 2']);
17
+ * generateUniqueValue('foo', v => !taken.has(v)) // → "foo 3"
18
+ *
19
+ * // Case 3: Using a prefix
20
+ * generateUniqueValue('base', v => v === 'base__3', '__') // → "base__3"
21
+ */
22
+ export const generateUniqueValue = (defaultValue, validation, prefix = '') => {
23
+ let i = 1;
24
+ let value = defaultValue;
25
+ while (!validation(value)) {
26
+ value = `${defaultValue} ${prefix}${i++}`;
27
+ }
28
+ return value;
29
+ };
@@ -0,0 +1,40 @@
1
+ import type { OpenAPIV3_1 } from '@scalar/openapi-types';
2
+ /**
3
+ * Collects all example names used on parameters and requestBodies
4
+ * within all operations of a PathItemObject.
5
+ *
6
+ * This is useful for checking which example names are already present
7
+ * so you can avoid clashes (e.g., generating unique example names
8
+ * when merging path items).
9
+ *
10
+ * @param path - The OpenAPI PathItemObject to scan
11
+ * @returns Set of all unique example names found across parameters and requestBodies
12
+ *
13
+ * @example
14
+ * const operation: OpenAPIV3_1.OperationObject = {
15
+ * parameters: [
16
+ * {
17
+ * name: 'id',
18
+ * in: 'query',
19
+ * examples: {
20
+ * foo: { value: 'bar' },
21
+ * bar: { value: 'baz' }
22
+ * }
23
+ * }
24
+ * ],
25
+ * requestBody: {
26
+ * content: {
27
+ * 'application/json': {
28
+ * examples: {
29
+ * default: { value: 1 },
30
+ * extra: { value: 2 }
31
+ * }
32
+ * }
33
+ * }
34
+ * }
35
+ * }
36
+ * const path = { get: operation }
37
+ * // getOperationExamples(path) => Set { 'foo', 'bar', 'default', 'extra' }
38
+ */
39
+ export declare const getOperationExamples: (path: OpenAPIV3_1.PathItemObject) => Set<string>;
40
+ //# sourceMappingURL=get-operation-examples.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-operation-examples.d.ts","sourceRoot":"","sources":["../../src/helpers/get-operation-examples.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,oBAAoB,GAAI,MAAM,WAAW,CAAC,cAAc,gBA2CpE,CAAA"}
@@ -0,0 +1,76 @@
1
+ import { HTTP_METHODS } from '@scalar/helpers/http/http-methods';
2
+ /**
3
+ * Collects all example names used on parameters and requestBodies
4
+ * within all operations of a PathItemObject.
5
+ *
6
+ * This is useful for checking which example names are already present
7
+ * so you can avoid clashes (e.g., generating unique example names
8
+ * when merging path items).
9
+ *
10
+ * @param path - The OpenAPI PathItemObject to scan
11
+ * @returns Set of all unique example names found across parameters and requestBodies
12
+ *
13
+ * @example
14
+ * const operation: OpenAPIV3_1.OperationObject = {
15
+ * parameters: [
16
+ * {
17
+ * name: 'id',
18
+ * in: 'query',
19
+ * examples: {
20
+ * foo: { value: 'bar' },
21
+ * bar: { value: 'baz' }
22
+ * }
23
+ * }
24
+ * ],
25
+ * requestBody: {
26
+ * content: {
27
+ * 'application/json': {
28
+ * examples: {
29
+ * default: { value: 1 },
30
+ * extra: { value: 2 }
31
+ * }
32
+ * }
33
+ * }
34
+ * }
35
+ * }
36
+ * const path = { get: operation }
37
+ * // getOperationExamples(path) => Set { 'foo', 'bar', 'default', 'extra' }
38
+ */
39
+ export const getOperationExamples = (path) => {
40
+ const exampleNames = new Set();
41
+ for (const key of HTTP_METHODS) {
42
+ const operation = path[key];
43
+ if (operation === undefined || typeof operation !== 'object') {
44
+ continue;
45
+ }
46
+ const op = operation;
47
+ if ('parameters' in op) {
48
+ op.parameters?.forEach((parameter) => {
49
+ if (parameter.examples) {
50
+ for (const exampleName of Object.keys(parameter.examples)) {
51
+ exampleNames.add(exampleName);
52
+ }
53
+ }
54
+ });
55
+ }
56
+ if ('requestBody' in op) {
57
+ const requestBody = op.requestBody;
58
+ if (requestBody?.content) {
59
+ for (const mediaTypeObject of Object.values(requestBody.content)) {
60
+ const examples = mediaTypeObject &&
61
+ typeof mediaTypeObject === 'object' &&
62
+ 'examples' in mediaTypeObject &&
63
+ typeof mediaTypeObject.examples === 'object'
64
+ ? mediaTypeObject.examples
65
+ : undefined;
66
+ if (examples) {
67
+ for (const exampleName of Object.keys(examples)) {
68
+ exampleNames.add(exampleName);
69
+ }
70
+ }
71
+ }
72
+ }
73
+ }
74
+ }
75
+ return exampleNames;
76
+ };
@@ -0,0 +1,55 @@
1
+ import type { OpenAPIV3_1 } from '@scalar/openapi-types';
2
+ /**
3
+ * Merges two OpenAPI OperationObject instances.
4
+ * Assumes that all example names (keys in the 'examples' objects) are unique across both operations.
5
+ * This assumption allows us to shallowly merge example maps without risk of overwriting.
6
+ *
7
+ * @example
8
+ * const op1: OpenAPIV3_1.OperationObject = {
9
+ * tags: ['user'],
10
+ * parameters: [
11
+ * {
12
+ * name: 'id',
13
+ * in: 'path',
14
+ * required: true,
15
+ * schema: { type: 'string' },
16
+ * examples: { A: { value: 1 } }
17
+ * }
18
+ * ],
19
+ * requestBody: {
20
+ * content: {
21
+ * 'application/json': {
22
+ * examples: { example1: { value: { foo: 'bar' } } }
23
+ * }
24
+ * }
25
+ * }
26
+ * }
27
+ *
28
+ * const op2: OpenAPIV3_1.OperationObject = {
29
+ * tags: ['admin'],
30
+ * parameters: [
31
+ * {
32
+ * name: 'id',
33
+ * in: 'path',
34
+ * required: true,
35
+ * schema: { type: 'string' },
36
+ * examples: { B: { value: 2 } }
37
+ * }
38
+ * ],
39
+ * requestBody: {
40
+ * content: {
41
+ * 'application/json': {
42
+ * examples: { example2: { value: { hello: 'world' } } }
43
+ * }
44
+ * }
45
+ * }
46
+ * }
47
+ *
48
+ * const merged = mergeOperations(op1, op2)
49
+ * // merged.tags -> ['user', 'admin']
50
+ * // merged.parameters[0].examples -> { B: { value: 2 }, A: { value: 1 } }
51
+ * // merged.requestBody.content['application/json'].examples ->
52
+ * // { example2: {...}, example1: {...} }
53
+ */
54
+ export declare const mergeOperations: (operation1: OpenAPIV3_1.OperationObject, operation2: OpenAPIV3_1.OperationObject) => OpenAPIV3_1.OperationObject;
55
+ //# sourceMappingURL=merge-operation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge-operation.d.ts","sourceRoot":"","sources":["../../src/helpers/merge-operation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,eAAO,MAAM,eAAe,GAC1B,YAAY,WAAW,CAAC,eAAe,EACvC,YAAY,WAAW,CAAC,eAAe,KACtC,WAAW,CAAC,eAiFd,CAAA"}
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Merges two OpenAPI OperationObject instances.
3
+ * Assumes that all example names (keys in the 'examples' objects) are unique across both operations.
4
+ * This assumption allows us to shallowly merge example maps without risk of overwriting.
5
+ *
6
+ * @example
7
+ * const op1: OpenAPIV3_1.OperationObject = {
8
+ * tags: ['user'],
9
+ * parameters: [
10
+ * {
11
+ * name: 'id',
12
+ * in: 'path',
13
+ * required: true,
14
+ * schema: { type: 'string' },
15
+ * examples: { A: { value: 1 } }
16
+ * }
17
+ * ],
18
+ * requestBody: {
19
+ * content: {
20
+ * 'application/json': {
21
+ * examples: { example1: { value: { foo: 'bar' } } }
22
+ * }
23
+ * }
24
+ * }
25
+ * }
26
+ *
27
+ * const op2: OpenAPIV3_1.OperationObject = {
28
+ * tags: ['admin'],
29
+ * parameters: [
30
+ * {
31
+ * name: 'id',
32
+ * in: 'path',
33
+ * required: true,
34
+ * schema: { type: 'string' },
35
+ * examples: { B: { value: 2 } }
36
+ * }
37
+ * ],
38
+ * requestBody: {
39
+ * content: {
40
+ * 'application/json': {
41
+ * examples: { example2: { value: { hello: 'world' } } }
42
+ * }
43
+ * }
44
+ * }
45
+ * }
46
+ *
47
+ * const merged = mergeOperations(op1, op2)
48
+ * // merged.tags -> ['user', 'admin']
49
+ * // merged.parameters[0].examples -> { B: { value: 2 }, A: { value: 1 } }
50
+ * // merged.requestBody.content['application/json'].examples ->
51
+ * // { example2: {...}, example1: {...} }
52
+ */
53
+ export const mergeOperations = (operation1, operation2) => {
54
+ const operation = { ...operation2 };
55
+ // Merge tags (union, preserving uniqueness)
56
+ if (operation1.tags || operation.tags) {
57
+ operation.tags = Array.from(new Set([...(operation1.tags ?? []), ...(operation.tags ?? [])]));
58
+ }
59
+ const parameters = new Map();
60
+ const generateParameterId = (param) => `${param.name}/${param.in}`;
61
+ // Seed parameter list from operation2 (the base)
62
+ if (operation.parameters) {
63
+ for (const parameter of operation.parameters) {
64
+ const id = generateParameterId(parameter);
65
+ parameters.set(id, parameter);
66
+ }
67
+ }
68
+ // Merge parameters from operation1 into parameters of operation2.
69
+ // For each parameter, merge their 'examples' objects, assuming example keys are unique.
70
+ if (operation1.parameters) {
71
+ for (const parameter of operation1.parameters) {
72
+ const id = generateParameterId(parameter);
73
+ if (parameters.has(id)) {
74
+ const existingParameter = parameters.get(id);
75
+ if (existingParameter) {
76
+ // Example keys are expected to be unique, so shallow merge is safe.
77
+ existingParameter.examples = {
78
+ ...existingParameter.examples,
79
+ ...parameter.examples,
80
+ };
81
+ }
82
+ }
83
+ else {
84
+ parameters.set(id, parameter);
85
+ }
86
+ }
87
+ }
88
+ if (parameters.size > 0) {
89
+ operation.parameters = Array.from(parameters.values());
90
+ }
91
+ const contentMediaTypeMap = new Map();
92
+ // Seed requestBody content from operation2 (the base)
93
+ if (operation.requestBody?.content) {
94
+ for (const [contentType, mediaType] of Object.entries(operation.requestBody.content)) {
95
+ contentMediaTypeMap.set(contentType, mediaType);
96
+ }
97
+ }
98
+ // Merge requestBody content from operation1 into the base.
99
+ // When merging 'examples', we expect example names to be unique (no overwrite).
100
+ if (operation1.requestBody?.content) {
101
+ for (const [contentType, mediaType] of Object.entries(operation1.requestBody.content)) {
102
+ const mediaTypeObj = mediaType;
103
+ if (contentMediaTypeMap.has(contentType)) {
104
+ const existingMediaType = contentMediaTypeMap.get(contentType);
105
+ if (existingMediaType && (existingMediaType.examples || mediaTypeObj.examples)) {
106
+ // Assumption: example names (keys) are unique, so this merge is safe
107
+ existingMediaType.examples = {
108
+ ...existingMediaType.examples,
109
+ ...mediaTypeObj.examples,
110
+ };
111
+ }
112
+ }
113
+ else {
114
+ contentMediaTypeMap.set(contentType, mediaTypeObj);
115
+ }
116
+ }
117
+ }
118
+ if (contentMediaTypeMap.size > 0) {
119
+ operation.requestBody = {
120
+ ...operation.requestBody,
121
+ content: Object.fromEntries(contentMediaTypeMap),
122
+ };
123
+ }
124
+ return operation;
125
+ };
@@ -0,0 +1,5 @@
1
+ import type { OpenAPIV3_1 } from '@scalar/openapi-types';
2
+ export declare const DEFAULT_EXAMPLE_NAME = "Default example";
3
+ export declare const OPERATION_KEYS: readonly (keyof OpenAPIV3_1.PathItemObject)[];
4
+ export declare const mergePathItem: (paths: OpenAPIV3_1.PathsObject, normalizedPathKey: string, pathItem: OpenAPIV3_1.PathItemObject, mergeOperation?: boolean) => void;
5
+ //# sourceMappingURL=merge-path-item.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge-path-item.d.ts","sourceRoot":"","sources":["../../src/helpers/merge-path-item.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAOxD,eAAO,MAAM,oBAAoB,oBAAoB,CAAA;AAErD,eAAO,MAAM,cAAc,EAAE,SAAS,CAAC,MAAM,WAAW,CAAC,cAAc,CAAC,EASvE,CAAA;AAED,eAAO,MAAM,aAAa,GACxB,OAAO,WAAW,CAAC,WAAW,EAC9B,mBAAmB,MAAM,EACzB,UAAU,WAAW,CAAC,cAAc,EACpC,iBAAgB,OAAe,KAC9B,IA8BF,CAAA"}
@@ -0,0 +1,37 @@
1
+ import { generateUniqueValue } from '../helpers/generate-unique-value.js';
2
+ import { getOperationExamples } from '../helpers/get-operation-examples.js';
3
+ import { mergeOperations } from '../helpers/merge-operation.js';
4
+ import { renameOperationExamples } from '../helpers/rename-operation-example.js';
5
+ export const DEFAULT_EXAMPLE_NAME = 'Default example';
6
+ export const OPERATION_KEYS = [
7
+ 'get',
8
+ 'put',
9
+ 'post',
10
+ 'delete',
11
+ 'options',
12
+ 'head',
13
+ 'patch',
14
+ 'trace',
15
+ ];
16
+ export const mergePathItem = (paths, normalizedPathKey, pathItem, mergeOperation = false) => {
17
+ const targetPath = (paths[normalizedPathKey] ?? {});
18
+ for (const [key, value] of Object.entries(pathItem)) {
19
+ if (value === undefined) {
20
+ continue;
21
+ }
22
+ const isOperationKey = OPERATION_KEYS.includes(key);
23
+ if (isOperationKey && targetPath[key] && mergeOperation) {
24
+ // Get all example names from the target path
25
+ const exampleNames = getOperationExamples(targetPath);
26
+ // Generate a unique example name
27
+ const newExampleName = generateUniqueValue(DEFAULT_EXAMPLE_NAME, (value) => !exampleNames.has(value), '#');
28
+ // Rename operation examples from the new path item (we know it's gonna have only the default example)
29
+ renameOperationExamples(pathItem[key], DEFAULT_EXAMPLE_NAME, newExampleName);
30
+ // Merge the operations
31
+ targetPath[key] = mergeOperations(targetPath[key], pathItem[key]);
32
+ continue;
33
+ }
34
+ targetPath[key] = value;
35
+ }
36
+ paths[normalizedPathKey] = targetPath;
37
+ };
@@ -4,9 +4,9 @@ import type { Request } from '../types.js';
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
6
  */
7
- export declare function extractParameters(request: Request): OpenAPIV3_1.ParameterObject[];
7
+ export declare function extractParameters(request: Request, exampleName: string): OpenAPIV3_1.ParameterObject[];
8
8
  /**
9
9
  * Creates an OpenAPI parameter object from a Postman parameter.
10
10
  */
11
- export declare function createParameterObject(param: any, paramIn: 'query' | 'path' | 'header'): OpenAPIV3_1.ParameterObject;
11
+ export declare function createParameterObject(param: any, paramIn: 'query' | 'path' | 'header', exampleName: string): OpenAPIV3_1.ParameterObject;
12
12
  //# sourceMappingURL=parameters.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"parameters.d.ts","sourceRoot":"","sources":["../../src/helpers/parameters.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD,OAAO,KAAK,EAAU,OAAO,EAAE,MAAM,SAAS,CAAA;AAI9C;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,WAAW,CAAC,eAAe,EAAE,CA2DjF;AAoBD;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,GAAG,WAAW,CAAC,eAAe,CAmDnH"}
1
+ {"version":3,"file":"parameters.d.ts","sourceRoot":"","sources":["../../src/helpers/parameters.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD,OAAO,KAAK,EAAU,OAAO,EAAE,MAAM,SAAS,CAAA;AAI9C;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,GAAG,WAAW,CAAC,eAAe,EAAE,CA2DtG;AAoBD;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,GAAG,EACV,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,EACpC,WAAW,EAAE,MAAM,GAClB,WAAW,CAAC,eAAe,CAwD7B"}
@@ -3,7 +3,7 @@ import { inferSchemaType } from './schemas.js';
3
3
  * Extracts parameters from a Postman request and converts them to OpenAPI parameter objects.
4
4
  * Processes query, path, and header parameters from the request URL and headers.
5
5
  */
6
- export function extractParameters(request) {
6
+ export function extractParameters(request, exampleName) {
7
7
  const parameters = [];
8
8
  const parameterMap = new Map();
9
9
  if (typeof request === 'string' || !request.url) {
@@ -13,7 +13,7 @@ export function extractParameters(request) {
13
13
  // Process query parameters
14
14
  if (url.query) {
15
15
  url.query.forEach((param) => {
16
- const paramObj = createParameterObject(param, 'query');
16
+ const paramObj = createParameterObject(param, 'query', exampleName);
17
17
  if (paramObj.name) {
18
18
  parameterMap.set(paramObj.name, paramObj);
19
19
  }
@@ -22,7 +22,7 @@ export function extractParameters(request) {
22
22
  // Process path parameters
23
23
  if (url.variable) {
24
24
  url.variable.forEach((param) => {
25
- const paramObj = createParameterObject(param, 'path');
25
+ const paramObj = createParameterObject(param, 'path', exampleName);
26
26
  if (paramObj.name) {
27
27
  parameterMap.set(paramObj.name, paramObj);
28
28
  }
@@ -48,7 +48,7 @@ export function extractParameters(request) {
48
48
  // Process header parameters
49
49
  if (request.header && Array.isArray(request.header)) {
50
50
  request.header.forEach((header) => {
51
- const paramObj = createParameterObject(header, 'header');
51
+ const paramObj = createParameterObject(header, 'header', exampleName);
52
52
  if (paramObj.name) {
53
53
  parameterMap.set(paramObj.name, paramObj);
54
54
  }
@@ -74,11 +74,17 @@ function extractPathVariablesFromPathArray(pathArray) {
74
74
  /**
75
75
  * Creates an OpenAPI parameter object from a Postman parameter.
76
76
  */
77
- export function createParameterObject(param, paramIn) {
77
+ export function createParameterObject(param, paramIn, exampleName) {
78
78
  const parameter = {
79
79
  name: param.key || '',
80
80
  in: paramIn,
81
81
  description: param.description,
82
+ examples: {
83
+ [exampleName]: {
84
+ value: param.value,
85
+ 'x-disabled': !!param.disabled,
86
+ },
87
+ },
82
88
  };
83
89
  // Path parameters are always required in OpenAPI
84
90
  if (paramIn === 'path') {
@@ -96,7 +102,6 @@ export function createParameterObject(param, paramIn) {
96
102
  }
97
103
  }
98
104
  if (param.value !== undefined) {
99
- parameter.example = param.value;
100
105
  // For path parameters, prefer string type unless value is explicitly a number type
101
106
  // This prevents converting string IDs like "testId" to integers
102
107
  if (paramIn === 'path') {
@@ -15,7 +15,7 @@ export type ServerUsage = {
15
15
  * Handles nested item groups, extracts request details, and generates corresponding
16
16
  * OpenAPI path items and operations.
17
17
  */
18
- export declare function processItem(item: Item | ItemGroup, parentTags?: string[], parentPath?: string): {
18
+ export declare function processItem(item: Item | ItemGroup, exampleName?: string, parentTags?: string[], parentPath?: string): {
19
19
  paths: OpenAPIV3_1.PathsObject;
20
20
  components: OpenAPIV3_1.ComponentsObject;
21
21
  serverUsage: ServerUsage[];
@@ -1 +1 @@
1
- {"version":3,"file":"path-items.d.ts","sourceRoot":"","sources":["../../src/helpers/path-items.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD,OAAO,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAW9C,KAAK,WAAW,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,CAAA;AAE7F;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,WAAW,CAAA;CACpB,CAAA;AAoBD;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,IAAI,GAAG,SAAS,EACtB,UAAU,GAAE,MAAM,EAAO,EACzB,UAAU,GAAE,MAAW,GACtB;IACD,KAAK,EAAE,WAAW,CAAC,WAAW,CAAA;IAC9B,UAAU,EAAE,WAAW,CAAC,gBAAgB,CAAA;IACxC,WAAW,EAAE,WAAW,EAAE,CAAA;CAC3B,CA2KA"}
1
+ {"version":3,"file":"path-items.d.ts","sourceRoot":"","sources":["../../src/helpers/path-items.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD,OAAO,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAW9C,KAAK,WAAW,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,CAAA;AAE7F;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,WAAW,CAAA;CACpB,CAAA;AAoBD;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,IAAI,GAAG,SAAS,EACtB,WAAW,GAAE,MAAkB,EAC/B,UAAU,GAAE,MAAM,EAAO,EACzB,UAAU,GAAE,MAAW,GACtB;IACD,KAAK,EAAE,WAAW,CAAC,WAAW,CAAA;IAC9B,UAAU,EAAE,WAAW,CAAC,gBAAgB,CAAA;IACxC,WAAW,EAAE,WAAW,EAAE,CAAA;CAC3B,CA2KA"}
@@ -27,14 +27,14 @@ function ensureRequestBodyContent(requestBody) {
27
27
  * Handles nested item groups, extracts request details, and generates corresponding
28
28
  * OpenAPI path items and operations.
29
29
  */
30
- export function processItem(item, parentTags = [], parentPath = '') {
30
+ export function processItem(item, exampleName = 'default', parentTags = [], parentPath = '') {
31
31
  const paths = {};
32
32
  const components = {};
33
33
  const serverUsage = [];
34
34
  if ('item' in item && Array.isArray(item.item)) {
35
35
  const newParentTags = item.name ? [...parentTags, item.name] : parentTags;
36
36
  item.item.forEach((childItem) => {
37
- const childResult = processItem(childItem, newParentTags, `${parentPath}/${item.name || ''}`);
37
+ const childResult = processItem(childItem, exampleName, newParentTags, `${parentPath}/${item.name || ''}`);
38
38
  // Merge child paths and components
39
39
  for (const [pathKey, pathItem] of Object.entries(childResult.paths)) {
40
40
  if (!paths[pathKey]) {
@@ -109,7 +109,7 @@ export function processItem(item, parentTags = [], parentPath = '') {
109
109
  }
110
110
  // Extract parameters from the request (query, path, header)
111
111
  // This should always happen, regardless of whether a description exists
112
- const extractedParameters = extractParameters(request);
112
+ const extractedParameters = extractParameters(request, exampleName);
113
113
  // Merge parameters, giving priority to those from the Markdown table if description exists
114
114
  const mergedParameters = new Map();
115
115
  // Add extracted parameters, filtering out path parameters not in the path
@@ -156,7 +156,7 @@ export function processItem(item, parentTags = [], parentPath = '') {
156
156
  }
157
157
  // Allow request bodies for all methods (including GET) if body is present
158
158
  if (typeof request !== 'string' && request.body) {
159
- const requestBody = extractRequestBody(request.body);
159
+ const requestBody = extractRequestBody(request.body, exampleName);
160
160
  ensureRequestBodyContent(requestBody);
161
161
  // Only add requestBody if it has content
162
162
  if (requestBody.content && Object.keys(requestBody.content).length > 0) {
@@ -175,8 +175,9 @@ const OPENAPI_PARAM_SCHEMA_TYPES = ['string', 'number', 'integer', 'boolean', 'o
175
175
  function toOpenApiParamSchemaType(s) {
176
176
  const value = s ?? 'string';
177
177
  for (const t of OPENAPI_PARAM_SCHEMA_TYPES) {
178
- if (t === value)
178
+ if (t === value) {
179
179
  return t;
180
+ }
180
181
  }
181
182
  return 'string';
182
183
  }
@@ -0,0 +1,40 @@
1
+ import type { OpenAPIV3_1 } from '@scalar/openapi-types';
2
+ /**
3
+ * Renames an example in all parameters and requestBody content of an operation object.
4
+ *
5
+ * This will copy the example (by name) and remove the old entry both for parameter examples
6
+ * and within every media type of the requestBody, if such examples exist.
7
+ *
8
+ * @param operation - The OpenAPI operation object to mutate
9
+ * @param exampleName - The existing example name to rename
10
+ * @param newExampleName - The new name to give that example
11
+ *
12
+ * @example
13
+ * // Given:
14
+ * const operation = {
15
+ * parameters: [
16
+ * {
17
+ * name: 'foo',
18
+ * in: 'query',
19
+ * examples: {
20
+ * oldName: { value: 'fooValue' }
21
+ * }
22
+ * }
23
+ * ],
24
+ * requestBody: {
25
+ * content: {
26
+ * 'application/json': {
27
+ * examples: {
28
+ * oldName: { value: 123 }
29
+ * }
30
+ * }
31
+ * }
32
+ * }
33
+ * }
34
+ * renameOperationExamples(operation, 'oldName', 'newName')
35
+ * // After:
36
+ * // operation.parameters[0].examples: { newName: { value: 'fooValue' } }
37
+ * // operation.requestBody.content['application/json'].examples: { newName: { value: 123 } }
38
+ */
39
+ export declare const renameOperationExamples: (operation: OpenAPIV3_1.OperationObject, exampleName: string, newExampleName: string) => void;
40
+ //# sourceMappingURL=rename-operation-example.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rename-operation-example.d.ts","sourceRoot":"","sources":["../../src/helpers/rename-operation-example.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,uBAAuB,GAClC,WAAW,WAAW,CAAC,eAAe,EACtC,aAAa,MAAM,EACnB,gBAAgB,MAAM,KACrB,IA0BF,CAAA"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Renames an example in all parameters and requestBody content of an operation object.
3
+ *
4
+ * This will copy the example (by name) and remove the old entry both for parameter examples
5
+ * and within every media type of the requestBody, if such examples exist.
6
+ *
7
+ * @param operation - The OpenAPI operation object to mutate
8
+ * @param exampleName - The existing example name to rename
9
+ * @param newExampleName - The new name to give that example
10
+ *
11
+ * @example
12
+ * // Given:
13
+ * const operation = {
14
+ * parameters: [
15
+ * {
16
+ * name: 'foo',
17
+ * in: 'query',
18
+ * examples: {
19
+ * oldName: { value: 'fooValue' }
20
+ * }
21
+ * }
22
+ * ],
23
+ * requestBody: {
24
+ * content: {
25
+ * 'application/json': {
26
+ * examples: {
27
+ * oldName: { value: 123 }
28
+ * }
29
+ * }
30
+ * }
31
+ * }
32
+ * }
33
+ * renameOperationExamples(operation, 'oldName', 'newName')
34
+ * // After:
35
+ * // operation.parameters[0].examples: { newName: { value: 'fooValue' } }
36
+ * // operation.requestBody.content['application/json'].examples: { newName: { value: 123 } }
37
+ */
38
+ export const renameOperationExamples = (operation, exampleName, newExampleName) => {
39
+ // Rename in parameter examples (if present)
40
+ if ('parameters' in operation) {
41
+ operation.parameters?.forEach((parameter) => {
42
+ if (parameter.examples?.[exampleName] && exampleName !== newExampleName) {
43
+ parameter.examples[newExampleName] = parameter.examples[exampleName];
44
+ delete parameter.examples[exampleName];
45
+ }
46
+ });
47
+ }
48
+ // Rename in requestBody content examples (if present)
49
+ if ('requestBody' in operation) {
50
+ Object.values(operation.requestBody?.content ?? {}).forEach((mediaTypeObject) => {
51
+ if (mediaTypeObject.examples &&
52
+ typeof mediaTypeObject.examples === 'object') {
53
+ const mediaCasted = mediaTypeObject;
54
+ if (mediaCasted.examples?.[exampleName] && exampleName !== newExampleName) {
55
+ mediaCasted.examples[newExampleName] = mediaCasted.examples[exampleName];
56
+ delete mediaCasted.examples[exampleName];
57
+ }
58
+ }
59
+ });
60
+ }
61
+ };
@@ -4,5 +4,5 @@ import type { RequestBody } from '../types.js';
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
6
  */
7
- export declare function extractRequestBody(body: RequestBody): OpenAPIV3_1.RequestBodyObject;
7
+ export declare function extractRequestBody(body: RequestBody, exampleName: string): OpenAPIV3_1.RequestBodyObject;
8
8
  //# sourceMappingURL=request-body.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"request-body.d.ts","sourceRoot":"","sources":["../../src/helpers/request-body.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD,OAAO,KAAK,EAAiB,WAAW,EAAuB,MAAM,SAAS,CAAA;AAK9E;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,WAAW,GAAG,WAAW,CAAC,iBAAiB,CAqBnF"}
1
+ {"version":3,"file":"request-body.d.ts","sourceRoot":"","sources":["../../src/helpers/request-body.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD,OAAO,KAAK,EAAiB,WAAW,EAAuB,MAAM,SAAS,CAAA;AAK9E;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,GAAG,WAAW,CAAC,iBAAiB,CAqBxG"}
@@ -4,25 +4,25 @@ import { createParameterObject } from './parameters.js';
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
6
  */
7
- export function extractRequestBody(body) {
7
+ export function extractRequestBody(body, exampleName) {
8
8
  const requestBody = {
9
9
  content: {},
10
10
  };
11
11
  if (body.mode === 'raw') {
12
- handleRawBody(body, requestBody);
12
+ handleRawBody(body, requestBody, exampleName);
13
13
  return requestBody;
14
14
  }
15
15
  if (body.mode === 'formdata' && body.formdata) {
16
- handleFormDataBody(body.formdata, requestBody);
16
+ handleFormDataBody(body.formdata, requestBody, exampleName);
17
17
  return requestBody;
18
18
  }
19
19
  if (body.mode === 'urlencoded' && body.urlencoded) {
20
- handleUrlEncodedBody(body.urlencoded, requestBody);
20
+ handleUrlEncodedBody(body.urlencoded, requestBody, exampleName);
21
21
  return requestBody;
22
22
  }
23
23
  return requestBody;
24
24
  }
25
- function handleRawBody(body, requestBody) {
25
+ function handleRawBody(body, requestBody, exampleName) {
26
26
  const rawBody = body.raw || '';
27
27
  const isJsonLanguage = body.options?.raw?.language === 'json';
28
28
  // Check if body contains Postman variables (like {{bodyData}})
@@ -44,7 +44,11 @@ function handleRawBody(body, requestBody) {
44
44
  'application/json': {
45
45
  schema: {
46
46
  type: 'object',
47
- example: jsonBody,
47
+ },
48
+ examples: {
49
+ [exampleName]: {
50
+ value: jsonBody,
51
+ },
48
52
  },
49
53
  },
50
54
  };
@@ -73,14 +77,19 @@ function handleRawBody(body, requestBody) {
73
77
  },
74
78
  };
75
79
  }
76
- function handleFormDataBody(formdata, requestBody) {
80
+ function handleFormDataBody(formdata, requestBody, exampleName) {
77
81
  requestBody.content = {
78
82
  'multipart/form-data': {
79
83
  schema: processFormDataSchema(formdata),
84
+ examples: {
85
+ [exampleName]: {
86
+ value: formdata,
87
+ },
88
+ },
80
89
  },
81
90
  };
82
91
  }
83
- function handleUrlEncodedBody(urlencoded, requestBody) {
92
+ function handleUrlEncodedBody(urlencoded, requestBody, exampleName) {
84
93
  const schema = {
85
94
  type: 'object',
86
95
  properties: {},
@@ -88,7 +97,7 @@ function handleUrlEncodedBody(urlencoded, requestBody) {
88
97
  };
89
98
  urlencoded.forEach((item) => {
90
99
  if (schema.properties) {
91
- const paramObject = createParameterObject(item, 'query');
100
+ const paramObject = createParameterObject(item, 'query', exampleName);
92
101
  const property = {
93
102
  type: 'string',
94
103
  examples: [item.value],
@@ -107,6 +116,11 @@ function handleUrlEncodedBody(urlencoded, requestBody) {
107
116
  requestBody.content = {
108
117
  'application/x-www-form-urlencoded': {
109
118
  schema,
119
+ examples: {
120
+ [exampleName]: {
121
+ value: urlencoded,
122
+ },
123
+ },
110
124
  },
111
125
  };
112
126
  }
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
- export { convert } from './convert.js';
1
+ export { type ConvertOptions, type PostmanRequestIndexPath, convert } from './convert.js';
2
+ export { extractPathFromUrl, normalizePath } from './helpers/urls.js';
2
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,uBAAuB,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACtF,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA"}
package/dist/index.js CHANGED
@@ -1 +1,2 @@
1
1
  export { convert } from './convert.js';
2
+ export { extractPathFromUrl, normalizePath } from './helpers/urls.js';
package/package.json CHANGED
@@ -19,7 +19,7 @@
19
19
  "export",
20
20
  "scalar"
21
21
  ],
22
- "version": "0.5.3",
22
+ "version": "0.6.0",
23
23
  "engines": {
24
24
  "node": ">=22"
25
25
  },
@@ -38,8 +38,8 @@
38
38
  "CHANGELOG.md"
39
39
  ],
40
40
  "dependencies": {
41
- "@scalar/openapi-types": "0.6.1",
42
- "@scalar/helpers": "0.4.2"
41
+ "@scalar/helpers": "0.4.2",
42
+ "@scalar/openapi-types": "0.6.1"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/node": "^24.1.0",
@@ -47,6 +47,7 @@
47
47
  },
48
48
  "scripts": {
49
49
  "build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
50
+ "generate:textures": "tsx ./scripts/generate-textures.ts",
50
51
  "test": "vitest",
51
52
  "types:check": "tsc --noEmit"
52
53
  }