diff --git a/README.md b/README.md index 8658935b04..94511703c1 100755 --- a/README.md +++ b/README.md @@ -901,7 +901,56 @@ axios.post('https://httpbin.org/post', {x: 1, buf: new Buffer(10)}, { Axios FormData serializer supports some special endings to perform the following operations: - `{}` - serialize the value with JSON.stringify -- `[]` - unwrap the array like object as separate fields with the same key +- `[]` - unwrap the array-like object as separate fields with the same key + +> NOTE: +> unwrap/expand operation will be used by default on array-like objects + +FormData serializer supports additional options via `config.formSerializer: object` property to handle rare cases: + +- `visitor: Function` - user-defined visitor function that will be called recursively to serialize the data object +to a `FormData` object by following custom rules. + +- `dots: boolean = false` - use dot notation instead of brackets to serialize arrays and objects; + +- `metaTokens: boolean = true` - add the special ending (e.g `user{}: '{"name": "John"}'`) in the FormData key. +The back-end body-parser could potentially use this meta-information to automatically parse the value as JSON. + +- `indexes: null|false|true = false` - controls how indexes will be added to unwrapped keys of `flat` array-like objects + + - `null` - don't add brackets (`arr: 1`, `arr: 2`, `arr: 3`) + - `false`(default) - add empty brackets (`arr[]: 1`, `arr[]: 2`, `arr[]: 3`) + - `true` - add brackets with indexes (`arr[0]: 1`, `arr[1]: 2`, `arr[2]: 3`) + +Let's say we have an object like this one: + +```js +const obj = { + x: 1, + arr: [1, 2, 3], + arr2: [1, [2], 3], + users: [{name: 'Peter', surname: 'Griffin'}, {name: 'Thomas', surname: 'Anderson'}], + 'obj2{}': [{x:1}] +}; +``` + +The following steps will be executed by the Axios serializer internally: + +```js +const formData= new FormData(); +formData.append('x', '1'); +formData.append('arr[]', '1'); +formData.append('arr[]', '2'); +formData.append('arr[]', '3'); +formData.append('arr2[0]', '1'); +formData.append('arr2[1][0]', '2'); +formData.append('arr2[2]', '3'); +formData.append('users[0][name]', 'Peter'); +formData.append('users[0][surname]', 'Griffin'); +formData.append('users[1][name]', 'Thomas'); +formData.append('users[1][surname]', 'Anderson'); +formData.append('obj2{}', '[{"x":1}]'); +``` ```js const axios= require('axios'); @@ -917,9 +966,9 @@ axios.post('https://httpbin.org/post', { ``` Axios supports the following shortcut methods: `postForm`, `putForm`, `patchForm` -which are just the corresponding http methods with a header preset: `Content-Type`: `multipart/form-data`. +which are just the corresponding http methods with the content-type header preset to `multipart/form-data`. -FileList object can be passed directly: +`FileList` object can be passed directly: ```js await axios.postForm('https://httpbin.org/post', document.querySelector('#fileInput').files) diff --git a/index.d.ts b/index.d.ts index fd9cd922cc..c363c4e905 100644 --- a/index.d.ts +++ b/index.d.ts @@ -79,6 +79,23 @@ export interface GenericAbortSignal { removeEventListener: (...args: any) => any; } +export interface FormDataVisitorHelpers { + defaultVisitor: FormDataVisitor; + convertValue: (value: any) => any; + isVisitable: (value: any) => boolean; +} + +export interface FormDataVisitor { + (value: any, key: string | number, path: null | Array, helpers: FormDataVisitorHelpers): boolean; +} + +export interface FormSerializerOptions { + visitor?: FormDataVisitor; + dots?: boolean; + metaTokens?: boolean; + indexes?: boolean; +} + export interface AxiosRequestConfig { url?: string; method?: Method | string; @@ -117,6 +134,7 @@ export interface AxiosRequestConfig { env?: { FormData?: new (...args: any[]) => object; }; + formSerializer?: FormSerializerOptions; } export interface HeadersDefaults { @@ -268,7 +286,7 @@ export interface AxiosStatic extends AxiosInstance { all(values: Array>): Promise; spread(callback: (...args: T[]) => R): (array: T[]) => R; isAxiosError(payload: any): payload is AxiosError; - toFormData(sourceObj: object, targetFormData?: GenericFormData): GenericFormData; + toFormData(sourceObj: object, targetFormData?: GenericFormData, options?: FormSerializerOptions): GenericFormData; } declare const axios: AxiosStatic; diff --git a/lib/defaults/index.js b/lib/defaults/index.js index 91998186c3..1be97ffd22 100644 --- a/lib/defaults/index.js +++ b/lib/defaults/index.js @@ -77,7 +77,11 @@ var defaults = { if ((isFileList = utils.isFileList(data)) || (isObjectPayload && contentType === 'multipart/form-data')) { var _FormData = this.env && this.env.FormData; - return toFormData(isFileList ? {'files[]': data} : data, _FormData && new _FormData()); + return toFormData( + isFileList ? {'files[]': data} : data, + _FormData && new _FormData(), + this.formSerializer + ); } else if (isObjectPayload || contentType === 'application/json') { setContentTypeIfUnset(headers, 'application/json'); return stringifySafely(data); diff --git a/lib/helpers/toFormData.js b/lib/helpers/toFormData.js index 5e3cc0f631..9cfdbd76a6 100644 --- a/lib/helpers/toFormData.js +++ b/lib/helpers/toFormData.js @@ -1,19 +1,68 @@ 'use strict'; var utils = require('../utils'); +var envFormData = require('../defaults/env/FormData'); + +function isVisitable(thing) { + return utils.isPlainObject(thing) || utils.isArray(thing); +} + +function removeBrackets(key) { + return utils.endsWith(key, '[]') ? key.slice(0, -2) : key; +} + +function renderKey(path, key, dots) { + if (!path) return key; + return path.concat(key).map(function each(token, i) { + // eslint-disable-next-line no-param-reassign + token = removeBrackets(token); + return !dots && i ? '[' + token + ']' : token; + }).join(dots ? '.' : ''); +} + +function isFlatArray(arr) { + return utils.isArray(arr) && !arr.some(isVisitable); +} + +var predicates = utils.toFlatObject(utils, {}, null, function filter(prop) { + return /^is[A-Z]/.test(prop); +}); /** * Convert a data object to FormData * @param {Object} obj * @param {?Object} [formData] + * @param {?Object} [options] + * @param {Function} [options.visitor] + * @param {Boolean} [options.metaTokens = true] + * @param {Boolean} [options.dots = false] + * @param {?Boolean} [options.indexes = false] * @returns {Object} **/ -function toFormData(obj, formData) { +function toFormData(obj, formData, options) { // eslint-disable-next-line no-param-reassign - formData = formData || new FormData(); + formData = formData || new (envFormData || FormData)(); - var stack = []; + // eslint-disable-next-line no-param-reassign + options = utils.toFlatObject(options, { + metaTokens: true, + dots: false, + indexes: false + }, false, function defined(option, source) { + // eslint-disable-next-line no-eq-null,eqeqeq + return !utils.isUndefined(source[option]); + }); + + var metaTokens = options.metaTokens; + // eslint-disable-next-line no-use-before-define + var visitor = options.visitor || defaultVisitor; + var dots = options.dots; + var indexes = options.indexes; + + if (!utils.isFunction(visitor)) { + throw new TypeError('visitor must be a function'); + } function convertValue(value) { if (value === null) return ''; @@ -29,39 +78,80 @@ function toFormData(obj, formData) { return value; } - function build(data, parentKey) { - if (utils.isPlainObject(data) || utils.isArray(data)) { - if (stack.indexOf(data) !== -1) { - throw Error('Circular reference detected in ' + parentKey); + + /** + * + * @param {*} value + * @param {String|Number} key + * @param {Array} path + * @this {FormData} + * @returns {boolean} return true to visit the each prop of the value recursively + */ + function defaultVisitor(value, key, path) { + var arr; + + if (value && !path && typeof value === 'object') { + if (utils.endsWith(key, '{}')) { + // eslint-disable-next-line no-param-reassign + key = metaTokens ? key : key.slice(0, -2); + // eslint-disable-next-line no-param-reassign + value = JSON.stringify(value); + } else if (!utils.isPlainObject(value) && (arr = utils.toArray(value)) && isFlatArray(arr)) { + // eslint-disable-next-line no-param-reassign + key = removeBrackets(key); + + arr.forEach(function each(el, index) { + !utils.isUndefined(el) && formData.append( + // eslint-disable-next-line no-nested-ternary + indexes === true ? renderKey([key], index, dots) : (indexes === null ? key : key + '[]'), + convertValue(el) + ); + }); + return false; } + } + + if (isVisitable(value)) { + return true; + } + + formData.append(renderKey(path, key, dots), convertValue(value)); + + return false; + } + + var stack = []; + + var exposedHelpers = Object.assign(predicates, { + defaultVisitor: defaultVisitor, + convertValue: convertValue, + isVisitable: isVisitable + }); + + function build(value, path) { + if (utils.isUndefined(value)) return; - stack.push(data); - - utils.forEach(data, function each(value, key) { - if (utils.isUndefined(value)) return; - var fullKey = parentKey ? parentKey + '.' + key : key; - var arr; - - if (value && !parentKey && typeof value === 'object') { - if (utils.endsWith(key, '{}')) { - // eslint-disable-next-line no-param-reassign - value = JSON.stringify(value); - } else if (utils.endsWith(key, '[]') && (arr = utils.toArray(value))) { - // eslint-disable-next-line func-names - arr.forEach(function(el) { - !utils.isUndefined(el) && formData.append(fullKey, convertValue(el)); - }); - return; - } - } - - build(value, fullKey); - }); - - stack.pop(); - } else { - formData.append(parentKey, convertValue(data)); + if (stack.indexOf(value) !== -1) { + throw Error('Circular reference detected in ' + path.join('.')); } + + stack.push(value); + + utils.forEach(value, function each(el, key) { + var result = !utils.isUndefined(el) && defaultVisitor.call( + formData, el, utils.isString(key) ? key.trim() : key, path, exposedHelpers + ); + + if (result === true) { + build(el, path ? path.concat(key) : [key]); + } + }); + + stack.pop(); + } + + if (!utils.isPlainObject(obj)) { + throw new TypeError('data must be a plain object'); } build(obj); diff --git a/lib/utils.js b/lib/utils.js index c43710e196..1ecd51ee09 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -366,29 +366,32 @@ function inherits(constructor, superConstructor, props, descriptors) { * Resolve object with deep prototype chain to a flat object * @param {Object} sourceObj source object * @param {Object} [destObj] - * @param {Function} [filter] + * @param {Function|Boolean} [filter] + * @param {Function} [propFilter] * @returns {Object} */ -function toFlatObject(sourceObj, destObj, filter) { +function toFlatObject(sourceObj, destObj, filter, propFilter) { var props; var i; var prop; var merged = {}; destObj = destObj || {}; + // eslint-disable-next-line no-eq-null,eqeqeq + if (sourceObj == null) return destObj; do { props = Object.getOwnPropertyNames(sourceObj); i = props.length; while (i-- > 0) { prop = props[i]; - if (!merged[prop]) { + if ((!propFilter || propFilter(prop, sourceObj, destObj)) && !merged[prop]) { destObj[prop] = sourceObj[prop]; merged[prop] = true; } } - sourceObj = Object.getPrototypeOf(sourceObj); + sourceObj = filter !== false && Object.getPrototypeOf(sourceObj); } while (sourceObj && (!filter || filter(sourceObj, destObj)) && sourceObj !== Object.prototype); return destObj; @@ -413,14 +416,15 @@ function endsWith(str, searchString, position) { /** - * Returns new array from array like object + * Returns new array from array like object or null if failed * @param {*} [thing] - * @returns {Array} + * @returns {?Array} */ function toArray(thing) { if (!thing) return null; + if (isArray(thing)) return thing; var i = thing.length; - if (isUndefined(i)) return null; + if (!isNumber(i)) return null; var arr = new Array(i); while (i-- > 0) { arr[i] = thing[i]; diff --git a/package-lock.json b/package-lock.json index 5e9797adf9..e537d68521 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "coveralls": "^3.1.1", "dtslint": "^4.2.1", "es6-promise": "^4.2.8", + "express": "^4.18.1", "formidable": "^2.0.1", "grunt": "^1.4.1", "grunt-banner": "^0.6.0", @@ -48,6 +49,7 @@ "load-grunt-tasks": "^5.1.0", "minimist": "^1.2.6", "mocha": "^8.2.1", + "multer": "^1.4.4", "rollup": "^2.67.0", "rollup-plugin-terser": "^7.0.2", "sinon": "^4.5.0", @@ -1565,6 +1567,12 @@ "node": ">= 8" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=", + "dev": true + }, "node_modules/aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", @@ -2753,6 +2761,37 @@ "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=", "dev": true }, + "node_modules/busboy": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", + "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", + "dev": true, + "dependencies": { + "dicer": "0.2.5", + "readable-stream": "1.1.x" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/busboy/node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/busboy/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -4485,6 +4524,37 @@ "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=", "dev": true }, + "node_modules/dicer": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", + "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", + "dev": true, + "dependencies": { + "readable-stream": "1.1.x", + "streamsearch": "0.1.2" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/dicer/node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/dicer/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, "node_modules/diff": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", @@ -5707,9 +5777,9 @@ } }, "node_modules/express": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.0.tgz", - "integrity": "sha512-EJEXxiTQJS3lIPrU1AE2vRuT7X7E+0KBbpm5GSoK524yl0K8X+er8zS2P14E64eqsVNoWbMCT7MpmQ+ErAhgRg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", + "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", "dev": true, "dependencies": { "accepts": "~1.3.8", @@ -10461,6 +10531,76 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "node_modules/multer": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4.tgz", + "integrity": "sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==", + "dev": true, + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^0.2.11", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "on-finished": "^2.3.0", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/multer/node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/multer/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "node_modules/multer/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/multer/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/multer/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/multicast-dns": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", @@ -13995,6 +14135,15 @@ "node": ">= 10.0.0" } }, + "node_modules/streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/strict-uri-encode": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", @@ -18175,6 +18324,12 @@ "picomatch": "^2.0.4" } }, + "append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=", + "dev": true + }, "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", @@ -19182,6 +19337,36 @@ "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=", "dev": true }, + "busboy": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", + "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", + "dev": true, + "requires": { + "dicer": "0.2.5", + "readable-stream": "1.1.x" + }, + "dependencies": { + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, "bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -20602,6 +20787,36 @@ "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=", "dev": true }, + "dicer": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", + "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", + "dev": true, + "requires": { + "readable-stream": "1.1.x", + "streamsearch": "0.1.2" + }, + "dependencies": { + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, "diff": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", @@ -21619,9 +21834,9 @@ } }, "express": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.0.tgz", - "integrity": "sha512-EJEXxiTQJS3lIPrU1AE2vRuT7X7E+0KBbpm5GSoK524yl0K8X+er8zS2P14E64eqsVNoWbMCT7MpmQ+ErAhgRg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", + "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", "dev": true, "requires": { "accepts": "~1.3.8", @@ -25397,6 +25612,72 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "multer": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4.tgz", + "integrity": "sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==", + "dev": true, + "requires": { + "append-field": "^1.0.0", + "busboy": "^0.2.11", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "on-finished": "^2.3.0", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "dependencies": { + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "multicast-dns": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", @@ -28306,6 +28587,12 @@ } } }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=", + "dev": true + }, "strict-uri-encode": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", diff --git a/package.json b/package.json index 5be6f78059..c262de8feb 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "coveralls": "^3.1.1", "dtslint": "^4.2.1", "es6-promise": "^4.2.8", + "express": "^4.18.1", "formidable": "^2.0.1", "grunt": "^1.4.1", "grunt-banner": "^0.6.0", @@ -66,6 +67,7 @@ "load-grunt-tasks": "^5.1.0", "minimist": "^1.2.6", "mocha": "^8.2.1", + "multer": "^1.4.4", "rollup": "^2.67.0", "rollup-plugin-terser": "^7.0.2", "sinon": "^4.5.0", diff --git a/test/specs/helpers/toFormData.spec.js b/test/specs/helpers/toFormData.spec.js index d833f2e4eb..523ad1bb94 100644 --- a/test/specs/helpers/toFormData.spec.js +++ b/test/specs/helpers/toFormData.spec.js @@ -1,6 +1,89 @@ var toFormData = require('../../../lib/helpers/toFormData'); describe('toFormData', function () { + it('should convert nested data object to FormData with dots option enabled', function () { + var o = { + val: 123, + nested: { + arr: ['hello', 'world'] + } + }; + + var form = toFormData(o, null, {dots: true}); + expect(form instanceof FormData).toEqual(true); + expect(Array.from(form.keys()).length).toEqual(3); + expect(form.get('val')).toEqual('123'); + expect(form.get('nested.arr.0')).toEqual('hello'); + }); + + it('should respect metaTokens option', function () { + var data = { + 'obj{}': {x: 1, y: 2} + }; + + var str = JSON.stringify(data['obj{}']); + + var form = toFormData(data, null, {metaTokens: false}); + + expect(Array.from(form.keys()).length).toEqual(1); + expect(form.getAll('obj')).toEqual([str]); + }); + + describe('Flat arrays serialization', function () { + it('should include full indexes when the `indexes` option is set to true', function () { + var data = { + arr: [1, 2, 3], + arr2: [1, [2], 3] + }; + + var form = toFormData(data, null, {indexes: true}); + + expect(Array.from(form.keys()).length).toEqual(6); + + expect(form.get('arr[0]')).toEqual('1'); + expect(form.get('arr[1]')).toEqual('2'); + expect(form.get('arr[2]')).toEqual('3'); + + expect(form.get('arr2[0]')).toEqual('1'); + expect(form.get('arr2[1][0]')).toEqual('2'); + expect(form.get('arr2[2]')).toEqual('3'); + }); + + it('should include brackets only when the `indexes` option is set to false', function () { + var data = { + arr: [1, 2, 3], + arr2: [1, [2], 3] + }; + + var form = toFormData(data, null, {indexes: false}); + + expect(Array.from(form.keys()).length).toEqual(6); + + expect(form.getAll('arr[]')).toEqual(['1', '2', '3']); + + expect(form.get('arr2[0]')).toEqual('1'); + expect(form.get('arr2[1][0]')).toEqual('2'); + expect(form.get('arr2[2]')).toEqual('3'); + }); + + it('should omit brackets when the `indexes` option is set to null', function () { + var data = { + arr: [1, 2, 3], + arr2: [1, [2], 3] + }; + + var form = toFormData(data, null, {indexes: null}); + + expect(Array.from(form.keys()).length).toEqual(6); + + expect(form.getAll('arr')).toEqual(['1', '2', '3']); + + expect(form.get('arr2[0]')).toEqual('1'); + expect(form.get('arr2[1][0]')).toEqual('2'); + expect(form.get('arr2[2]')).toEqual('3'); + }); + }); + it('should convert nested data object to FormData', function () { var o = { val: 123, @@ -13,7 +96,7 @@ describe('toFormData', function () { expect(form instanceof FormData).toEqual(true); expect(Array.from(form.keys()).length).toEqual(3); expect(form.get('val')).toEqual('123'); - expect(form.get('nested.arr.0')).toEqual('hello'); + expect(form.get('nested[arr][0]')).toEqual('hello'); }); it('should append value whose key ends with [] as separate values with the same key', function () { diff --git a/test/unit/adapters/http.js b/test/unit/adapters/http.js index 5264f63362..c92e0afd2d 100644 --- a/test/unit/adapters/http.js +++ b/test/unit/adapters/http.js @@ -12,6 +12,8 @@ var server, proxy; var AxiosError = require('../../../lib/core/AxiosError'); var FormData = require('form-data'); var formidable = require('formidable'); +const express = require('express'); +const multer = require('multer'); describe('supports http with nodejs', function () { @@ -1244,44 +1246,82 @@ describe('supports http with nodejs', function () { }); }); - it('should allow passing FormData', function (done) { - var form = new FormData(); - var file1= Buffer.from('foo', 'utf8'); + describe('FormData', function () { + it('should allow passing FormData', function (done) { + var form = new FormData(); + var file1 = Buffer.from('foo', 'utf8'); - form.append('foo', "bar"); - form.append('file1', file1, { - filename: 'bar.jpg', - filepath: 'temp/bar.jpg', - contentType: 'image/jpeg' - }); + form.append('foo', "bar"); + form.append('file1', file1, { + filename: 'bar.jpg', + filepath: 'temp/bar.jpg', + contentType: 'image/jpeg' + }); - server = http.createServer(function (req, res) { - var receivedForm = new formidable.IncomingForm(); + server = http.createServer(function (req, res) { + var receivedForm = new formidable.IncomingForm(); - receivedForm.parse(req, function (err, fields, files) { - if (err) { - return done(err); - } + receivedForm.parse(req, function (err, fields, files) { + if (err) { + return done(err); + } + + res.end(JSON.stringify({ + fields: fields, + files: files + })); + }); + }).listen(4444, function () { + axios.post('http://localhost:4444/', form, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }).then(function (res) { + assert.deepStrictEqual(res.data.fields, {foo: 'bar'}); + + assert.strictEqual(res.data.files.file1.mimetype, 'image/jpeg'); + assert.strictEqual(res.data.files.file1.originalFilename, 'temp/bar.jpg'); + assert.strictEqual(res.data.files.file1.size, 3); - res.end(JSON.stringify({ - fields: fields, - files: files - })); + done(); + }).catch(done); }); - }).listen(4444, function () { - axios.post('http://localhost:4444/', form, { - headers: { - 'Content-Type': 'multipart/form-data' - } - }).then(function (res) { - assert.deepStrictEqual(res.data.fields,{foo: 'bar'}); + }); + describe('toFormData helper', function () { + it('should properly serialize nested objects for parsing with multer.js (express.js)', function (done) { + const app = express(); + var obj = { + arr1: ['1', '2', '3'], + arr2: ['1', ['2'], '3'], + obj: {x: '1', y: {z: '1'}}, + users: [{name: 'Peter', surname: 'griffin'}, {name: 'Thomas', surname: 'Anderson'}] + }; - assert.strictEqual(res.data.files.file1.mimetype,'image/jpeg'); - assert.strictEqual(res.data.files.file1.originalFilename,'temp/bar.jpg'); - assert.strictEqual(res.data.files.file1.size,3); + app.post('/', multer().none(), function (req, res, next) { + res.send(JSON.stringify(req.body)); + }); - done(); - }).catch(done); + server = app.listen(3001, function () { + // multer can parse the following key/value pairs to an array (indexes: null, false, true): + // arr: '1' + // arr: '2' + // ------------- + // arr[]: '1' + // arr[]: '2' + // ------------- + // arr[0]: '1' + // arr[1]: '2' + // ------------- + Promise.all([null, false, true].map(function (mode) { + return axios.postForm('http://localhost:3001/', obj, {formSerializer: {indexes: mode}}) + .then(function (res) { + assert.deepStrictEqual(res.data, obj, 'Index mode ' + mode); + }); + })).then(function (){ + done(); + }, done) + }); + }); }); }); });