UNPKG

openapi-to-postmanv2

Version:

Convert a given OpenAPI specification to Postman Collection v2.0

1,340 lines (1,215 loc) 207 kB
/** * This file contains util functions that need OAS-awareness * utils.js contains other util functions */ const { formatDataPath, checkIsCorrectType, isKnownType } = require('./common/schemaUtilsCommon.js'), { getConcreteSchemaUtils, isSwagger, validateSupportedVersion } = require('./common/versionUtils.js'), async = require('async'), { Variable } = require('postman-collection/lib/collection/variable'), { QueryParam } = require('postman-collection/lib/collection/query-param'), { Header } = require('postman-collection/lib/collection/header'), { ItemGroup } = require('postman-collection/lib/collection/item-group'), { Item } = require('postman-collection/lib/collection/item'), { FormParam } = require('postman-collection/lib/collection/form-param'), { RequestAuth } = require('postman-collection/lib/collection/request-auth'), { Response } = require('postman-collection/lib/collection/response'), { RequestBody } = require('postman-collection/lib/collection/request-body'), schemaFaker = require('../assets/json-schema-faker.js'), deref = require('./deref.js'), _ = require('lodash'), xmlFaker = require('./xmlSchemaFaker.js'), openApiErr = require('./error.js'), ajvValidationError = require('./ajValidation/ajvValidationError'), utils = require('./utils.js'), { Node, Trie } = require('./trie.js'), { validateSchema } = require('./ajValidation/ajvValidation'), inputValidation = require('./30XUtils/inputValidation'), traverseUtility = require('neotraverse/legacy'), { ParseError } = require('./common/ParseError.js'), SCHEMA_FORMATS = { DEFAULT: 'default', // used for non-request-body data and json XML: 'xml' // used for request-body XMLs }, URLENCODED = 'application/x-www-form-urlencoded', APP_JSON = 'application/json', APP_JS = 'application/javascript', TEXT_XML = 'text/xml', APP_XML = 'application/xml', TEXT_PLAIN = 'text/plain', TEXT_HTML = 'text/html', FORM_DATA = 'multipart/form-data', REQUEST_TYPE = { EXAMPLE: 'EXAMPLE', ROOT: 'ROOT' }, PARAMETER_SOURCE = { REQUEST: 'REQUEST', RESPONSE: 'RESPONSE' }, HEADER_TYPE = { JSON: 'json', XML: 'xml', INVALID: 'invalid' }, PREVIEW_LANGUAGE = { JSON: 'json', XML: 'xml', TEXT: 'text', HTML: 'html' }, authMap = { basicAuth: 'basic', bearerAuth: 'bearer', digestAuth: 'digest', hawkAuth: 'hawk', oAuth1: 'oauth1', oAuth2: 'oauth2', ntlmAuth: 'ntlm', awsSigV4: 'awsv4', normal: null }, propNames = { QUERYPARAM: 'query parameter', PATHVARIABLE: 'path variable', HEADER: 'header', BODY: 'request body', RESPONSE_HEADER: 'response header', RESPONSE_BODY: 'response body' }, // Specifies types of processing Refs PROCESSING_TYPE = { VALIDATION: 'VALIDATION', CONVERSION: 'CONVERSION' }, FLOW_TYPE = { authorizationCode: 'authorization_code', implicit: 'implicit', password: 'password_credentials', clientCredentials: 'client_credentials' }, // These are the methods supported in the PathItem schema // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#pathItemObject METHODS = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'], // These headers are to be validated explicitly // As these are not defined under usual parameters object and need special handling IMPLICIT_HEADERS = [ 'content-type', // 'content-type' is defined based on content/media-type of req/res body, 'accept', 'authorization' ], crypto = require('crypto'), DEFAULT_SCHEMA_UTILS = require('./30XUtils/schemaUtils30X'), { getRelatedFiles } = require('./relatedFiles'), { compareVersion } = require('./common/versionUtils.js'), parse = require('./parse'), { getBundleContentAndComponents, parseFileOrThrow } = require('./bundle.js'), MULTI_FILE_API_TYPE_ALLOWED_VALUE = 'multiFile', MEDIA_TYPE_ALL_RANGES = '*/*'; /* eslint-enable */ // See https://github.com/json-schema-faker/json-schema-faker/tree/master/docs#available-options schemaFaker.option({ requiredOnly: false, optionalsProbability: 1.0, // always add optional fields maxLength: 256, minItems: 1, // for arrays maxItems: 20, // limit on maximum number of items faked for (type: arrray) useDefaultValue: true, ignoreMissingRefs: true, avoidExampleItemsLength: true // option to avoid validating type array schema example's minItems and maxItems props. }); /** * * @param {*} input - input string that needs to be hashed * @returns {*} sha1 hash of the string */ function hash(input) { return crypto.createHash('sha1').update(input).digest('base64'); } /** * Remove or keep the deprecated properties according to the option * @param {object} resolvedSchema - the schema to verify properties * @param {boolean} includeDeprecated - Whether to include the deprecated properties * @returns {undefined} undefined */ function verifyDeprecatedProperties(resolvedSchema, includeDeprecated) { traverseUtility(resolvedSchema.properties).forEach(function (property) { if (property && typeof property === 'object') { if (property.deprecated === true && includeDeprecated === false) { this.delete(); } } }); } /** * Adds XML version content for XML specific bodies. * * @param {*} bodyContent - XML Body content * @returns {*} - Body with correct XML version content */ function getXmlVersionContent (bodyContent) { bodyContent = (typeof bodyContent === 'string') ? bodyContent : ''; const regExp = new RegExp('([<\\?xml]+[\\s{1,}]+[version="\\d.\\d"]+[\\sencoding="]+.{1,15}"\\?>)'); let xmlBody = bodyContent; if (!bodyContent.match(regExp)) { const versionContent = '<?xml version="1.0" encoding="UTF-8"?>\n'; xmlBody = versionContent + xmlBody; } return xmlBody; } /** * Safe wrapper for schemaFaker that resolves references and * removes things that might make schemaFaker crash * @param {*} oldSchema the schema to fake * @param {string} resolveTo The desired JSON-generation mechanism (schema: prefer using the JSONschema to generate a fake object, example: use specified examples as-is). Default: schema * @param {*} resolveFor - resolve refs for flow validation/conversion (value to be one of VALIDATION/CONVERSION) * @param {string} parameterSourceOption Specifies whether the schema being faked is from a request or response. * @param {*} components list of predefined components (with schemas) * @param {string} schemaFormat default or xml * @param {object} schemaCache - object storing schemaFaker and schemaResolution caches * @param {object} options - a standard list of options that's globally passed around. Check options.js for more. * @returns {object} fakedObject */ function safeSchemaFaker (oldSchema, resolveTo, resolveFor, parameterSourceOption, components, schemaFormat, schemaCache, options) { var prop, key, resolvedSchema, fakedSchema, schemaFakerCache = _.get(schemaCache, 'schemaFakerCache', {}); let concreteUtils = components && components.hasOwnProperty('concreteUtils') ? components.concreteUtils : DEFAULT_SCHEMA_UTILS; const indentCharacter = options.indentCharacter, includeDeprecated = options.includeDeprecated; resolvedSchema = deref.resolveRefs(oldSchema, parameterSourceOption, components, { resolveFor, resolveTo, stackLimit: options.stackLimit, analytics: _.get(schemaCache, 'analytics', {}) }); resolvedSchema = concreteUtils.fixExamplesByVersion(resolvedSchema); key = JSON.stringify(resolvedSchema); if (resolveTo === 'schema') { key = 'resolveToSchema ' + key; schemaFaker.option({ useExamplesValue: false, useDefaultValue: true }); } else if (resolveTo === 'example') { key = 'resolveToExample ' + key; schemaFaker.option({ useExamplesValue: true }); } if (resolveFor === PROCESSING_TYPE.VALIDATION) { schemaFaker.option({ useDefaultValue: false, avoidExampleItemsLength: false }); } if (schemaFormat === 'xml') { key += ' schemaFormatXML'; } else { key += ' schemaFormatDEFAULT'; } key = hash(key); if (schemaFakerCache[key]) { return schemaFakerCache[key]; } if (resolvedSchema.properties) { // If any property exists with format:binary (and type: string) schemaFaker crashes // we just delete based on format=binary for (prop in resolvedSchema.properties) { if (resolvedSchema.properties.hasOwnProperty(prop)) { if (resolvedSchema.properties[prop].format === 'binary') { delete resolvedSchema.properties[prop].format; } } } verifyDeprecatedProperties(resolvedSchema, includeDeprecated); } try { if (schemaFormat === SCHEMA_FORMATS.XML) { fakedSchema = xmlFaker(null, resolvedSchema, indentCharacter, resolveTo); schemaFakerCache[key] = fakedSchema; return fakedSchema; } // for JSON, the indentCharacter will be applied in the JSON.stringify step later on fakedSchema = schemaFaker(resolvedSchema, null, _.get(schemaCache, 'schemaValidationCache')); schemaFakerCache[key] = fakedSchema; return fakedSchema; } catch (e) { console.warn( 'Error faking a schema. Not faking this schema. Schema:', resolvedSchema, 'Error', e ); return null; } } /** * Verifies if the deprecated operations should be added * * @param {object} operation - openAPI operation object * @param {object} options - a standard list of options that's globally passed around. Check options.js for more. * @returns {boolean} whether to add or not the deprecated operation */ function shouldAddDeprecatedOperation (operation, options) { if (typeof operation === 'object') { return !operation.deprecated || (operation.deprecated === true && options.includeDeprecated === true); } return false; } module.exports = { safeSchemaFaker: safeSchemaFaker, /** * Analyzes the spec to determine the size of the spec, * number of request that will be generated out this spec, and * number or references present in the spec. * * @param {Object} spec JSON * @return {Object} returns number of requests that will be generated, * number of refs present and size of the spec. */ analyzeSpec: function (spec) { var size, numberOfRefs = 0, numberOfExamples = 0, specString, numberOfRequests = 0; // Stringify and add whitespaces as there would be in a normal file // To get accurate disk size specString = JSON.stringify(spec); // Size in MB size = Buffer.byteLength(specString, 'utf8') / (1024 * 1024); // No need to check for number of requests or refs if the size is greater than 8 MB // The complexity is 10. if (size < 8) { // Finds the number of requests that would be generated from this spec if (_.isObject(spec.paths)) { Object.values(spec.paths).forEach((value) => { _.keys(value).forEach((key) => { if (METHODS.includes(key)) { numberOfRequests++; } }); }); } // Number of times the term $ref is repeated in the spec. numberOfRefs = (specString.match(/\$ref/g) || []).length; // Number of times `example` is present numberOfExamples = (specString.match(/example/g) || []).length; } return { size, numberOfRefs, numberOfRequests, numberOfExamples }; }, /** Determines the complexity score and stackLimit * * @param {Object} analysis the object returned by analyzeSpec function * @param {Object} options Current options * * @returns {Object} computedOptions - contains two new options i.e. stackLimit and complexity score */ determineOptions: function (analysis, options) { let size = analysis.size, numberOfRefs = analysis.numberOfRefs, numberOfRequests = analysis.numberOfRequests; var computedOptions = _.clone(options); computedOptions.stackLimit = 10; // This is the score that is given to each spec on the basis of the // number of references present in spec and the number of requests that will be generated. // This ranges from 0-10. computedOptions.complexityScore = 0; // Anything above the size of 8MB will be considered a big spec and given the // least stack limit and the highest complexity score. if (size >= 8) { console.warn('Complexity score = 10'); computedOptions.stackLimit = 2; computedOptions.complexityScore = 10; return computedOptions; } else if (size >= 5 || numberOfRequests > 1500 || numberOfRefs > 1500) { computedOptions.stackLimit = 3; computedOptions.complexityScore = 9; return computedOptions; } else if (size >= 1 && (numberOfRequests > 1000 || numberOfRefs > 1000)) { computedOptions.stackLimit = 5; computedOptions.complexityScore = 8; return computedOptions; } else if (numberOfRefs > 500 || numberOfRequests > 500) { computedOptions.stackLimit = 6; computedOptions.complexityScore = 6; return computedOptions; } return computedOptions; }, /** * Changes the {} around scheme and path variables to :variable * @param {string} url - the url string * @returns {string} string after replacing /{pet}/ with /:pet/ */ fixPathVariablesInUrl: function (url) { // URL should always be string so update value if non-string value is found if (typeof url !== 'string') { return ''; } // All complicated logic removed // This simply replaces all instances of {text} with {{text}} // text cannot have any of these 3 chars: /{} // {{text}} will not be converted let replacer = function (match, p1, offset, string) { if (string[offset - 1] === '{' && string[offset + match.length + 1] !== '}') { return match; } return '{' + p1 + '}'; }; return _.isString(url) ? url.replace(/(\{[^\/\{\}]+\})/g, replacer) : ''; }, /** * Changes path structure that contains {var} to :var and '/' to '_' * This is done so generated collection variable is in correct format * i.e. variable '{{item/{itemId}}}' is considered separates variable in URL by collection sdk * @param {string} path - path defined in openapi spec * @returns {string} - string after replacing {itemId} with :itemId */ fixPathVariableName: function (path) { // Replaces structure like 'item/{itemId}' into 'item-itemId-Url' return path.replace(/\//g, '-').replace(/[{}]/g, '') + '-Url'; }, /** * Returns a description that's usable at the collection-level * Adds the collection description and uses any relevant contact info * @param {*} openapi The JSON representation of the OAS spec * @returns {string} description */ getCollectionDescription: function (openapi) { let description = _.get(openapi, 'info.description', ''); if (_.get(openapi, 'info.contact')) { let contact = []; if (openapi.info.contact.name) { contact.push(' Name: ' + openapi.info.contact.name); } if (openapi.info.contact.email) { contact.push(' Email: ' + openapi.info.contact.email); } if (contact.length > 0) { // why to add unnecessary lines if there is no description if (description !== '') { description += '\n\n'; } description += 'Contact Support:\n' + contact.join('\n'); } } return description; }, /** * Get the format of content type header * @param {string} cTypeHeader - the content type header string * @returns {string} type of content type header */ getHeaderFamily: function(cTypeHeader) { let mediaType = this.parseMediaType(cTypeHeader); if (mediaType.type === 'application' && (mediaType.subtype === 'json' || _.endsWith(mediaType.subtype, '+json'))) { return HEADER_TYPE.JSON; } if ((mediaType.type === 'application' || mediaType.type === 'text') && (mediaType.subtype === 'xml' || _.endsWith(mediaType.subtype, '+xml'))) { return HEADER_TYPE.XML; } return HEADER_TYPE.INVALID; }, /** * Gets the description of the parameter. * If the parameter is required, it prepends a `(Requried)` before the parameter description * If the parameter type is enum, it appends the possible enum values * @param {object} parameter - input param for which description needs to be returned * @returns {string} description of the parameters */ getParameterDescription: function(parameter) { if (!_.isObject(parameter)) { return ''; } return (parameter.required ? '(Required) ' : '') + (parameter.description || '') + (parameter.enum ? ' (This can only be one of ' + parameter.enum + ')' : ''); }, /** * Given parameter objects, it assigns example/examples of parameter object as schema example. * * @param {Object} parameter - parameter object * @returns {null} - null */ assignParameterExamples: function (parameter) { let example = _.get(parameter, 'example'), examples = _.values(_.get(parameter, 'examples')); if (example !== undefined) { _.set(parameter, 'schema.example', example); } else if (examples) { let exampleToUse = _.get(examples, '[0].value'); !_.isUndefined(exampleToUse) && (_.set(parameter, 'schema.example', exampleToUse)); } }, /** * Converts the necessary server variables to the * something that can be added to the collection * TODO: Figure out better description * @param {object} serverVariables - Object containing the server variables at the root/path-item level * @param {string} keyName - an additional key to add the serverUrl to the variable list * @param {string} serverUrl - URL from the server object * @returns {object} modified collection after the addition of the server variables */ convertToPmCollectionVariables: function(serverVariables, keyName, serverUrl = '') { var variables = []; if (serverVariables) { _.forOwn(serverVariables, (value, key) => { let description = this.getParameterDescription(value); variables.push(new Variable({ key: key, value: value.default || '', description: description })); }); } if (keyName) { variables.push(new Variable({ key: keyName, value: serverUrl, type: 'string' })); } return variables; }, /** * Returns params applied to specific operation with resolved references. Params from parent * blocks (collection/folder) are merged, so that the request has a flattened list of params needed. * OperationParams take precedence over pathParams * @param {array} operationParam operation (Postman request)-level params. * @param {array} pathParam are path parent-level params. * @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. * @returns {*} combined requestParams from operation and path params. */ getRequestParams: function(operationParam, pathParam, components, options) { if (!Array.isArray(operationParam)) { operationParam = []; } if (!Array.isArray(pathParam)) { pathParam = []; } pathParam.forEach((param, index, arr) => { if (_.has(param, '$ref')) { arr[index] = this.getRefObject(param.$ref, components, options); } }); operationParam.forEach((param, index, arr) => { if (_.has(param, '$ref')) { arr[index] = this.getRefObject(param.$ref, components, options); } }); if (_.isEmpty(pathParam)) { return operationParam; } else if (_.isEmpty(operationParam)) { return pathParam; } // If both path and operation params exist, // we need to de-duplicate // A param with the same name and 'in' value from operationParam // will get precedence var reqParam = operationParam.slice(); pathParam.forEach((param) => { var dupParamIndex = operationParam.findIndex(function(element) { return element.name === param.name && element.in === param.in && // the below two conditions because undefined === undefined returns true element.name && param.name && element.in && param.in; }); if (dupParamIndex === -1) { // if there's no duplicate param in operationParam, // use the one from the common pathParam list // this ensures that operationParam is given precedence reqParam.push(param); } else { // duplicate exists; prefer operation-level param but fallback description from path-level var opParam = reqParam[dupParamIndex]; if (!_.get(opParam, 'description') && _.get(param, 'description')) { opParam.description = param.description; } } }); return reqParam; }, /** * Generates a Trie-like folder structure from the root path object of the OpenAPI specification. * @param {Object} spec - specification in json format * @param {object} options - a standard list of options that's globally passed around. Check options.js for more. * @param {boolean} fromWebhooks - true when we are creating the webhooks group trie - default: false * @returns {Object} - The final object consists of the tree structure */ generateTrieFromPaths: function (spec, options, fromWebhooks = false) { let concreteUtils = getConcreteSchemaUtils({ type: 'json', data: spec }), specComponentsAndUtils = { concreteUtils }; var paths = fromWebhooks ? spec.webhooks : spec.paths, // the first level of paths currentPath = '', currentPathObject = '', commonParams = '', collectionVariables = {}, operationItem, pathLevelServers = '', pathLength, currentPathRequestCount, currentNode, i, summary, path, pathMethods = [], // creating a root node for the trie (serves as the root dir) trie = new Trie(new Node({ name: '/' })), // returns a list of methods supported at each pathItem // some pathItem props are not methods // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#pathItemObject getPathMethods = function(pathKeys) { var methods = []; // TODO: Show warning for incorrect schema if !pathKeys pathKeys && pathKeys.forEach(function(element) { if (METHODS.includes(element)) { methods.push(element); } }); return methods; }; Object.assign(specComponentsAndUtils, concreteUtils.getRequiredData(spec)); for (path in paths) { if (paths.hasOwnProperty(path) && typeof paths[path] === 'object' && paths[path]) { currentPathObject = paths[path]; // discard the leading slash, if it exists if (path[0] === '/') { path = path.substring(1); } if (fromWebhooks) { // When we work with webhooks the path value corresponds to the webhook's name // and it won't be treated as path currentPath = path === '' ? ['(root)'] : [path]; } else { // split the path into indiv. segments for trie generation // unless path it the root endpoint currentPath = path === '' ? ['(root)'] : path.split('/').filter((pathItem) => { // remove any empty pathItems that might have cropped in // due to trailing or double '/' characters return pathItem !== ''; }); } pathLength = currentPath.length; // get method names available for this path pathMethods = getPathMethods(_.keys(currentPathObject)); // the number of requests under this node currentPathRequestCount = pathMethods.length; currentNode = trie.root; // adding children for the nodes in the trie // start at the top-level and do a DFS for (i = 0; i < pathLength; i++) { /** * Use hasOwnProperty to determine if property exists as certain JS fuction are present * as part of each object. e.g. `constructor`. */ if (!(typeof currentNode.children === 'object' && currentNode.children.hasOwnProperty(currentPath[i]))) { // if the currentPath doesn't already exist at this node, // add it as a folder currentNode.addChildren(currentPath[i], new Node({ name: currentPath[i], requestCount: 0, requests: [], children: {}, type: 'item-group', childCount: 0 })); // We are keeping the count children in a folder which can be a request or folder // For ex- In case of /pets/a/b, pets has 1 childCount (i.e a) currentNode.childCount += 1; } // requestCount increment for the node we just added currentNode.children[currentPath[i]].requestCount += currentPathRequestCount; currentNode = currentNode.children[currentPath[i]]; } // extracting common parameters for all the methods in the current path item if (currentPathObject.hasOwnProperty('parameters')) { commonParams = currentPathObject.parameters; } // storing common path/collection vars from the server object at the path item level if (currentPathObject.hasOwnProperty('servers')) { pathLevelServers = currentPathObject.servers; collectionVariables[this.fixPathVariableName(path)] = pathLevelServers[0]; delete currentPathObject.servers; } // add methods to node // eslint-disable-next-line no-loop-func _.each(pathMethods, (method) => { // base operationItem operationItem = currentPathObject[method] || {}; // params - these contain path/header/body params operationItem.parameters = this.getRequestParams(operationItem.parameters, commonParams, specComponentsAndUtils, options); summary = operationItem.summary || operationItem.description; _.isFunction(currentNode.addMethod) && currentNode.addMethod({ name: summary, method: method, path: path, properties: operationItem, type: 'item', servers: pathLevelServers || undefined }); currentNode.childCount += 1; }); pathLevelServers = undefined; commonParams = []; } } return { tree: trie, variables: collectionVariables // server variables that are to be converted into collection variables. }; }, addCollectionItemsFromWebhooks: function(spec, generatedStore, components, options, schemaCache) { let webhooksObj = this.generateTrieFromPaths(spec, options, true), webhooksTree = webhooksObj.tree, webhooksFolder = new ItemGroup({ name: 'Webhooks' }), variableStore = {}, webhooksVariables = []; if (_.keys(webhooksTree.root.children).length === 0) { return; } for (let child in webhooksTree.root.children) { if ( webhooksTree.root.children.hasOwnProperty(child) && webhooksTree.root.children[child].requestCount > 0 ) { webhooksVariables.push(new Variable({ key: this.cleanWebhookName(child), value: '/', type: 'string' })); webhooksFolder.items.add( this.convertChildToItemGroup( spec, webhooksTree.root.children[child], components, options, schemaCache, variableStore, true ), ); } } generatedStore.collection.items.add(webhooksFolder); webhooksVariables.forEach((variable) => { generatedStore.collection.variables.add(variable); }); }, /** * Adds Postman Collection Items using paths. * Folders are grouped based on trie that's generated using all paths. * * @param {object} spec - openAPI spec object * @param {object} generatedStore - the store that holds the generated collection. Modified in-place * @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 * @returns {void} - generatedStore is modified in-place */ addCollectionItemsUsingPaths: function (spec, generatedStore, components, options, schemaCache) { var folderTree, folderObj, child, key, variableStore = {}; /** We need a trie because the decision of whether or not a node is a folder or request can only be made once the whole trie is generated This has a .trie and a .variables prop */ folderObj = this.generateTrieFromPaths(spec, options); folderTree = folderObj.tree; /* * these are variables identified at the collection level * they need to be added explicitly to collection variables * deeper down in the trie, variables will be added directly to folders * If the folderObj.variables have their own variables, we add * them to the collectionVars */ if (folderObj.variables) { _.forOwn(folderObj.variables, (server, key) => { // TODO: Figure out what this does this.convertToPmCollectionVariables( server.variables, // these are path variables in the server block key, // the name of the variable this.fixPathVariablesInUrl(server.url) ).forEach((element) => { generatedStore.collection.variables.add(element); }); }); } // Adds items from the trie into the collection that's in the store for (child in folderTree.root.children) { // A Postman request or folder is added if atleast one request is present in that sub-child's tree // requestCount is a property added to each node (folder/request) while constructing the trie 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) ); } } for (key in variableStore) { // variableStore contains all the kinds of variable created. // Add only the variables with type 'collection' to generatedStore.collection.variables if (variableStore[key].type === 'collection') { const collectionVar = new Variable(variableStore[key]); generatedStore.collection.variables.add(collectionVar); } } }, /** * Adds Postman Collection Items using tags. * Each tag from OpenAPI tags object is mapped to a collection item-group (Folder), and all operation that has * corresponding tag in operation object's tags array is included in mapped item-group. * * @param {object} spec - openAPI spec object * @param {object} generatedStore - the store that holds the generated collection. Modified in-place * @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 * @returns {object} returns an object containing objects of tags and their requests */ addCollectionItemsUsingTags: function(spec, generatedStore, components, options, schemaCache) { var globalTags = spec.tags || [], paths = spec.paths || {}, pathMethods, variableStore = {}, tagFolders = {}; // adding globalTags in the tagFolder object that are defined at the root level _.forEach(globalTags, (globalTag) => { tagFolders[globalTag.name] = { description: _.get(globalTag, 'description', ''), requests: [] }; }); _.forEach(paths, (currentPathObject, path) => { var commonParams = [], collectionVariables, pathLevelServers = ''; // discard the leading slash, if it exists if (path[0] === '/') { path = path.substring(1); } // extracting common parameters for all the methods in the current path item if (currentPathObject.hasOwnProperty('parameters')) { commonParams = currentPathObject.parameters; } // storing common path/collection vars from the server object at the path item level if (currentPathObject.hasOwnProperty('servers')) { pathLevelServers = currentPathObject.servers; // add path level server object's URL as collection variable collectionVariables = this.convertToPmCollectionVariables( pathLevelServers[0].variables, // these are path variables in the server block this.fixPathVariableName(path), // the name of the variable this.fixPathVariablesInUrl(pathLevelServers[0].url) ); _.forEach(collectionVariables, (collectionVariable) => { generatedStore.collection.variables.add(collectionVariable); }); delete currentPathObject.servers; } // get available method names for this path (path item object can have keys apart from operations) pathMethods = _.filter(_.keys(currentPathObject), (key) => { return _.includes(METHODS, key); }); _.forEach(pathMethods, (pathMethod) => { var summary, operationItem = currentPathObject[pathMethod] || {}, localTags = operationItem.tags; // params - these contain path/header/body params operationItem.parameters = this.getRequestParams(operationItem.parameters, commonParams, components, options); summary = operationItem.summary || operationItem.description; // add the request which has not any tags if (_.isEmpty(localTags)) { let tempRequest = { name: summary, method: pathMethod, path: path, properties: operationItem, type: 'item', servers: pathLevelServers || undefined }; if (shouldAddDeprecatedOperation(tempRequest.properties, options)) { generatedStore.collection.items.add(this.convertRequestToItem( spec, tempRequest, components, options, schemaCache, variableStore)); } } else { _.forEach(localTags, (localTag) => { // add undefined tag object with empty description if (!_.has(tagFolders, localTag)) { tagFolders[localTag] = { description: '', requests: [] }; } tagFolders[localTag].requests.push({ name: summary, method: pathMethod, path: path, properties: operationItem, type: 'item', servers: pathLevelServers || undefined }); }); } }); }); // 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) => { var itemGroup = new ItemGroup({ name: tagName, description: tagFolder.description }); _.forEach(tagFolder.requests, (request) => { if (shouldAddDeprecatedOperation(request.properties, options)) { itemGroup.items.add( this.convertRequestToItem(spec, request, components, options, schemaCache, variableStore)); } }); // Add folders first (before requests) in generated collection generatedStore.collection.items.prepend(itemGroup); }); // variableStore contains all the kinds of variable created. // Add only the variables with type 'collection' to generatedStore.collection.variables _.forEach(variableStore, (variable) => { if (variable.type === 'collection') { const collectionVar = new Variable(variable); generatedStore.collection.variables.add(collectionVar); } }); }, /** * Generates an array of SDK Variables from the common and provided path vars * @param {string} type - Level at the tree root/path level. Can be method/root/param. * method: request(operation)-level, root: spec-level, param: url-level * @param {Array<object>} providedPathVars - Array of path variables * @param {object|array} commonPathVars - Object of path variables taken from the specification * @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 * @returns {Array<object>} returns an array of Collection SDK Variable */ convertPathVariables: function(type, providedPathVars, commonPathVars, components, options, schemaCache) { var variables = []; // converting the base uri path variables, if any // commonPathVars is an object for type = root/method // array otherwise if (type === 'root' || type === 'method') { _.forOwn(commonPathVars, (value, key) => { let description = this.getParameterDescription(value); variables.push({ key: key, value: type === 'root' ? '{{' + key + '}}' : value.default, description: description }); }); } else { _.forEach(commonPathVars, (variable) => { let fakedData, convertedPathVar; this.assignParameterExamples(variable); fakedData = options.schemaFaker ? safeSchemaFaker(variable.schema || {}, options.requestParametersResolution, PROCESSING_TYPE.CONVERSION, PARAMETER_SOURCE.REQUEST, components, SCHEMA_FORMATS.DEFAULT, schemaCache, options) : ''; convertedPathVar = this.convertParamsWithStyle(variable, fakedData, PARAMETER_SOURCE.REQUEST, components, schemaCache, options); variables = _.concat(variables, convertedPathVar); }); } // keep already provided varables (server variables) at last return _.concat(variables, providedPathVars); }, /** * convert childItem from OpenAPI to Postman itemGroup if requestCount(no of requests inside childitem)>1 * otherwise return postman request * @param {*} openapi object with root-level data like pathVariables baseurl * @param {*} child object is of type itemGroup or request * resolve references while generating params. * @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} variableStore - array for storing collection variables * @param {boolean} fromWebhooks - true if we are processing the webhooks group, false by default * @returns {*} Postman itemGroup or request * @no-unit-test */ convertChildToItemGroup: function (openapi, child, components, options, schemaCache, variableStore, fromWebhooks = false) { var resource = child, itemGroup, subChild, i, requestCount; // 3 options: // 1. folder with more than one request in its subtree // (immediate children or otherwise) if (resource.requestCount > 1) { // 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 ItemGroup({ name: resource.name // TODO: have to add auth here (but first, auth to be put into the openapi tree) }); // If a folder has only one child which is a folder then we collapsed the child folder // with parent folder. /* eslint-disable max-depth */ if (resource.childCount === 1 && options.collapseFolders) { let subChild = _.keys(resource.children)[0], resourceSubChild = resource.children[subChild]; resourceSubChild.name = resource.name + '/' + resourceSubChild.name; return this.convertChildToItemGroup(openapi, resourceSubChild, components, options, schemaCache, variableStore, fromWebhooks); } /* eslint-enable */ // recurse over child leaf nodes // and add as children to this folder for (i = 0, requestCount = resource.requests.length; i < requestCount; i++) { if (shouldAddDeprecatedOperation(resource.requests[i].properties, options)) { itemGroup.items.add( this.convertRequestToItem(openapi, resource.requests[i], components, options, schemaCache, variableStore, fromWebhooks) ); } } // recurse over child folders // and add as child folders to this folder /* eslint-disable max-depth*/ for (subChild in resource.children) { if (resource.children.hasOwnProperty(subChild) && resource.children[subChild].requestCount > 0) { itemGroup.items.add( this.convertChildToItemGroup(openapi, resource.children[subChild], components, options, schemaCache, variableStore, fromWebhooks) ); } } /* eslint-enable */ return itemGroup; } // 2. it has only 1 direct request of its own if (resource.requests.length === 1) { if (shouldAddDeprecatedOperation(resource.requests[0].properties, options)) { return this.convertRequestToItem(openapi, resource.requests[0], components, options, schemaCache, variableStore, fromWebhooks); } } // 3. it's a folder that has no child request // but one request somewhere in its child folders 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, fromWebhooks); } } }, /** * Gets helper object based on the root spec and the operation.security object * @param {*} openapi - the json object representing the OAS spec * @param {Array<object>} securitySet - the security object at an operation level * @returns {object} The authHelper to use while constructing the Postman Request. This is * not directly supported in the SDK - the caller needs to determine the header/body based on the return * value * @no-unit-test */ getAuthHelper: function(openapi, securitySet) { var securityDef, helper; // return false if security set is not defined // or is an empty array // this will set the request's auth to null - which is 'inherit from parent' if (!securitySet || (Array.isArray(securitySet) && securitySet.length === 0)) { return null; } _.forEach(securitySet, (security) => { if (_.isObject(security) && _.isEmpty(security)) { helper = { type: 'noauth' }; return false; } securityDef = _.get(openapi, ['securityDefs', _.keys(security)[0]]); if (!_.isObject(securityDef)) { return; } else if (securityDef.type === 'http') { if (_.toLower(securityDef.scheme) === 'basic') { helper = { type: 'basic', basic: [ { key: 'username', value: '{{basicAuthUsername}}' }, { key: 'password', value: '{{basicAuthPassword}}' } ] }; } else if (_.toLower(securityDef.scheme) === 'bearer') { helper = { type: 'bearer', bearer: [{ key: 'token', value: '{{bearerToken}}' }] }; } else if (_.toLower(securityDef.scheme) === 'digest') { helper = { type: 'digest', digest: [ { key: 'username', value: '{{digestAuthUsername}}' }, { key: 'password', value: '{{digestAuthPassword}}' }, { key: 'realm', value: '{{realm}}' } ] }; } else if (_.toLower(securityDef.scheme) === 'oauth' || _.toLower(securityDef.scheme) === 'oauth1') { helper = { type: 'oauth1', oauth1: [ { key: 'consumerSecret', value: '{{consumerSecret}}' }, { key: 'consumerKey', value: '{{consumerKey}}' }, { key: 'addParamsToHeader', value: true } ] }; } } else if (securityDef.type === 'oauth2') { let flowObj, currentFlowType; helper = { type: 'oauth2', oauth2: [] }; if (_.isObject(securityDef.flows) && FLOW_TYPE[_.keys(securityDef.flows)[0]]) { /* //===================[]========================\\ || OAuth2 Flow Name || Key name in collection || |]===================[]========================[| || clientCredentials || client_credentials || || password || password_credentials || || implicit || implicit || || authorizationCode || authorization_code || \\===================[]========================// Ref : https://swagger.io/docs/specification/authentication/oauth2/ In case of multiple flow types, the first one will be preferred and passed on to the collection. Other flow types in collection which are not explicitly present in OA 3 • "authorization_code_with_pkce" */ currentFlowType = FLOW_TYPE[_.keys(securityDef.flows)[0]]; flowObj = _.get(securityDef, `flows.${_.keys(securityDef.flows)[0]}`); } if (currentFlowType) { // Means the flow is of supported type // Fields supported by all flows -> refreshUrl, scopes if (!_.isEmpty(flowObj.scopes)) { helper.oauth2.push({ key: 'scope', value: _.keys(flowObj.scopes).join(' ') }); } /* refreshURL is indicated by key 'redirect_uri' in collection Ref : https://stackoverflow.com/a/42131366/19078409 */ if (!_.isEmpty(flowObj.refreshUrl)) { helper.oauth2.push({ key: 'redirect_uri', value: _.isString(flowObj.refreshUrl) ? flowObj.refreshUrl : '{{oAuth2CallbackURL}}' }); } // Fields supported by all flows except implicit -> tokenUrl if (currentFlowType !== FLOW_TYPE.implicit) { if (!_.isEmpty(flowObj.tokenUrl)) { helper.oauth2.push({ key: 'accessTokenUrl', value: _.isString(flowObj.tokenUrl) ? flowObj.tokenUrl : '{{oAuth2AccessTokenURL}}' }); } } // Fields supported by all flows all except password, clientCredentials -> authorizationUrl if (currentFlowType !== FLOW_TYPE.password && currentFlowType !== FLOW_TYPE.clientCredentials) { if (!_.isEmpty(flowObj.authorizationUrl)) { helper.oauth2.push({ key: 'authUrl', value: _.isString(flowObj.authorizationUrl) ? flowObj.authorizationUrl : '{{oAuth2AuthURL}}' }); } } helper.oauth2.push({ key: 'grant_type', value: currentFlowType }); } } else if (securityDef.type === 'apiKey') { helper = { type: 'apikey', apikey: [ { key: 'key', value: _.isString(securityDef.name) ? securityDef.name : '{{apiKeyName}}' }, { key: 'value', value: '{{apiKey}}' }, { key: 'in', value: _.includes(['query', 'header'], securityDef.in) ? securityDef.in : 'header' } ] }; } // stop searching for helper if valid auth scheme is found if (!_.isEmpty(helper)) { return false; } }); return helper; }, /** * Generates appropriate collection element based on parameter location * * @param {Object} param - Parameter object habing key, value and description (optional) * @param {String} location - Parameter location ("in" property of OAS defined parameter object) * @returns {Object} - SDK element */ generateSdkParam: function (param, location) { const sdkElementMap = { 'query': QueryParam, 'header': Header, 'path': Variable }; let generatedParam = { key: param.key, value: param.value }; _.has(param, 'disabled') && (generatedParam.disabled = param.disabled); // use appropriate sdk element based on location parmaeter is in for param generation if (sdkElementMap[location]) { generatedParam = new sdkElementMap[location](generatedParam); } param.