openapi-to-postmanv2
Version:
Convert a given OpenAPI specification to Postman Collection v2.0
1,340 lines (1,215 loc) • 207 kB
JavaScript
/**
* 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.