Skip to content

Commit a4d0aa4

Browse files
authored
Merge pull request #864 from postmanlabs/release/v5.1.0
Release version v5.1.0
2 parents 9571cfd + 7211cbd commit a4d0aa4

File tree

9 files changed

+475
-148
lines changed

9 files changed

+475
-148
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## [Unreleased]
44

5+
## [v5.1.0] - 2025-09-01
6+
57
## [v5.0.2] - 2025-08-25
68

79
## [v5.0.1] - 2025-07-22
@@ -659,7 +661,9 @@ Newer releases follow the [Keep a Changelog](https://keepachangelog.com/en/1.0.0
659661

660662
- Base release
661663

662-
[Unreleased]: https://github.com/postmanlabs/openapi-to-postman/compare/v5.0.2...HEAD
664+
[Unreleased]: https://github.com/postmanlabs/openapi-to-postman/compare/v5.1.0...HEAD
665+
666+
[v5.1.0]: https://github.com/postmanlabs/openapi-to-postman/compare/v5.0.2...v5.1.0
663667

664668
[v5.0.2]: https://github.com/postmanlabs/openapi-to-postman/compare/v5.0.1...v5.0.2
665669

OPTIONS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,5 @@ includeWebhooks|boolean|-|false|Select whether to include Webhooks in the genera
2727
includeReferenceMap|boolean|-|false|Whether or not to include reference map or not as part of output|BUNDLE|v2, v1
2828
includeDeprecated|boolean|-|true|Select whether to include deprecated operations, parameters, and properties in generated collection or not|CONVERSION, VALIDATION|v2, v1
2929
alwaysInheritAuthentication|boolean|-|false|Whether authentication details should be included on every request, or always inherited from the collection.|CONVERSION|v2, v1
30+
nestedFolderHierarchy|boolean|-|false|Enable this option to create subfolders in the collection based on the order of tags.|CONVERSION|v2
3031
preferredRequestBodyType|enum|x-www-form-urlencoded, form-data, raw, first-listed|first-listed|When there are multiple content-types defined in the request body of OpenAPI, the conversion selects the preferred option content-type as request body.If "first-listed" is set, the first content-type defined in the OpenAPI spec will be selected.|CONVERSION|v2

lib/options.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,17 @@ module.exports = {
180180
supportedIn: [VERSION20, VERSION30, VERSION31],
181181
supportedModuleVersion: [MODULE_VERSION.V2, MODULE_VERSION.V1]
182182
},
183+
{
184+
name: 'Nested folder organization using tags',
185+
id: 'nestedFolderHierarchy',
186+
type: 'boolean',
187+
default: false,
188+
description: 'Enable this option to create subfolders in the collection based on the order of tags.',
189+
external: true,
190+
usage: ['CONVERSION'],
191+
supportedIn: [VERSION20, VERSION30, VERSION31],
192+
supportedModuleVersion: [MODULE_VERSION.V2]
193+
},
183194
{
184195
name: 'Enable Schema Faking',
185196
id: 'schemaFaker',

libV2/helpers/collection/generateSkeletionTreeFromOpenAPI.js

Lines changed: 126 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -176,148 +176,6 @@ let _ = require('lodash'),
176176
return tree;
177177
},
178178

179-
// _generateTreeFromPaths = function (openapi, { includeDeprecated }) {
180-
// /**
181-
// * We will create a unidirectional graph
182-
// */
183-
// let tree = new Graph();
184-
185-
// tree.setNode('root:collection', {
186-
// type: 'collection',
187-
// data: {},
188-
// meta: {}
189-
// });
190-
191-
// _.forEach(openapi.paths, function (methods, path) {
192-
// let pathSplit = path === '/' ? [path] : _.compact(path.split('/'));
193-
194-
// // if after path split we just have one entry
195-
// // that means no folders need to be generated.
196-
// // check for all the methods inside it and expand.
197-
// if (pathSplit.length === 1) {
198-
// /**
199-
// * Always first try to find the node if it already exists.
200-
// * if yes, bail out nothing is needed to be done.
201-
// *
202-
// * if the path length is 1, then also generate
203-
// * the folder otherwise /pet and /pet/:id will never be in same folder.
204-
// */
205-
// // if (!tree.hasNode(`path:${pathSplit[0]}`)) {
206-
// // tree.setNode(`path:${pathSplit[0]}`, {
207-
// // type: 'folder',
208-
// // meta: {
209-
// // name: pathSplit[0],
210-
// // path: pathSplit[0],
211-
// // pathIdentifier: pathIdentifier
212-
// // },
213-
// // data: {}
214-
// // });
215-
216-
// // tree.setEdge('root:collection', `path:${pathSplit[0]}`);
217-
// // }
218-
219-
220-
// _.forEach(methods, function (data, method) {
221-
// if (!ALLOWED_HTTP_METHODS[method]) {
222-
// return;
223-
// }
224-
225-
// /**
226-
// * include deprecated handling.
227-
// * If true, add in the postman collection. If false ignore the request.
228-
// */
229-
// if (!includeDeprecated && data.deprecated) {
230-
// return;
231-
// }
232-
233-
// tree.setNode(`path:${pathSplit[0]}:${method}`, {
234-
// type: 'request',
235-
// meta: {
236-
// path: path,
237-
// method: method,
238-
// pathIdentifier: pathSplit[0]
239-
// },
240-
// data: {}
241-
// });
242-
243-
// tree.setEdge(`path:${pathSplit[0]}`, `path:${pathSplit[0]}:${method}`);
244-
// });
245-
// }
246-
247-
// else {
248-
// _.forEach(pathSplit, function (p, index) {
249-
// let previousPathIdentified = pathSplit.slice(0, index).join('/'),
250-
// pathIdentifier = pathSplit.slice(0, index + 1).join('/');
251-
252-
// /**
253-
// * Always first try to find the node if it already exists.
254-
// * if yes, bail out nothing is needed to be done.
255-
// */
256-
// if (tree.hasNode(`path:${pathIdentifier}`)) {
257-
// return;
258-
// }
259-
260-
// else {
261-
// tree.setNode(`path:${pathIdentifier}`, {
262-
// type: 'folder',
263-
// meta: {
264-
// name: p,
265-
// path: p,
266-
// pathIdentifier: pathIdentifier
267-
// },
268-
// data: {}
269-
// });
270-
271-
// /**
272-
// * If index is 0, this means that we are on the first level.
273-
// * Hence it is folder/request to be added on the first level
274-
// *
275-
// * If after the split we have more than one paths, then we need
276-
// * to add to the previous node.
277-
// */
278-
// tree.setEdge(index === 0 ? 'root:collection' : `path:${previousPathIdentified}`, `path:${pathIdentifier}`);
279-
// }
280-
// });
281-
282-
// /**
283-
// * Now for all the methods present in the path, add the request nodes.
284-
// */
285-
286-
// _.forEach(methods, function (data, method) {
287-
// if (!ALLOWED_HTTP_METHODS[method]) {
288-
// return;
289-
// }
290-
291-
// /**
292-
// * include deprecated handling.
293-
// * If true, add in the postman collection. If false ignore the request.
294-
// */
295-
// if (!includeDeprecated && data.deprecated) {
296-
// return;
297-
// }
298-
299-
// // join till the last path i.e. the folder.
300-
// let previousPathIdentified = pathSplit.slice(0, (pathSplit.length)).join('/'),
301-
// pathIdentifier = `${pathSplit.join('/')}:${method}`;
302-
303-
// tree.setNode(`path:${pathIdentifier}`, {
304-
// type: 'request',
305-
// data: {},
306-
// meta: {
307-
// path: path,
308-
// method: method,
309-
// pathIdentifier: pathIdentifier
310-
// }
311-
// });
312-
313-
// tree.setEdge(`path:${previousPathIdentified}`, `path:${pathIdentifier}`);
314-
// });
315-
// }
316-
// });
317-
318-
// return tree;
319-
// },
320-
321179
_generateTreeFromTags = function (openapi, { includeDeprecated }) {
322180
let tree = new Graph(),
323181

@@ -424,6 +282,124 @@ let _ = require('lodash'),
424282
return tree;
425283
},
426284

285+
/**
286+
* Generates tree structure with nested folders based on tag order
287+
* @param {Object} openapi - OpenAPI specification
288+
* @param {Object} options - Generation options
289+
* @param {boolean} options.includeDeprecated - Whether to include deprecated operations
290+
* @returns {Object} - Graph tree with nested folder structure
291+
*/
292+
_generateTreeFromNestedTags = function (openapi, { includeDeprecated }) {
293+
let tree = new Graph(),
294+
295+
tagDescMap = _.reduce(openapi.tags, function (acc, data) {
296+
acc[data.name] = data.description;
297+
298+
return acc;
299+
}, {});
300+
301+
tree.setNode('root:collection', {
302+
type: 'collection',
303+
data: {},
304+
meta: {}
305+
});
306+
307+
/**
308+
* Helper function to create nested folder structure for tags
309+
* @param {Array} tags - Array of tags to create nested folders for
310+
* @returns {String} - Node ID of the deepest folder created
311+
*/
312+
const createNestedFolders = function (tags) {
313+
if (!tags || tags.length === 0) {
314+
return 'root:collection';
315+
}
316+
317+
let parentNodeId = 'root:collection';
318+
319+
// Create nested folder structure based on tag order
320+
for (let i = 0; i < tags.length; i++) {
321+
const tag = tags[i],
322+
folderPath = tags.slice(0, i + 1).join(':'),
323+
currentNodeId = `path:${folderPath}`;
324+
325+
// Create folder node if it doesn't exist
326+
if (!tree.hasNode(currentNodeId)) {
327+
tree.setNode(currentNodeId, {
328+
type: 'folder',
329+
meta: {
330+
path: '',
331+
name: tag,
332+
description: tagDescMap[tag] || ''
333+
},
334+
data: {}
335+
});
336+
337+
// Connect to parent (either root collection or previous folder)
338+
tree.setEdge(parentNodeId, currentNodeId);
339+
}
340+
341+
parentNodeId = currentNodeId;
342+
}
343+
344+
return parentNodeId;
345+
};
346+
347+
_.forEach(openapi.paths, function (methods, path) {
348+
_.forEach(methods, function (data, method) {
349+
if (!ALLOWED_HTTP_METHODS[method]) {
350+
return;
351+
}
352+
353+
/**
354+
* include deprecated handling.
355+
* If true, add in the postman collection. If false ignore the request.
356+
*/
357+
if (!includeDeprecated && data.deprecated) {
358+
return;
359+
}
360+
361+
/**
362+
* Create nested folder structure based on tags order
363+
* and place the request in the deepest folder
364+
*/
365+
if (data.tags && data.tags.length > 0) {
366+
// Create nested folder structure and get the deepest folder node
367+
const deepestFolderNodeId = createNestedFolders(data.tags),
368+
// Create a unique request node ID (one per operation)
369+
requestNodeId = `request:${path}:${method}`;
370+
371+
tree.setNode(requestNodeId, {
372+
type: 'request',
373+
data: {},
374+
meta: {
375+
tags: data.tags,
376+
path: path,
377+
method: method
378+
}
379+
});
380+
381+
// Connect request to the deepest folder
382+
tree.setEdge(deepestFolderNodeId, requestNodeId);
383+
}
384+
else {
385+
// No tags - place request directly under root collection
386+
tree.setNode(`path:${path}:${method}`, {
387+
type: 'request',
388+
data: {},
389+
meta: {
390+
path: path,
391+
method: method
392+
}
393+
});
394+
395+
tree.setEdge('root:collection', `path:${path}:${method}`);
396+
}
397+
});
398+
});
399+
400+
return tree;
401+
},
402+
427403
_generateWebhookEndpoints = function (openapi, tree, { includeDeprecated }) {
428404
if (!_.isEmpty(openapi.webhooks)) {
429405
tree.setNode(`${PATH_WEBHOOK}:folder`, {
@@ -470,12 +446,18 @@ let _ = require('lodash'),
470446
*
471447
* @returns {Object} - tree format
472448
*/
473-
module.exports = function (openapi, { folderStrategy, includeWebhooks, includeDeprecated }) {
449+
module.exports = function (openapi, { folderStrategy, includeWebhooks, includeDeprecated, nestedFolderHierarchy }) {
474450
let skeletonTree;
475451

476452
switch (folderStrategy) {
477453
case 'tags':
478-
skeletonTree = _generateTreeFromTags(openapi, { includeDeprecated });
454+
if (nestedFolderHierarchy) {
455+
skeletonTree = _generateTreeFromNestedTags(openapi, { includeDeprecated });
456+
}
457+
else {
458+
skeletonTree = _generateTreeFromTags(openapi, { includeDeprecated });
459+
}
460+
479461
break;
480462

481463
case 'paths':

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "openapi-to-postmanv2",
3-
"version": "5.0.2",
3+
"version": "5.1.0",
44
"description": "Convert a given OpenAPI specification to Postman Collection v2.0",
55
"homepage": "https://github.com/postmanlabs/openapi-to-postman",
66
"bugs": "https://github.com/postmanlabs/openapi-to-postman/issues",

0 commit comments

Comments
 (0)