var sdk = require('postman-collection'), schemaFaker = require('json-schema-faker'), parse = require('./parse.js'), deref = require('./deref.js'), _ = require('lodash'); const URLENCODED = 'application/x-www-form-urlencoded', APP_JSON = 'application/json', APP_JS = 'application/javascript', APP_XML = 'application/xml', TEXT_XML = 'text/xml', TEXT_PLAIN = 'text/plain', TEXT_HTML = 'text/html', FORM_DATA = 'multipart/form-data'; schemaFaker.option({ requiredOnly: false, minLength: 4, maxLength: 4, minItems: 1, maxItems: 2 }); /** * Removes things that might make schemaFaker crash * @param {*} oldSchema the schema to fake * @param {*} components list of predefined components (with schemas) * @returns {object} fakedObject */ function safeSchemaFaker(oldSchema, components) { var prop, savedSchemaName, schema = deref.resolveRefs(oldSchema, components); if (schema.anyOf) { return safeSchemaFaker(schema.anyOf[0], components); } if (schema.$ref) { // points to an existing location // .split will return [#, components, schemas, schemaName] try { savedSchemaName = schema.$ref.split('/').slice(3)[0]; if (components.schemas[savedSchemaName]) { return safeSchemaFaker(components.schemas[savedSchemaName], components); } return ''; } catch (e) { // could not slice or fake reference return ''; } } if (schema.properties) { // 1. If any property exists with format:binary (and type: string) schemaFaker crashes // we just delete based on format=binary for (prop in schema.properties) { if (schema.properties.hasOwnProperty(prop)) { if (schema.properties[prop].format === 'binary') { delete schema.properties[prop].format; } } } } return schemaFaker(schema); } /** * Class for the node of the tree containing the folders * @param {object} options - Contains details about the folder node * @returns {void} */ function Node (options) { this.name = options ? options.name : '/'; this.requestCount = options ? options.requestCount : 0; this.type = options ? options.type : 'item'; this.children = {}; // object of objects this.requests = options ? options.requests : []; // request will be an array of objects this.addChildren = function (child, value) { this.children[child] = value; }; this.addMethod = function (method) { this.requests.push(method); }; } class Trie { constructor(node) { this.root = node; } } module.exports = { // list of predefined schemas in components components: {}, options: {}, // enviVariables: [], /** * Adds the neccessary server variables to the collection * @param {object} collection - POSTMAN Collection JSON object * @param {object} serverVariables - Object containing the server variables at the root/path-item level * @param {string} level - root / path-item level * @param {string} serverUrl - URL from the server object * * @returns {object} modified collection after the addition of the server variables */ addCollectionVariables: function(collection, serverVariables, level, serverUrl = '') { var modifiedCollection = collection; if (serverVariables) { _.forOwn(serverVariables, (value, key) => { modifiedCollection.variables.add(new sdk.Variable({ id: key, value: value.default || '', description: value.description + (value.enum || '') })); }); } else { modifiedCollection.variables.add(new sdk.Variable({ id: level, value: serverUrl, type: 'string' })); } return modifiedCollection; }, /** * Parses an open api string as a YAML or JSON * @param {YAML/JSON} openApiSpec - The swagger 3.0.0 specification specified in either YAML or JSON * @returns {Object} - Contains the folder trie and the array of collection variables to be created */ parseSpec: function (openApiSpec) { var openApiObj = openApiSpec, rootValidation; // If the open api specification is a string could be YAML or JSON if (typeof openApiSpec === 'string') { try { openApiObj = parse.asYaml(openApiSpec); } catch (yamlException) { // Could be a JSON as well try { openApiObj = parse.asJson(openApiSpec); } catch (jsonException) { // Its neither JSON nor YAML return { result: false, reason: 'Invalid format. Input must be in YAML or JSON format' }; } } } // spec is a valid JSON // Validate the root level object rootValidation = parse.validateRoot(openApiObj); if (!rootValidation.result) { return { result: false, reason: rootValidation.reason }; } // Valid openapi root object return { result: true, openapi: rootValidation.openapi }; }, /** * Generates a Trie like folder structure from the path object of the OPENAPI specification * @param {Object} spec - specification in json format * @returns {Object} - The final object consists of the tree structure and collection variables */ generateTrieFromPaths: function (spec) { var paths = spec.paths, currentPath = '', currentPathObject = '', commonParams = '', collectionVariables = [], root = new Node({ name: '/' }), method, operationItem, pathLevelServers = '', pathLength, currentPathRequestCount, currentNode, i, summary, path, // creating a root node for the trie (serves as the root dir) trie = new Trie(root); for (path in paths) { if (paths.hasOwnProperty(path)) { // decalring a variable to be in this loops context only currentPathObject = paths[path]; // console.log("path object = ", currentPathObject); // the key should not be empty if (path[0] === '/') { path = path.substring(1); } currentPath = path.split('/'); pathLength = currentPath.length; currentPathRequestCount = Object.keys(currentPathObject).length; currentNode = trie.root; // adding children for the nodes in the trie for (i = 0; i < pathLength; i++) { if (!currentNode.children[currentPath[i]]) { currentNode.addChildren(currentPath[i], new Node({ name: currentPath[i], requestCount: 0, requests: [], children: {}, type: 'item-group' })); } currentNode.children[currentPath[i]].requestCount += currentPathRequestCount; currentNode = currentNode.children[currentPath[i]]; } // handling common parameters for all the methods in the current path item if (currentPathObject.hasOwnProperty('parameters')) { commonParams = currentPathObject.parameters; // console.log(' ' ,commonParams); delete currentPathObject.parameters; } // handling the server object at the path item level if (currentPathObject.hasOwnProperty('servers')) { pathLevelServers = currentPathObject.servers; delete currentPathObject.servers; if (pathLevelServers[0].hasOwnProperty('variables')) { collectionVariables.push(pathLevelServers[0].variables); } } for (method in currentPathObject) { if (currentPathObject.hasOwnProperty(method)) { operationItem = currentPathObject[method]; operationItem.parameters = operationItem.parameters || commonParams; summary = operationItem.summary || operationItem.description; // extending the parameters array for each method with the common ones // for ease of accessing currentNode.addMethod({ name: summary, // id: xthis.options.requestName === 'url' ? '' : operationItem[xthis.options.requestName], method: method, path: path, properties: operationItem, type: 'item', servers: pathLevelServers || undefined }); } } } } return { tree: trie, variables: collectionVariables }; }, /** * Converts the path variable in a more accessible format * @param {string} type - Level at the tree root/path level * @param {Array} pathVarArray - Array of path variables * @param {object} pathVariables - Object of path variables taken from the specification * @returns {Array} returns array of variables */ convertPathVariables: function(type, pathVarArray, pathVariables) { var variables = pathVarArray; // converting the base uri path variables if any if (type === 'root') { _.forOwn(pathVariables, (value, key) => { variables.push({ key: key, value: '{{' + key + '}}', description: value.enum ? value.description + 'can be only one of' + value.enum.toString() : value.description }); }); } else { _.forEach(pathVariables, (variable) => { variables.push({ key: variable.name, value: this.options.schemaFaker ? safeSchemaFaker(variable.schema || {}, this.components) : '', description: variable.description || '' }); }); } return variables; }, /** * Helper function in order to handle query string with delimiters * @param {String} paramValue - name of the query parameter * @param {String} delimiter - the delimiter which is to be used * @returns {String} returns the query string with the delimiter at appropriate points */ getQueryStringWithStyle: function(paramValue, delimiter) { var queryString, paramNameArray = Object.keys(paramValue), paramValueArray = Object.values(paramValue), queryParams = paramNameArray.map((value, index) => { if (!paramValueArray[index]) { return value; } return value + delimiter + paramValueArray[index]; }); queryString = queryParams.join(delimiter); return queryString; }, convertChildToItemGroup: function(openapi, child) { var resource = child, itemGroup, subChild, item, i, requestCount; if (resource.type === 'item-group') { if (resource.requestCount > 1) { // the resource should be a folder itemGroup = new sdk.ItemGroup({ name: resource.name // have to add auth here (first auth to be put in the tree) }); for (subChild in resource.children) { if (resource.children.hasOwnProperty(subChild)) { itemGroup.items.add( this.convertChildToItemGroup(openapi, resource.children[subChild]) ); } } for (i = 0, requestCount = resource.requests.length; i < requestCount; i++) { itemGroup.items.add( this.convertChildToItemGroup(openapi, resource.requests[i]) ); } return itemGroup; } if (resource.requests.length === 1) { return this.convertChildToItemGroup(openapi, resource.requests[0]); } for (subChild in resource.children) { if (resource.children.hasOwnProperty(subChild) && resource.children[subChild].requestCount === 1) { return this.convertChildToItemGroup(openapi, resource.children[subChild]); } } } item = this.convertRequestToItem(openapi, resource); return item; }, getAuthHelper: function(openapi, securitySet) { var securityDef, helper; if (!securitySet) { return { type: 'noauth' }; } _.forEach(securitySet, (security) => { securityDef = openapi.securityDefs[Object.keys(security)[0]]; if (securityDef.type === 'http') { helper = { type: securityDef.scheme }; } else if (securityDef.type === 'oauth2') { helper = { type: 'oauth2' }; } else if (securityDef.type === 'apiKey') { helper = { type: 'api-key', properties: securityDef }; } return false; }); return helper; }, convertToPmResponseBody: function(contentObj) { var responseBody; if (!contentObj) { return ''; } if (contentObj[APP_JSON]) { responseBody = this.convertToPmBodyData(contentObj[APP_JSON]); } else if (contentObj[APP_XML]) { responseBody = this.convertToPmBodyData(contentObj[APP_XML]); } else if (contentObj[APP_JS]) { responseBody = this.convertToPmBodyData(contentObj[APP_XML]); } else if (contentObj[TEXT_PLAIN]) { responseBody = this.convertToPmBodyData(contentObj[TEXT_PLAIN]); } return JSON.stringify(responseBody, null, 4); }, // map for creating parameters specific for a request getParametersForPathItem: function(openapi, localParams) { var tempParam, params = { query: [], header: [], path: [] }; _.forEach(localParams, (param) => { tempParam = param; // tempParam = { param: param, stage: 'local' }; // if (tempParam.$ref) { // var refArr = tempparam.$ref.split('/').slice(2); // tempParam = { param: openapi.components.parameters[refArr[1]], stage: 'global' }; // } if (tempParam.in === 'query') { params.query.push(tempParam); } else if (tempParam.in === 'header') { params.header.push(tempParam); } else if (tempParam.in === 'path') { params.path.push(tempParam); } }); return params; }, getExampleData: function(exampleObj) { var example, exampleKey; if (exampleObj) { return {}; } exampleKey = Object.keys(exampleObj)[0]; example = exampleObj[exampleKey]; return example; }, convertToPmBodyData: function(bodyObj) { var bodyData = ''; // This part is to remove format:binary from any string-type properties // will cause schemaFaker to crash if left untreated if (bodyObj.hasOwnProperty('schema')) { bodyData = this.options.schemaFaker ? safeSchemaFaker(bodyObj.schema || {}, this.components) : ''; } else if (bodyObj.hasOwnProperty('examples')) { bodyData = this.getExampleData(bodyObj.examples); // take one of the examples as the body and not all } else if (bodyObj.hasOwnProperty('example')) { bodyData = bodyObj.example; } return bodyData; }, addQueryParameters: function(requestObj, queryParameters) { var modifiedRequestObj = requestObj, modifiedUrl = requestObj.url.toString(), queryParams, paramNameArray, handleExplode = (explodeFlag, paramValue, paramName) => { if (explodeFlag) { paramNameArray = Array.from(Array(paramValue.length), () => { return paramName; }); queryParams = paramNameArray.map((value, index) => { return (value + '=' + paramValue[index]); }); modifiedUrl += queryParams.join('&'); } else { modifiedUrl += paramName + '=' + paramValue.join(','); } return modifiedUrl; }, paramValue; if (queryParameters === []) { return requestObj; } modifiedUrl += '?'; // need to set name based on param stage // if (param.stage === 'global') { // this.enviVariables.push({ // key: 'url', // value: url, // enabled: true, // type: 'text' // }); // } _.forEach(queryParameters, (param) => { // check for existence of schema if (param.hasOwnProperty('schema')) { // fake data generated paramValue = this.options.schemaFaker ? safeSchemaFaker(param.schema, this.components) : ''; paramType = param.schema.type; // checking the type of the query parameter if (paramType === 'array') { // which style is there ? if (param.style === 'form') { // check for the truthness of explode modifiedUrl = handleExplode(param.explode, paramValue, param.name); } else if (param.style === 'spaceDelimited') { // explode parameter doesn't really affect anything here modifiedUrl += param.name + '=' + paramValue.join('%20'); } else if (param.style === 'pipeDelimited') { modifiedUrl += param.name + '=' + paramValue.join('|'); } else if (param.style === 'deepObject') { modifiedUrl += _.map(paramValue, (pv) => { return (param.name + '[]=' + pv); }).join('&'); } else { // if there is not style parameter we assume that it will be form by default; modifiedUrl += param.name + '=' + paramValue.join(','); } } else if (paramType === 'object') { // following similar checks as above if (param.hasOwnProperty('style')) { if (param.style === 'form') { if (param.explode) { // if explode is true paramNameArray = Object.keys(paramValue); queryParams = paramNameArray.map((value, index) => { return (value + '=' + Object.values(paramValue)[index]); }); modifiedUrl += queryParams.join('&'); } else { modifiedUrl += this.getQueryStringWithStyle(paramValue, ','); } } else if (param.style === 'spaceDelimited') { modifiedUrl += this.getQueryStringWithStyle(paramValue, '%20'); } else if (param.style === 'pipeDelimited') { modifiedUrl += this.getQueryStringWithStyle(paramValue, '|'); } else if (param.style === 'deepObject') { _.forOwn(paramValue, (value, key) => { modifiedUrl += param.name + '[' + key + ']=' + value + '&'; }); modifiedUrl = modifiedUrl.slice(0, -1); } } else { modifiedUrl += param.name + '=' + (paramValue); } } else { modifiedUrl += param.name + '=' + (paramValue); } } else { // since no schema present add the parameter with no value modifiedUrl += param.name + '='; } modifiedUrl += '&'; }); modifiedUrl = modifiedUrl.slice(0, -1); // updating the request url modifiedRequestObj.url = new sdk.Url(modifiedUrl); return modifiedRequestObj; }, convertToPmHeader: function(header) { var fakeData, reqHeader; if (header.hasOwnProperty('schema')) { fakeData = this.options.schemaFaker ? safeSchemaFaker(header.schema || {}, this.components) : ''; } else { fakeData = ''; } reqHeader = new sdk.Header({ key: header.name, value: fakeData }); reqHeader.description = header.description; return reqHeader; }, convertToPmBody: function(requestBody) { var contentObj, // content is required bodyData, param, paramArray = [], updateOptions = {}, reqBody = new sdk.RequestBody(), contentHeader, rDataMode; if (!requestBody) { return postmanItemObj; } // how do I support multiple content types contentObj = requestBody.content; // handling for the urlencoded media type if (contentObj.hasOwnProperty(URLENCODED)) { rDataMode = 'urlencoded'; bodyData = this.convertToPmBodyData(contentObj[URLENCODED]); // create query parameters and add it to the request body object _.forOwn(bodyData, (value, key) => { if (typeof value === 'object') { value = JSON.stringify(value); } param = new sdk.QueryParam({ key: key, value: value }); paramArray.push(param); }); updateOptions = { mode: rDataMode, urlencoded: paramArray }; // add a content type header for each media type for the request body contentHeader = new sdk.Header({ key: 'Content-Type', value: URLENCODED }); // update the request body with the options reqBody.update(updateOptions); } else if (contentObj.hasOwnProperty(FORM_DATA)) { rDataMode = 'formdata'; bodyData = this.convertToPmBodyData(contentObj[FORM_DATA]); // create the form parameters and add it to the request body object _.forOwn(bodyData, (value, key) => { if (typeof value === 'object') { value = JSON.stringify(value); } param = new sdk.FormParam({ key: key, value: value }); paramArray.push(param); }); updateOptions = { mode: rDataMode, formdata: paramArray }; // add a content type header for the pertaining media type contentHeader = new sdk.Header({ key: 'Content-Type', value: FORM_DATA }); // update the request body reqBody.update(updateOptions); } else { rDataMode = 'raw'; let bodyType; // checking for all possible raw types if (contentObj.hasOwnProperty(APP_JS)) { bodyType = APP_JS; } else if (contentObj.hasOwnProperty(APP_JSON)) { bodyType = APP_JSON; } else if (contentObj.hasOwnProperty(TEXT_HTML)) { bodyType = TEXT_HTML; } else if (contentObj.hasOwnProperty(TEXT_PLAIN)) { bodyType = TEXT_PLAIN; } else if (contentObj.hasOwnProperty(TEXT_XML)) { bodyType = TEXT_XML; } bodyData = this.convertToPmBodyData(contentObj[bodyType]); updateOptions = { mode: rDataMode, raw: JSON.stringify(bodyData, null, 4) }; contentHeader = new sdk.Header({ key: 'Content-Type', value: bodyType }); reqBody.update(updateOptions); } return { body: reqBody, contentHeader: contentHeader }; }, convertToPmResponse: function(response, code) { var responseHeaders = [], header; _.forOwn(response.headers, (value, key) => { if (key !== 'Content-Type') { header = value; header.name = key; responseHeaders.push(this.convertToPmHeader(header)); } }); if (response.content && Object.keys(response.content).length > 0) { responseHeaders.push({ name: 'Content-Type', value: Object.keys(response.content)[0] }); } else { responseHeaders.push({ name: 'Content-Type', value: TEXT_PLAIN }); } return new sdk.Response({ name: response.description, code: code === 'default' ? 500 : Number(code), header: responseHeaders, body: this.convertToPmResponseBody(response.content) }); }, // function to convert an openapi path item to postman item convertRequestToItem: function(openapi, operationItem) { var reqName, // itemName = operationItem.id, pathVariables = openapi.baseUrlVariables, operation = operationItem.properties, reqBody = operationItem.properties.requestBody, itemParams = operationItem.properties.parameters, reqParams = this.getParametersForPathItem(openapi, itemParams), baseUrl = openapi.baseUrl, pathVarArray, authHelper, item, serverObj, displayUrl, reqUrl = '/' + operationItem.path, pmBody; // handling path templating in request url if any reqUrl = reqUrl.replace(/{/g, ':').replace(/}/g, ''); // accounting for the overriding of the root level servers object if present at the path level if (operationItem.hasOwnProperty('servers') && operationItem.servers) { serverObj = operationItem.servers[0]; baseUrl = serverObj.url.replace(/{/g, ':').replace(/}/g, ''); pathVariables = serverObj.variables; } else { baseUrl += reqUrl; if (pathVariables) { displayUrl = baseUrl; } else { displayUrl = '{{baseUrl}}' + reqUrl; } } pathVarArray = this.convertPathVariables('root', [], pathVariables); reqName = this.options.requestName === 'fallback' ? operation.summary || operation.operationId || operation.description || (displayUrl || baseUrl) : ( this.options.requestName === 'url' ? (displayUrl || baseUrl) : operation[this.options.requestName]); if (!reqName) { throw new Error(`requestName (${this.options.requestName})` + ` in options is invalid or property does not exist in ${operationItem.path}`); } // handling authentication here (for http type only) authHelper = this.getAuthHelper(openapi, operation.security); // creating the request object item = new sdk.Item({ name: reqName, request: { description: operation.description, url: displayUrl || baseUrl, name: reqName, method: operationItem.method.toUpperCase() } }); // using the auth helper if (authHelper.type === 'api-key') { if (authHelper.properties.in === 'header') { item.request.addHeader(this.convertToPmHeader(authHelper.properties)); item.request.auth = { type: 'noauth' }; } else if (authHelper.properties.in === 'query') { item.request = this.addQueryParameters(item.request, [authHelper.properties]); item.request.auth = { type: 'noauth' }; } } else { item.request.auth = authHelper; } // console.log("path params = ", reqParams.path, pathVarArray) item.request = this.addQueryParameters(item.request, reqParams.query); item.request.url.variables = this.convertPathVariables('method', pathVarArray, reqParams.path); // adding headers to request from reqParam _.forEach(reqParams.header, (header) => { item.request.addHeader(this.convertToPmHeader(header)); }); // adding request body and content type header if (reqBody) { pmBody = this.convertToPmBody(reqBody); item.request.body = pmBody.body; item.request.addHeader(pmBody.contentHeader); } // adding responses to request item if (operation.responses) { _.forOwn(operation.responses, (response, code) => { item.responses.add(this.convertToPmResponse(response, code)); }); } return item; } };