/** * @fileOverview * * This module consists all request body transformer functions based on the request body modes supported * Ideally, this should one day move to a function in SDK something like request.getNodeRequestOptions() * * * _ * ( ) ,,,,, * \\ . . , * \\ | - D , * (._) \__- | , * | |.. * \\|_ , ,---- _ |----. * \__ ( ( / ) _ * | \/ \. ' _.| \ ( ) * | \ /( / /\_ \ // * \ / ( / / ) // * ( , / / , (_.) * |......\ | \, * / / ) \--- * /___/___^// */ var _ = require('lodash'), CONTENT_TYPE_HEADER_KEY = 'Content-Type', /** * Map content-type to respective body language. * * @private * @type {Object} */ CONTENT_TYPE_LANGUAGE = { 'html': 'text/html', 'text': 'text/plain', 'json': 'application/json', 'javascript': 'application/javascript', 'xml': 'application/xml' }, STRING = 'string', E = '', oneNormalizedHeader, // the following two are reducer functions. we keep it defined here to avoid redefinition upon each parse urlEncodedBodyReducer, formDataBodyReducer; /** * Find the enabled header with the given name. * * @todo Add this helper in Collection SDK. * * @private * @param {HeaderList} headers * @param {String} name * @returns {Header|undefined} */ oneNormalizedHeader = function oneNormalizedHeader (headers, name) { var i, header; // get all headers with `name` headers = headers.reference[name.toLowerCase()]; if (Array.isArray(headers)) { // traverse the headers list in reverse direction in order to find the last enabled for (i = headers.length - 1; i >= 0; i--) { header = headers[i]; if (header && !header.disabled) { return header; } } // bail out if no enabled header was found return; } // return the single enabled header if (headers && !headers.disabled) { return headers; } }; /** * Reduces postman SDK url encoded form definition (flattened to array) into Node compatible body options * * @param {Object} form - url encoded form params accumulator * @param {Object} param - url encoded form param * * @returns {Object} */ urlEncodedBodyReducer = function (form, param) { if (!param || param.disabled) { return form; } var key = param.key, value = param.value; // add the parameter to the form while accounting for duplicate values if (!form.hasOwnProperty(key)) { form[key] = value; return form; } // at this point, we know that form has duplicate, so we need to accumulate it in an array if (!Array.isArray(form[key])) { form[key] = [form[key]]; } form[key].push(value); // finally push the duplicate and return return form; }; /** * Reduces postman SDK multi-part form definition (flattened to array) into Node compatible body options * * @param {Array} data - multi-part form params accumulator * @param {Object} param - multi-part form param * * @returns {Array} */ formDataBodyReducer = function (data, param) { if (!param || param.disabled) { return data; } var formParam = { key: param.key, value: param.value }, options; // we keep the default blank and then set to object wherever needed. saves doing object keyLength // make sure that value is either string or read stream otherwise it'll cause error in postman-request if (param.type !== 'file' && typeof formParam.value !== STRING) { try { formParam.value = JSON.stringify(formParam.value); } catch (err) { formParam.value = E; } } // make sure `filename` param is sent for every file without `value` // so that `filename=""` is added to content-disposition header in form data if (param.type === 'file' && !formParam.value && typeof param.fileName !== 'string') { param.fileName = E; formParam.value = E; // make sure value is not null/undefined ever } // if data has a truthy content type, we mutate the value to take the options. we are assuming that // blank string will not be considered as an accepted content type. if (param.contentType && typeof param.contentType === STRING) { (options || (options = {})).contentType = param.contentType; } // additionally parse the file name and length if sent // @note: Add support for fileName & fileLength option in Schema & SDK. // The filepath property overrides filename and may contain a relative path. if (typeof param.fileName === STRING) { (options || (options = {})).filename = param.fileName; } if (typeof param.fileLength === 'number') { (options || (options = {})).knownLength = param.fileLength; } // if options were set, add them to formParam options && (formParam.options = options); data.push(formParam); return data; }; /** * This module exposes functions that are named corresponding to Postman collection body modes. It accepts the body * definition, usually like `request.body.raw` where mode is `raw` and returns its equivalent structure that needs to be * sent to node request module */ module.exports = { /** * @param {Object} content - request body content * @param {Request} [request] - request object * @returns {Object} */ raw: function (content, request) { var contentLanguage = _.get(request, 'body.options.raw.language', 'text'); // Add `Content-Type` header from body options if not set already if (request && !oneNormalizedHeader(request.headers, CONTENT_TYPE_HEADER_KEY)) { request.headers.add({ key: CONTENT_TYPE_HEADER_KEY, value: CONTENT_TYPE_LANGUAGE[contentLanguage] || CONTENT_TYPE_LANGUAGE.text, system: true }); } if (typeof content !== STRING) { content = JSON.stringify(content); } return { body: content }; }, /** * @param {Object} content - request body content * @returns {Object} */ urlencoded: function (content) { if (content && _.isFunction(content.all)) { content = content.all(); } // flatten the body content return { form: _.reduce(content, urlEncodedBodyReducer, {}) }; }, /** * @param {Object} content - request body content * @returns {Object} */ formdata: function (content) { if (content && _.isFunction(content.all)) { content = content.all(); } // flatten the body content return { formData: _.reduce(content, formDataBodyReducer, []) }; }, /** * @param {Object} content - request body content * @returns {Object} */ file: function (content) { return { body: content && content.content }; }, /** * @param {Object} content - request body content * @param {Request} [request] - Request object * @returns {Object} */ graphql: function (content, request) { var body; // implicitly add `Content-Type` header if not set already if (request && !oneNormalizedHeader(request.headers, CONTENT_TYPE_HEADER_KEY)) { request.headers.add({ key: CONTENT_TYPE_HEADER_KEY, value: CONTENT_TYPE_LANGUAGE.json, system: true }); } // if `variables` is an object, just stringify the entire content if (content && typeof content.variables !== STRING) { // if any property of graphql is undefined, it will not get stringified // as a result, if no content object's properties are present then the // result will be a blank object being sent. // note that this behavior has to be imitated later when we are // receiving variables as string return { body: JSON.stringify({ query: content.query, operationName: content.operationName, variables: content.variables }) }; } // otherwise, traverse the graphql properties and generate the // stringified content. This avoids parsing the variables. body = []; if (content.hasOwnProperty('query') && (typeof content.query === STRING)) { body.push('"query":' + JSON.stringify(content.query)); } if (content.hasOwnProperty('operationName') && (typeof content.operationName === STRING)) { body.push('"operationName":' + JSON.stringify(content.operationName)); } if (content.hasOwnProperty('variables') && (typeof content.variables === STRING) && // even though users are free to send even malformed json string, the case of empty string has to be // specially disallowed since in most default cases if a text editor is used to accept this data, it will // send a blank string for an empty text-editor state and that would be an error flow. That implies majority // default use case will become error flow and handling for the same has to be also coded in every other // place where runtime is used. (content.variables !== E)) { body.push('"variables":' + content.variables); // already a stringified JSON } return { body: '{' + body.join(',') + '}' // note that [] body = {} ¯\_(ツ)_/¯ }; } };