diff --git a/bin/openapi2postmanv2.js b/bin/openapi2postmanv2.js index 11025d98a..822fc5be3 100755 --- a/bin/openapi2postmanv2.js +++ b/bin/openapi2postmanv2.js @@ -7,13 +7,16 @@ var program = require('commander'), outputFile, prettyPrintFlag, testFlag, + sourceMapFile, swaggerInput, - swaggerData; + swaggerData, + sourceMapData; program .version(require('../package.json').version, '-v, --version') .option('-s, --spec ', 'Convert given OPENAPI 3.0.0 spec to Postman Collection v2.0') .option('-o, --output ', 'Write the collection to an output file') + .option('-m, --sourceMap ', 'Source map to use for operation to request mapping') .option('-t, --test', 'Test the OPENAPI converter') .option('-p, --pretty', 'Pretty print the JSON file'); @@ -41,6 +44,7 @@ inputFile = program.spec; outputFile = program.output || false; testFlag = program.test || false; prettyPrintFlag = program.pretty || false; +sourceMapFile = program.sourceMap; swaggerInput; swaggerData; @@ -52,7 +56,7 @@ swaggerData; * @param {Object} collection - POSTMAN collection object * @returns {void} */ -function writetoFile(prettyPrintFlag, file, collection) { +function writetoFile(prettyPrintFlag, file, collection, sourceMapFile, sourceMap) { if (prettyPrintFlag) { fs.writeFile(file, JSON.stringify(collection, null, 4), (err) => { if (err) { console.log('Could not write to file', err); } @@ -65,6 +69,13 @@ function writetoFile(prettyPrintFlag, file, collection) { console.log('Conversion successful', 'Collection written to file'); }); } + + if (sourceMapFile) { + fs.writeFile(sourceMapFile, JSON.stringify(sourceMap), (err) => { + if (err) { console.log('Could not write to source map file', err); } + console.log('Source Map written to file'); + }); + } } /** @@ -72,10 +83,11 @@ function writetoFile(prettyPrintFlag, file, collection) { * @param {String} swaggerData - swagger data used for conversion input * @returns {void} */ -function convert(swaggerData) { +function convert(swaggerData, sourceMapData) { Converter.convert({ type: 'string', - data: swaggerData + data: swaggerData, + sourceMap: sourceMapData ? JSON.parse(sourceMapData) : '' }, {}, (err, status) => { if (err) { return console.error(err); @@ -87,7 +99,7 @@ function convert(swaggerData) { else if (outputFile) { let file = path.resolve(outputFile); console.log('Writing to file: ', prettyPrintFlag, file, status); // eslint-disable-line no-console - writetoFile(prettyPrintFlag, file, status.output[0].data); + writetoFile(prettyPrintFlag, file, status.output[0].data, sourceMapFile, status.sourceMap); } else { console.log(status.output[0].data); // eslint-disable-line no-console @@ -107,7 +119,14 @@ else if (inputFile) { // this will fix https://github.com/postmanlabs/openapi-to-postman/issues/4 // inputFile should be read from the cwd, not the path of the executable swaggerData = fs.readFileSync(inputFile, 'utf8'); - convert(swaggerData); + if (sourceMapFile) { + try { + sourceMapData = fs.readFileSync(sourceMapFile, 'utf8'); + } catch (_e) { + sourceMapData = '{}'; + } + } + convert(swaggerData, sourceMapData); } else { program.emit('--help'); diff --git a/lib/schemaUtils.js b/lib/schemaUtils.js index 7d88ec693..42a804f2e 100644 --- a/lib/schemaUtils.js +++ b/lib/schemaUtils.js @@ -99,6 +99,8 @@ schemaFaker.option({ ignoreMissingRefs: true }); +sdk.Request.prototype._postman_propertyRequiresId = true; + /** * * @param {*} input - input string that needs to be hashed @@ -535,9 +537,10 @@ module.exports = { * resolve references while generating params. * @param {object} options - a standard list of options that's globally passed around. Check options.js for more. * @param {object} schemaCache - object storing schemaFaker and schmeResolution caches + * @param {object} sourceMap - object storing a map between OpenAPI elements and Postman collection elements. * @returns {void} - generatedStore is modified in-place */ - addCollectionItemsUsingPaths: function (spec, generatedStore, components, options, schemaCache) { + addCollectionItemsUsingPaths: function (spec, generatedStore, components, options, schemaCache, sourceMap) { var folderTree, folderObj, child, @@ -579,7 +582,7 @@ module.exports = { if (folderTree.root.children.hasOwnProperty(child) && folderTree.root.children[child].requestCount > 0) { generatedStore.collection.items.add( this.convertChildToItemGroup(spec, folderTree.root.children[child], - components, options, schemaCache, variableStore) + components, options, schemaCache, variableStore, sourceMap) ); } } @@ -604,9 +607,10 @@ module.exports = { * resolve references while generating params. * @param {object} options - a standard list of options that's globally passed around. Check options.js for more. * @param {object} schemaCache - object storing schemaFaker and schmeResolution caches + * @param {object} sourceMap - object storing a map between OpenAPI elements and Postman collection elements. * @returns {object} returns an object containing objects of tags and their requests */ - addCollectionItemsUsingTags: function(spec, generatedStore, components, options, schemaCache) { + addCollectionItemsUsingTags: function(spec, generatedStore, components, options, schemaCache, sourceMap) { var globalTags = spec.tags || [], paths = spec.paths || {}, pathMethods, @@ -683,7 +687,7 @@ module.exports = { }; generatedStore.collection.items.add(this.convertRequestToItem( - spec, tempRequest, components, options, schemaCache, variableStore)); + spec, tempRequest, components, options, schemaCache, variableStore, sourceMap)); } else { _.forEach(localTags, (localTag) => { @@ -711,13 +715,17 @@ module.exports = { // Add all folders created from tags and corresponding operations // Iterate from bottom to top order to maintain tag order in spec _.forEachRight(tagFolders, (tagFolder, tagName) => { + const sourceMapKey = `#/folders/${encodeURIComponent(resource.name)}`; var itemGroup = new sdk.ItemGroup({ + id: sourceMap[sourceMapKey] || undefined, name: tagName, description: tagFolder.description }); + sourceMap[sourceMapKey] = itemGroup.id; _.forEach(tagFolder.requests, (request) => { - itemGroup.items.add(this.convertRequestToItem(spec, request, components, options, schemaCache, variableStore)); + itemGroup.items.add(this.convertRequestToItem(spec, request, components, options, + schemaCache, variableStore, sourceMap)); }); // Add folders first (before requests) in generated collection @@ -810,10 +818,11 @@ module.exports = { * @param {object} options - a standard list of options that's globally passed around. Check options.js for more. * @param {object} schemaCache - object storing schemaFaker and schmeResolution caches * @param {object} variableStore - array for storing collection variables + * @param {object} sourceMap - object storing a map between OpenAPI elements and Postman collection elements. * @returns {*} Postman itemGroup or request * @no-unit-test */ - convertChildToItemGroup: function (openapi, child, components, options, schemaCache, variableStore) { + convertChildToItemGroup: function (openapi, child, components, options, schemaCache, variableStore, sourceMap) { options = _.merge({}, defaultOptions, options); var resource = child, @@ -827,12 +836,15 @@ module.exports = { // 1. folder with more than one request in its subtree // (immediate children or otherwise) if (resource.requestCount > 1) { + const sourceMapKey = `#/folders/${encodeURIComponent(resource.name)}`; // only return a Postman folder if this folder has>1 children in its subtree // otherwise we can end up with 10 levels of folders with 1 request in the end itemGroup = new sdk.ItemGroup({ + id: sourceMap[sourceMapKey] || undefined, name: utils.insertSpacesInName(resource.name) // TODO: have to add auth here (but first, auth to be put into the openapi tree) }); + sourceMap[sourceMapKey] = itemGroup.id; // If a folder has only one child which is a folder then we collapsed the child folder // with parent folder. /* eslint-disable max-depth */ @@ -841,14 +853,16 @@ module.exports = { resourceSubChild = resource.children[subChild]; resourceSubChild.name = resource.name + '/' + resourceSubChild.name; - return this.convertChildToItemGroup(openapi, resourceSubChild, components, options, schemaCache, variableStore); + return this.convertChildToItemGroup(openapi, resourceSubChild, components, options, + schemaCache, variableStore, sourceMap); } /* eslint-enable */ // recurse over child leaf nodes // and add as children to this folder for (i = 0, requestCount = resource.requests.length; i < requestCount; i++) { itemGroup.items.add( - this.convertRequestToItem(openapi, resource.requests[i], components, options, schemaCache, variableStore) + this.convertRequestToItem(openapi, resource.requests[i], components, options, + schemaCache, variableStore, sourceMap) ); } @@ -859,7 +873,7 @@ module.exports = { if (resource.children.hasOwnProperty(subChild) && resource.children[subChild].requestCount > 0) { itemGroup.items.add( this.convertChildToItemGroup(openapi, resource.children[subChild], components, options, schemaCache, - variableStore) + variableStore, sourceMap) ); } } @@ -870,7 +884,8 @@ module.exports = { // 2. it has only 1 direct request of its own if (resource.requests.length === 1) { - return this.convertRequestToItem(openapi, resource.requests[0], components, options, schemaCache, variableStore); + return this.convertRequestToItem(openapi, resource.requests[0], components, options, + schemaCache, variableStore, sourceMap); } // 3. it's a folder that has no child request @@ -878,7 +893,7 @@ module.exports = { for (subChild in resource.children) { if (resource.children.hasOwnProperty(subChild) && resource.children[subChild].requestCount === 1) { return this.convertChildToItemGroup(openapi, resource.children[subChild], components, options, schemaCache, - variableStore); + variableStore, sourceMap); } } }, @@ -1605,11 +1620,14 @@ module.exports = { * @param {*} originalRequest - the request for the example * @param {object} components - components defined in the OAS spec. These are used to * resolve references while generating params. - * @param {object} options - a standard list of options that's globally passed around. Check options.js for more. - * @param {object} schemaCache - object storing schemaFaker and schmeResolution caches + * @param {object} options - a standard list of options that's globally passed around. Check options.js for more. + * @param {object} schemaCache - object storing schemaFaker and schmeResolution caches + * @param {object} operationItem - object storing the OpenAPI operation. + * @param {object} sourceMap - object storing a map between OpenAPI elements and Postman collection elements. * @returns {Object} postman response */ - convertToPmResponse: function(response, code, originalRequest, components, options, schemaCache) { + convertToPmResponse: function(response, code, originalRequest, components, options, + schemaCache, operationItem, sourceMap) { options = _.merge({}, defaultOptions, options); var responseHeaders = [], previewLanguage = 'text', @@ -1662,7 +1680,11 @@ module.exports = { } code = code.replace(/X/g, '0'); + const encodedPath = encodeURIComponent(`/${operationItem.path}`), + sourceMapKey = `#/paths/${encodedPath}/${operationItem.method}/responses/${code}`; + sdkResponse = new sdk.Response({ + id: sourceMap[sourceMapKey] ? sourceMap[sourceMapKey] : undefined, name: response.description, code: code === 'default' ? 500 : Number(code), header: responseHeaders, @@ -1671,6 +1693,8 @@ module.exports = { }); sdkResponse._postman_previewlanguage = previewLanguage; + sourceMap[sourceMapKey] = sdkResponse.id; + return sdkResponse; }, @@ -1783,17 +1807,18 @@ module.exports = { /** * function to convert an openapi path item to postman item - * @param {*} openapi openapi object with root properties - * @param {*} operationItem path operationItem from tree structure - * @param {object} components - components defined in the OAS spec. These are used to + * @param {*} openapi openapi object with root properties + * @param {*} operationItem path operationItem from tree structure + * @param {object} components - components defined in the OAS spec. These are used to * resolve references while generating params. * @param {object} options - a standard list of options that's globally passed around. Check options.js for more. - * @param {object} schemaCache - object storing schemaFaker and schmeResolution caches - * @param {array} variableStore - array - * @returns {Object} postman request Item - * @no-unit-test + * @param {object} schemaCache - object storing schemaFaker and schmeResolution caches + * @param {array} variableStore - array + * @param {object} sourceMap - object storing a map between OpenAPI elements and Postman collection elements. + * @returns {Object} postman request Item + * @no-unit-test */ - convertRequestToItem: function(openapi, operationItem, components, options, schemaCache, variableStore) { + convertRequestToItem: function(openapi, operationItem, components, options, schemaCache, variableStore, sourceMap) { options = _.merge({}, defaultOptions, options); var reqName, pathVariables = openapi.baseUrlVariables, @@ -1933,10 +1958,15 @@ module.exports = { // handling authentication here (for http type only) authHelper = this.getAuthHelper(openapi, operation.security); + const sourceMapKey = `#/paths/${encodeURIComponent(`/${operationItem.path}`)}/${operationItem.method}`, + sourceMapRequestKey = `#/paths/${encodeURIComponent(`/${operationItem.path}`)}/${operationItem.method}.request`; + // creating the request object item = new sdk.Item({ + id: sourceMap[sourceMapKey] || undefined, name: reqName, request: { + id: sourceMap[sourceMapRequestKey] || undefined, description: operation.description, url: displayUrl || baseUrl, name: reqName, @@ -1944,6 +1974,11 @@ module.exports = { } }); + sourceMap[sourceMapKey] = item.id; + + // NOTE: This isn't actually propagating once submitted via the Postman API. + sourceMap[sourceMapRequestKey] = item.request.id; + // using the auth helper authMeta = operation['x-postman-meta']; if (authMeta && authMeta.currentHelper && authMap[authMeta.currentHelper]) { @@ -2061,7 +2096,7 @@ module.exports = { thisOriginalRequest.body = {}; } convertedResponse = this.convertToPmResponse(swagResponse, code, thisOriginalRequest, - components, options, schemaCache); + components, options, schemaCache, operationItem, sourceMap); convertedResponse && item.responses.add(convertedResponse); }); } diff --git a/lib/schemapack.js b/lib/schemapack.js index 1dac423fb..5e76b97d5 100644 --- a/lib/schemapack.js +++ b/lib/schemapack.js @@ -219,7 +219,8 @@ class SchemaPack { schemaCache = { schemaResolutionCache: this.schemaResolutionCache, schemaFakerCache: this.schemaFakerCache - }; + }, + sourceMap = this.input.sourceMap || {}; if (!this.validated) { return callback(new OpenApiErr('The schema must be validated before attempting conversion')); @@ -271,10 +272,12 @@ class SchemaPack { // For paths, All operations are grouped based on corresponding paths try { if (options.folderStrategy === 'tags') { - schemaUtils.addCollectionItemsUsingTags(openapi, generatedStore, componentsAndPaths, options, schemaCache); + schemaUtils.addCollectionItemsUsingTags(openapi, generatedStore, componentsAndPaths, options, + schemaCache, sourceMap); } else { - schemaUtils.addCollectionItemsUsingPaths(openapi, generatedStore, componentsAndPaths, options, schemaCache); + schemaUtils.addCollectionItemsUsingPaths(openapi, generatedStore, componentsAndPaths, options, + schemaCache, sourceMap); } } catch (e) { @@ -290,6 +293,7 @@ class SchemaPack { return callback(null, { result: true, + sourceMap: sourceMap, output: [{ type: 'collection', data: collectionJSON