feat(HTTP Request Node): Redesign and add the ability to import cURL commands (#3860)

*  Initial commit

* 👕 Fix linting issue

*  Add import button

*  Remove ligh versioning

*  Improvements

*  Improvements

* 🔥 Remove HttpRequest2 file used for testing

* 🐛 Fix building issue

*  Small improvement

* 👕 Fix linting issue

* 🔥 Remove HttpRequest2 from loader

*  Update package-lock.json

*  Improvements

*  Small change

* 🐛 Fix issue retrieving splitIntoItems

* 🐛 Fix issue retrieving neverError parameter

* 🐛 Fix issue with displayOptions

*  Improvements

*  Improvements

*  Improvements

*  Improvements

*  Move cURL section to its own component

*  Improvements

*  Improvements

*  Add fix for  batching in all versions

*  Add notice to cURL modal

* 🔥 Remove comments

*  Improvements

*  Type curl-to-json endpoint

*  Fix typo

* 🔥 Remove console.logs

*  Fix typo in curl-to-json endpoint

*  Improvements

*  Improvements

*  Update package-lock.json

*  Rename import modal constant

*  Add return types to methods

*  Add CSS modules to ImportParameter component

*  Rename ImportParameter component to use kebab-case

*  Improvements

*  update package-lock.json

*  Fix linting issues

* Fix issue with css reference in ImportParameter component

*  Small improvements

*  Rename redirects to redirect

*  Allow to set multiple parameters on valueChanged

* 👕 Fix linting issue

* 🐛 Add mistakenly removed openExistingCredentials

*  Improve curl regex

*  Keep  headers as defined in the cURL command

*  Account for all protocols supported by cURL

*  Add tests

* 🔥 Remove unnecessary lines

*  Add more testing

*  Add noDataExpression to dependent fields

* 🐛 Fix bug not handling multipart-form data correctly

*  Change error messages

* 🐛 Fix response format string for empty values

* Fix typo
This commit is contained in:
Ricardo Espinoza 2022-09-29 17:28:02 -04:00 committed by GitHub
parent 5526057efc
commit f37d6ba03b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 5816 additions and 1805 deletions

297
package-lock.json generated
View file

@ -2485,6 +2485,57 @@
"node": ">=0.1.90"
}
},
"node_modules/@curlconverter/yargs": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/@curlconverter/yargs/-/yargs-0.0.2.tgz",
"integrity": "sha512-Q1YEebpCY61kxme4wvU0/IN/uMBfG5pZOKCo9FU+w20ElPvN+eH2qEVbK1C12t3Tee3qeYLLEU6HkiUeO1gc4A==",
"dependencies": {
"@curlconverter/yargs-parser": "^0.0.1",
"cliui": "^7.0.2",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.0",
"y18n": "^5.0.5"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@curlconverter/yargs-parser": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/@curlconverter/yargs-parser/-/yargs-parser-0.0.1.tgz",
"integrity": "sha512-DbEVRYqrorzwqc63MQ3RODflut1tNla8ZCKo1h83lF7+fbntgubZsDfRDBv5Lxj3vkKuvAolysNM2ekwJev8wA==",
"engines": {
"node": ">=10"
}
},
"node_modules/@curlconverter/yargs/node_modules/cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^7.0.0"
}
},
"node_modules/@curlconverter/yargs/node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/@curlconverter/yargs/node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"engines": {
"node": ">=10"
}
},
"node_modules/@dabh/diagnostics": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz",
@ -9038,6 +9089,11 @@
"vite": "^2.1.5 || ^3.0.5"
}
},
"node_modules/a-sync-waterfall": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz",
"integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA=="
},
"node_modules/abab": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
@ -9808,8 +9864,7 @@
"node_modules/asap": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
"dev": true
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="
},
"node_modules/asn1": {
"version": "0.2.6",
@ -13788,6 +13843,59 @@
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
"dev": true
},
"node_modules/curlconverter": {
"version": "3.21.0",
"resolved": "https://registry.npmjs.org/curlconverter/-/curlconverter-3.21.0.tgz",
"integrity": "sha512-DXCnp1A/Xa69FujksUfdvWQFAnIn/C+4Wuv8t+UVdZkF/lY5bzj98GGKOGme7V/ckSHDLxE29Xp76sJ5Cpsp5A==",
"dependencies": {
"@curlconverter/yargs": "^0.0.2",
"cookie": "^0.4.1",
"jsesc": "^3.0.2",
"nunjucks": "^3.2.3",
"query-string": "^7.0.1",
"string.prototype.startswith": "^1.0.0",
"yamljs": "^0.3.0"
},
"bin": {
"curlconverter": "bin/cli.js"
}
},
"node_modules/curlconverter/node_modules/jsesc": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
"integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==",
"bin": {
"jsesc": "bin/jsesc"
},
"engines": {
"node": ">=6"
}
},
"node_modules/curlconverter/node_modules/query-string": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.1.tgz",
"integrity": "sha512-MplouLRDHBZSG9z7fpuAAcI7aAYjDLhtsiVZsevsfaHWDS2IDdORKbSd1kWUA+V4zyva/HZoSfpwnYMMQDhb0w==",
"dependencies": {
"decode-uri-component": "^0.2.0",
"filter-obj": "^1.1.0",
"split-on-first": "^1.0.0",
"strict-uri-encode": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/curlconverter/node_modules/strict-uri-encode": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
"integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==",
"engines": {
"node": ">=4"
}
},
"node_modules/currently-unhandled": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
@ -13958,7 +14066,6 @@
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
"integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==",
"dev": true,
"engines": {
"node": ">=0.10"
}
@ -17037,6 +17144,14 @@
"node": ">=8"
}
},
"node_modules/filter-obj": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz",
"integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/finalhandler": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
@ -28003,6 +28118,38 @@
"node": ">=0.10.0"
}
},
"node_modules/nunjucks": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.3.tgz",
"integrity": "sha512-psb6xjLj47+fE76JdZwskvwG4MYsQKXUtMsPh6U0YMvmyjRtKRFcxnlXGWglNybtNTNVmGdp94K62/+NjF5FDQ==",
"dependencies": {
"a-sync-waterfall": "^1.0.0",
"asap": "^2.0.3",
"commander": "^5.1.0"
},
"bin": {
"nunjucks-precompile": "bin/precompile"
},
"engines": {
"node": ">= 6.9.0"
},
"peerDependencies": {
"chokidar": "^3.3.0"
},
"peerDependenciesMeta": {
"chokidar": {
"optional": true
}
}
},
"node_modules/nunjucks/node_modules/commander": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
"integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
"engines": {
"node": ">= 6"
}
},
"node_modules/nwsapi": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz",
@ -33584,6 +33731,14 @@
"node": ">=4.5"
}
},
"node_modules/split-on-first": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
"integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==",
"engines": {
"node": ">=6"
}
},
"node_modules/split-string": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
@ -34265,6 +34420,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/string.prototype.startswith": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/string.prototype.startswith/-/string.prototype.startswith-1.0.0.tgz",
"integrity": "sha512-VHhsDkuf8gsw4JNRK9cIZjYe6r7PsVUutVohaBhqYAoPaRADoQH+mMgUg7Cs/TgQeDGEvI+PzPEMOdvdsCMvpg==",
"dependencies": {
"define-properties": "^1.1.3",
"es-abstract": "^1.17.5"
}
},
"node_modules/string.prototype.trimend": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz",
@ -41556,6 +41720,7 @@
"cookie-parser": "^1.4.6",
"crypto-js": "~4.1.1",
"csrf": "^3.1.0",
"curlconverter": "^3.0.0",
"dotenv": "^8.0.0",
"express": "^4.16.4",
"express-openapi-validator": "^4.13.6",
@ -44294,6 +44459,47 @@
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
"integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="
},
"@curlconverter/yargs": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/@curlconverter/yargs/-/yargs-0.0.2.tgz",
"integrity": "sha512-Q1YEebpCY61kxme4wvU0/IN/uMBfG5pZOKCo9FU+w20ElPvN+eH2qEVbK1C12t3Tee3qeYLLEU6HkiUeO1gc4A==",
"requires": {
"@curlconverter/yargs-parser": "^0.0.1",
"cliui": "^7.0.2",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.0",
"y18n": "^5.0.5"
},
"dependencies": {
"cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
"requires": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^7.0.0"
}
},
"get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
},
"y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="
}
}
},
"@curlconverter/yargs-parser": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/@curlconverter/yargs-parser/-/yargs-parser-0.0.1.tgz",
"integrity": "sha512-DbEVRYqrorzwqc63MQ3RODflut1tNla8ZCKo1h83lF7+fbntgubZsDfRDBv5Lxj3vkKuvAolysNM2ekwJev8wA=="
},
"@dabh/diagnostics": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz",
@ -50563,6 +50769,11 @@
"integrity": "sha512-HlhVMhrOpBHlkkdSm5wQ6YN65ZqdF9YqP7HS1at6NjQgEOIhBiP7tRZ+dYM3ymztKAX0ovhyKCY9ZnQ/GMK0Qg==",
"dev": true
},
"a-sync-waterfall": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz",
"integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA=="
},
"abab": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
@ -51132,8 +51343,7 @@
"asap": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
"dev": true
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="
},
"asn1": {
"version": "0.2.6",
@ -54331,6 +54541,43 @@
}
}
},
"curlconverter": {
"version": "3.21.0",
"resolved": "https://registry.npmjs.org/curlconverter/-/curlconverter-3.21.0.tgz",
"integrity": "sha512-DXCnp1A/Xa69FujksUfdvWQFAnIn/C+4Wuv8t+UVdZkF/lY5bzj98GGKOGme7V/ckSHDLxE29Xp76sJ5Cpsp5A==",
"requires": {
"@curlconverter/yargs": "^0.0.2",
"cookie": "^0.4.1",
"jsesc": "^3.0.2",
"nunjucks": "^3.2.3",
"query-string": "^7.0.1",
"string.prototype.startswith": "^1.0.0",
"yamljs": "^0.3.0"
},
"dependencies": {
"jsesc": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
"integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g=="
},
"query-string": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.1.tgz",
"integrity": "sha512-MplouLRDHBZSG9z7fpuAAcI7aAYjDLhtsiVZsevsfaHWDS2IDdORKbSd1kWUA+V4zyva/HZoSfpwnYMMQDhb0w==",
"requires": {
"decode-uri-component": "^0.2.0",
"filter-obj": "^1.1.0",
"split-on-first": "^1.0.0",
"strict-uri-encode": "^2.0.0"
}
},
"strict-uri-encode": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
"integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ=="
}
}
},
"currently-unhandled": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
@ -54463,8 +54710,7 @@
"decode-uri-component": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
"integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==",
"dev": true
"integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og=="
},
"dedent": {
"version": "0.7.0",
@ -56777,6 +57023,11 @@
"to-regex-range": "^5.0.1"
}
},
"filter-obj": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz",
"integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ=="
},
"finalhandler": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
@ -64791,6 +65042,7 @@
"cookie-parser": "^1.4.6",
"crypto-js": "~4.1.1",
"csrf": "^3.1.0",
"curlconverter": "^3.0.0",
"dotenv": "^8.0.0",
"express": "^4.16.4",
"express-openapi-validator": "^4.13.6",
@ -66266,6 +66518,23 @@
"integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==",
"dev": true
},
"nunjucks": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.3.tgz",
"integrity": "sha512-psb6xjLj47+fE76JdZwskvwG4MYsQKXUtMsPh6U0YMvmyjRtKRFcxnlXGWglNybtNTNVmGdp94K62/+NjF5FDQ==",
"requires": {
"a-sync-waterfall": "^1.0.0",
"asap": "^2.0.3",
"commander": "^5.1.0"
},
"dependencies": {
"commander": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
"integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="
}
}
},
"nwsapi": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz",
@ -70672,6 +70941,11 @@
"resolved": "https://registry.npmjs.org/spex/-/spex-3.2.0.tgz",
"integrity": "sha512-9srjJM7NaymrpwMHvSmpDeIK5GoRMX/Tq0E8aOlDPS54dDnDUIp30DrP9SphMPEETDLzEM9+4qo+KipmbtPecg=="
},
"split-on-first": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
"integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw=="
},
"split-string": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
@ -71205,6 +71479,15 @@
"es-abstract": "^1.19.1"
}
},
"string.prototype.startswith": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/string.prototype.startswith/-/string.prototype.startswith-1.0.0.tgz",
"integrity": "sha512-VHhsDkuf8gsw4JNRK9cIZjYe6r7PsVUutVohaBhqYAoPaRADoQH+mMgUg7Cs/TgQeDGEvI+PzPEMOdvdsCMvpg==",
"requires": {
"define-properties": "^1.1.3",
"es-abstract": "^1.17.5"
}
},
"string.prototype.trimend": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz",

View file

@ -124,6 +124,7 @@
"cookie-parser": "^1.4.6",
"crypto-js": "~4.1.1",
"csrf": "^3.1.0",
"curlconverter": "^3.0.0",
"dotenv": "^8.0.0",
"express": "^4.16.4",
"express-openapi-validator": "^4.13.6",

5
packages/cli/src/CurlConverter.d.ts vendored Normal file
View file

@ -0,0 +1,5 @@
/* eslint-disable @typescript-eslint/naming-convention */
/* eslint-disable import/no-default-export */
declare module 'curlconverter' {
export function toJsonString(data: string): string;
}

View file

@ -0,0 +1,466 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import curlconverter from 'curlconverter';
import get from 'lodash.get';
interface CurlJson {
url: string;
raw_url?: string;
method: string;
contentType?: string;
cookies?: {
[key: string]: string;
};
auth?: {
user: string;
password: string;
};
headers?: {
[key: string]: string;
};
files?: {
[key: string]: string;
};
queries: {
[key: string]: string;
};
data?: {
[key: string]: string;
};
}
interface Parameter {
parameterType?: string;
name: string;
value: string;
}
export interface HttpNodeParameters {
url?: string;
method: string;
sendBody?: boolean;
authentication: string;
contentType?: 'form-urlencoded' | 'multipart-form-data' | 'json' | 'raw' | 'binaryData';
rawContentType?: string;
specifyBody?: 'json' | 'keypair';
bodyParameters?: {
parameters: Parameter[];
};
jsonBody?: object;
options: {
allowUnauthorizedCerts?: boolean;
proxy?: string;
timeout?: number;
redirect: {
redirect: {
followRedirects?: boolean;
maxRedirects?: number;
};
};
response: {
response: {
fullResponse?: boolean;
responseFormat?: string;
outputPropertyName?: string;
};
};
};
sendHeaders?: boolean;
headerParameters?: {
parameters: Parameter[];
};
sendQuery?: boolean;
queryParameters?: {
parameters: Parameter[];
};
}
type HttpNodeHeaders = Pick<HttpNodeParameters, 'sendHeaders' | 'headerParameters'>;
type HttpNodeQueries = Pick<HttpNodeParameters, 'sendQuery' | 'queryParameters'>;
enum ContentTypes {
applicationJson = 'application/json',
applicationFormUrlEncoded = 'application/x-www-form-urlencoded',
applicationMultipart = 'multipart/form-data',
}
const SUPPORTED_CONTENT_TYPES = [
ContentTypes.applicationJson,
ContentTypes.applicationFormUrlEncoded,
ContentTypes.applicationMultipart,
];
const CONTENT_TYPE_KEY = 'content-type';
const FOLLOW_REDIRECT_FLAGS = ['--location', '-L'];
const MAX_REDIRECT_FLAG = '--max-redirs';
const PROXY_FLAGS = ['-x', '--proxy'];
const INCLUDE_HEADERS_IN_OUTPUT_FLAGS = ['-i', '--include'];
const REQUEST_FLAGS = ['-X', '--request'];
const TIMEOUT_FLAGS = ['--connect-timeout'];
const DOWNLOAD_FILE_FLAGS = ['-O', '-o'];
const IGNORE_SSL_ISSUES_FLAGS = ['-k', '--insecure'];
const curlToJson = (curlCommand: string): CurlJson => {
// eslint-disable-next-line n8n-local-rules/no-uncaught-json-parse
return JSON.parse(curlconverter.toJsonString(curlCommand)) as CurlJson;
};
const isContentType = (headers: CurlJson['headers'], contentType: ContentTypes): boolean => {
return get(headers, CONTENT_TYPE_KEY) === contentType;
};
const isJsonRequest = (curlJson: CurlJson): boolean => {
if (isContentType(curlJson.headers, ContentTypes.applicationJson)) return true;
if (curlJson.data) {
const bodyKey = Object.keys(curlJson.data)[0];
try {
JSON.parse(bodyKey);
return true;
} catch (_) {
return false;
}
}
return false;
};
const isFormUrlEncodedRequest = (curlJson: CurlJson): boolean => {
if (isContentType(curlJson.headers, ContentTypes.applicationFormUrlEncoded)) return true;
if (curlJson.data && !curlJson.files) return true;
return false;
};
const isMultipartRequest = (curlJson: CurlJson): boolean => {
if (isContentType(curlJson.headers, ContentTypes.applicationMultipart)) return true;
// only multipart/form-data request include files
if (curlJson.files) return true;
return false;
};
const isBinaryRequest = (curlJson: CurlJson): boolean => {
if (curlJson?.headers?.[CONTENT_TYPE_KEY]) {
const contentType = curlJson?.headers?.[CONTENT_TYPE_KEY];
return ['image', 'video', 'audio'].some((d) => contentType.includes(d));
}
return false;
};
const sanatizeCurlCommand = (curlCommand: string) =>
curlCommand
.replace(/\r\n/g, ' ')
.replace(/\n/g, ' ')
.replace(/\\/g, ' ')
.replace(/[ ]{2,}/g, ' ');
const toKeyValueArray = ([key, value]: string[]) => ({ name: key, value });
const extractHeaders = (headers: CurlJson['headers'] = {}): HttpNodeHeaders => {
const emptyHeaders = !Object.keys(headers).length;
const onlyContentTypeHeaderDefined =
Object.keys(headers).length === 1 && headers[CONTENT_TYPE_KEY] !== undefined;
if (emptyHeaders || onlyContentTypeHeaderDefined) return { sendHeaders: false };
return {
sendHeaders: true,
headerParameters: {
parameters: Object.entries(headers)
.map(toKeyValueArray)
.filter((parameter) => parameter.name !== CONTENT_TYPE_KEY),
},
};
};
const extractQueries = (queries: CurlJson['queries'] = {}): HttpNodeQueries => {
const emptyQueries = !Object.keys(queries).length;
if (emptyQueries) return { sendQuery: false };
return {
sendQuery: true,
queryParameters: {
parameters: Object.entries(queries).map(toKeyValueArray),
},
};
};
const extractJson = (body: CurlJson['data']) =>
//@ts-ignore
// eslint-disable-next-line n8n-local-rules/no-uncaught-json-parse
JSON.parse(Object.keys(body)[0]) as { [key: string]: string };
const jsonBodyToNodeParameters = (body: CurlJson['data'] = {}): Parameter[] | [] => {
const data = extractJson(body);
return Object.entries(data).map(toKeyValueArray);
};
const multipartToNodeParameters = (
body: CurlJson['data'] = {},
files: CurlJson['files'] = {},
): Parameter[] | [] => {
return [
...Object.entries(body)
.map(toKeyValueArray)
.map((e) => ({ parameterType: 'formData', ...e })),
...Object.entries(files)
.map(toKeyValueArray)
.map((e) => ({ parameterType: 'formBinaryData', ...e })),
];
};
const keyValueBodyToNodeParameters = (body: CurlJson['data'] = {}): Parameter[] | [] => {
return Object.entries(body).map(toKeyValueArray);
};
const lowerCaseContentTypeKey = (obj: { [x: string]: string }): void => {
const regex = new RegExp(CONTENT_TYPE_KEY, 'gi');
const contentTypeKey = Object.keys(obj).find((key) => {
const group = Array.from(key.matchAll(regex));
if (group.length) return true;
return false;
});
if (!contentTypeKey) return;
const value = obj[contentTypeKey];
delete obj[contentTypeKey];
obj[CONTENT_TYPE_KEY] = value;
};
const encodeBasicAuthentication = (username: string, password: string) =>
Buffer.from(`${username}:${password}`).toString('base64');
const jsonHasNestedObjects = (json: { [key: string]: string | number | object }) =>
Object.values(json).some((e) => typeof e === 'object');
const extractGroup = (curlCommand: string, regex: RegExp) => curlCommand.matchAll(regex);
const mapCookies = (cookies: CurlJson['cookies']): { cookie: string } | {} => {
if (!cookies) return {};
const cookiesValues = Object.entries(cookies).reduce(
(accumulator: string, entry: [string, string]) => {
accumulator += `${entry[0]}=${entry[1]};`;
return accumulator;
},
'',
);
if (!cookiesValues) return {};
return {
cookie: cookiesValues,
};
};
export const toHttpNodeParameters = (curlCommand: string): HttpNodeParameters => {
const curlJson = curlToJson(curlCommand);
if (!curlJson.headers) curlJson.headers = {};
lowerCaseContentTypeKey(curlJson.headers);
// set basic authentication
if (curlJson.auth) {
const { user, password: pass } = curlJson.auth;
Object.assign(curlJson.headers, {
authorization: `Basic ${encodeBasicAuthentication(user, pass)}`,
});
}
const httpNodeParameters: HttpNodeParameters = {
url: curlJson.url,
authentication: 'none',
method: curlJson.method.toUpperCase(),
...extractHeaders({ ...curlJson.headers, ...mapCookies(curlJson.cookies) }),
...extractQueries(curlJson.queries),
options: {
redirect: {
redirect: {},
},
response: {
response: {},
},
},
};
//attempt to get the curl flags not supported by the library
const curl = sanatizeCurlCommand(curlCommand);
//check for follow redirect flags
if (FOLLOW_REDIRECT_FLAGS.some((flag) => curl.includes(` ${flag}`))) {
Object.assign(httpNodeParameters.options.redirect?.redirect, { followRedirects: true });
if (curl.includes(` ${MAX_REDIRECT_FLAG}`)) {
const extractedValue = Array.from(
extractGroup(curl, new RegExp(` ${MAX_REDIRECT_FLAG} (\\d+)`, 'g')),
);
if (extractedValue.length) {
const [_, maxRedirects] = extractedValue[0];
if (maxRedirects) {
Object.assign(httpNodeParameters.options.redirect?.redirect, { maxRedirects });
}
}
}
}
//check for proxy flags
if (PROXY_FLAGS.some((flag) => curl.includes(` ${flag}`))) {
const foundFlag = PROXY_FLAGS.find((flag) => curl.includes(` ${flag}`));
if (foundFlag) {
const extractedValue = Array.from(
extractGroup(curl, new RegExp(` ${foundFlag} (\\S*)`, 'g')),
);
if (extractedValue.length) {
const [_, proxy] = extractedValue[0];
Object.assign(httpNodeParameters.options, { proxy });
}
}
}
// check for "include header in output" flag
if (INCLUDE_HEADERS_IN_OUTPUT_FLAGS.some((flag) => curl.includes(` ${flag}`))) {
Object.assign(httpNodeParameters.options?.response?.response, {
fullResponse: true,
responseFormat: 'autodetect',
});
}
// check for request flag
if (REQUEST_FLAGS.some((flag) => curl.includes(` ${flag}`))) {
const foundFlag = REQUEST_FLAGS.find((flag) => curl.includes(` ${flag}`));
if (foundFlag) {
const extractedValue = Array.from(
extractGroup(curl, new RegExp(` ${foundFlag} (\\w+)`, 'g')),
);
if (extractedValue.length) {
const [_, request] = extractedValue[0];
httpNodeParameters.method = request.toUpperCase();
}
}
}
// check for timeout flag
if (TIMEOUT_FLAGS.some((flag) => curl.includes(` ${flag}`))) {
const foundFlag = TIMEOUT_FLAGS.find((flag) => curl.includes(` ${flag}`));
if (foundFlag) {
const extractedValue = Array.from(
extractGroup(curl, new RegExp(` ${foundFlag} (\\d+)`, 'g')),
);
if (extractedValue.length) {
const [_, timeout] = extractedValue[0];
Object.assign(httpNodeParameters.options, {
timeout: parseInt(timeout, 10) * 1000,
});
}
}
}
// check for download flag
if (DOWNLOAD_FILE_FLAGS.some((flag) => curl.includes(` ${flag}`))) {
const foundFlag = DOWNLOAD_FILE_FLAGS.find((flag) => curl.includes(` ${flag}`));
if (foundFlag) {
Object.assign(httpNodeParameters.options.response.response, {
responseFormat: 'file',
outputPropertyName: 'data',
});
}
}
if (IGNORE_SSL_ISSUES_FLAGS.some((flag) => curl.includes(` ${flag}`))) {
const foundFlag = IGNORE_SSL_ISSUES_FLAGS.find((flag) => curl.includes(` ${flag}`));
if (foundFlag) {
Object.assign(httpNodeParameters.options, {
allowUnauthorizedCerts: true,
});
}
}
const contentType = curlJson?.headers?.[CONTENT_TYPE_KEY] as ContentTypes;
if (isBinaryRequest(curlJson)) {
return Object.assign(httpNodeParameters, {
contentType: 'binaryData',
sendBody: true,
});
}
if (contentType && !SUPPORTED_CONTENT_TYPES.includes(contentType)) {
return Object.assign(httpNodeParameters, {
sendBody: true,
contentType: 'raw',
rawContentType: contentType,
body: Object.keys(curlJson?.data ?? {})[0],
});
}
if (isJsonRequest(curlJson)) {
Object.assign(httpNodeParameters, {
contentType: 'json',
sendBody: true,
});
const json = extractJson(curlJson.data);
if (jsonHasNestedObjects(json)) {
// json body
Object.assign(httpNodeParameters, {
specifyBody: 'json',
jsonBody: JSON.stringify(json),
});
} else {
// key-value body
Object.assign(httpNodeParameters, {
specifyBody: 'keypair',
bodyParameters: {
parameters: jsonBodyToNodeParameters(curlJson.data),
},
});
}
} else if (isFormUrlEncodedRequest(curlJson)) {
Object.assign(httpNodeParameters, {
contentType: 'form-urlencoded',
sendBody: true,
specifyBody: 'keypair',
bodyParameters: {
parameters: keyValueBodyToNodeParameters(curlJson.data),
},
});
} else if (isMultipartRequest(curlJson)) {
Object.assign(httpNodeParameters, {
contentType: 'multipart-form-data',
sendBody: true,
bodyParameters: {
parameters: multipartToNodeParameters(curlJson.data, curlJson.files),
},
});
} else {
// could not figure the content type so do not set the body
Object.assign(httpNodeParameters, {
sendBody: false,
});
}
if (!Object.keys(httpNodeParameters.options?.redirect.redirect).length) {
// @ts-ignore
delete httpNodeParameters.options.redirect;
}
if (!Object.keys(httpNodeParameters.options.response.response).length) {
// @ts-ignore
delete httpNodeParameters.options.response;
}
return httpNodeParameters;
};

View file

@ -221,3 +221,13 @@ export function unflattenExecutionData(fullExecutionData: IExecutionFlattedDb):
return returnData;
}
export const flattenObject = (obj: { [x: string]: any }, prefix = '') =>
Object.keys(obj).reduce((acc, k) => {
const pre = prefix.length ? prefix + '.' : '';
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
if (typeof obj[k] === 'object') Object.assign(acc, flattenObject(obj[k], pre + k));
//@ts-ignore
else acc[pre + k] = obj[k];
return acc;
}, {});

View file

@ -43,6 +43,7 @@ import { FindManyOptions, getConnectionManager, In } from 'typeorm';
// eslint-disable-next-line import/no-extraneous-dependencies
import axios, { AxiosRequestConfig } from 'axios';
import clientOAuth1, { RequestOptions } from 'oauth-1.0a';
import curlconverter from 'curlconverter';
// IMPORTANT! Do not switch to anther bcrypt library unless really necessary and
// tested with all possible systems like Windows, Alpine on ARM, FreeBSD, ...
import { compare } from 'bcryptjs';
@ -96,6 +97,7 @@ import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES } from './constants';
import { credentialsController } from './credentials/credentials.controller';
import { oauth2CredentialController } from './credentials/oauth2Credential.api';
import type {
CurlHelper,
ExecutionRequest,
NodeListSearchRequest,
NodeParameterOptionsRequest,
@ -151,6 +153,8 @@ import {
import glob from 'fast-glob';
import { ResponseError } from './ResponseHelper';
import { toHttpNodeParameters } from './CurlConverterHelper';
require('body-parser-xml')(bodyParser);
const exec = promisify(callbackExec);
@ -1061,6 +1065,27 @@ class App {
}),
);
// ----------------------------------------
// curl-converter
// ----------------------------------------
this.app.post(
`/${this.restEndpoint}/curl-to-json`,
ResponseHelper.send(
async (
req: CurlHelper.ToJson,
res: express.Response,
): Promise<{ [key: string]: string }> => {
const curlCommand = req.body.curlCommand ?? '';
try {
const parameters = toHttpNodeParameters(curlCommand);
return ResponseHelper.flattenObject(parameters, 'parameters');
} catch (e) {
throw new ResponseHelper.ResponseError(`Invalid cURL command`, undefined, 400);
}
},
),
);
// ----------------------------------------
// Credential-Types
// ----------------------------------------

View file

@ -327,3 +327,11 @@ export declare namespace NodeRequest {
type Update = Post;
}
// ----------------------------------
// /curl-to-json
// ----------------------------------
export declare namespace CurlHelper {
type ToJson = AuthenticatedRequest<{}, {}, { curlCommand?: string }>;
}

View file

@ -0,0 +1,275 @@
import { toHttpNodeParameters } from "../../src/CurlConverterHelper";
describe('CurlConverterHelper', () => {
test('Should parse form-urlencoded content type correctly', () => {
const curl = 'curl -X POST https://reqbin.com/echo/post/form -H "Content-Type: application/x-www-form-urlencoded" -d "param1=value1&param2=value2"';
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo/post/form');
expect(parameters.sendBody).toBe(true);
expect(parameters.bodyParameters?.parameters[0].name).toBe('param1');
expect(parameters.bodyParameters?.parameters[0].value).toBe('value1');
expect(parameters.bodyParameters?.parameters[1].name).toBe('param2');
expect(parameters.bodyParameters?.parameters[1].value).toBe('value2');
expect(parameters.contentType).toBe('form-urlencoded');
expect(parameters.sendHeaders).toBe(false);
expect(parameters.sendQuery).toBe(false);
});
test('Should parse JSON content type correctly', () => {
const curl = `curl -X POST https://reqbin.com/echo/post/json -H 'Content-Type: application/json' -d '{"login":"my_login","password":"my_password"}'`;
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo/post/json');
expect(parameters.sendBody).toBe(true);
expect(parameters.bodyParameters?.parameters[0].name).toBe('login');
expect(parameters.bodyParameters?.parameters[0].value).toBe('my_login');
expect(parameters.bodyParameters?.parameters[1].name).toBe('password');
expect(parameters.bodyParameters?.parameters[1].value).toBe('my_password');
expect(parameters.contentType).toBe('json');
expect(parameters.sendHeaders).toBe(false);
expect(parameters.sendQuery).toBe(false);
});
test('Should parse multipart-form-data content type correctly', () => {
const curl = `curl -X POST https://reqbin.com/echo/post/json -v -F key1=value1 -F upload=@localfilename`;
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo/post/json');
expect(parameters.sendBody).toBe(true);
expect(parameters.bodyParameters?.parameters[0].parameterType).toBe('formData');
expect(parameters.bodyParameters?.parameters[0].name).toBe('key1');
expect(parameters.bodyParameters?.parameters[0].value).toBe('value1');
expect(parameters.bodyParameters?.parameters[1].parameterType).toBe('formBinaryData');
expect(parameters.bodyParameters?.parameters[1].name).toBe('upload');
expect(parameters.contentType).toBe('multipart-form-data');
expect(parameters.sendHeaders).toBe(false);
expect(parameters.sendQuery).toBe(false);
});
test('Should parse binary request correctly', () => {
const curl = `curl --location --request POST 'https://www.website.com' --header 'Content-Type: image/png' --data-binary '@/Users/image.png`;
const parameters = toHttpNodeParameters(curl);
console.log(JSON.stringify(parameters, undefined, 2));
expect(parameters.url).toBe('https://www.website.com');
expect(parameters.method).toBe('POST');
expect(parameters.sendBody).toBe(true);
expect(parameters.contentType).toBe('binaryData');
expect(parameters.sendHeaders).toBe(false);
expect(parameters.sendQuery).toBe(false);
});
test('Should parse unknown content type correctly', () => {
const curl = `curl -X POST https://reqbin.com/echo/post/xml
-H "Content-Type: application/xml"
-H "Accept: application/xml"
-d "<Request><Login>my_login</Login><Password>my_password</Password></Request>"`;
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo/post/xml');
expect(parameters.method).toBe('POST');
expect(parameters.sendBody).toBe(true);
expect(parameters.contentType).toBe('raw');
expect(parameters.rawContentType).toBe('application/xml');
expect(parameters.sendHeaders).toBe(true);
expect(parameters.headerParameters?.parameters[0].name).toBe('Accept');
expect(parameters.headerParameters?.parameters[0].value).toBe('application/xml');
expect(parameters.sendQuery).toBe(false);
});
test('Should parse header properties and keep the original case', () => {
const curl = `curl -X POST https://reqbin.com/echo/post/json -v -F key1=value1 -F upload=@localfilename -H "ACCEPT: text/javascript" -H "content-type: multipart/form-data"`;
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo/post/json');
expect(parameters.sendBody).toBe(true);
expect(parameters.bodyParameters?.parameters[0].parameterType).toBe('formData');
expect(parameters.bodyParameters?.parameters[0].name).toBe('key1');
expect(parameters.bodyParameters?.parameters[0].value).toBe('value1');
expect(parameters.bodyParameters?.parameters[1].parameterType).toBe('formBinaryData');
expect(parameters.bodyParameters?.parameters[1].name).toBe('upload');
expect(parameters.contentType).toBe('multipart-form-data');
expect(parameters.sendHeaders).toBe(true);
expect(parameters.headerParameters?.parameters[0].name).toBe('ACCEPT');
expect(parameters.headerParameters?.parameters[0].value).toBe('text/javascript');
expect(parameters.sendQuery).toBe(false);
});
test('Should parse querystring properties', () => {
const curl = `curl -G -d 'q=kitties' -d 'count=20' https://google.com/search`;
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://google.com/search');
expect(parameters.sendBody).toBe(false);
expect(parameters.contentType).toBeUndefined();
expect(parameters.sendHeaders).toBe(false);
expect(parameters.sendQuery).toBe(true);
expect(parameters.queryParameters?.parameters[0].name).toBe('q');
expect(parameters.queryParameters?.parameters[0].value).toBe('kitties');
expect(parameters.queryParameters?.parameters[1].name).toBe('count');
expect(parameters.queryParameters?.parameters[1].value).toBe('20');
});
test('Should parse basic authentication property and keep the original case', () => {
const curl = `curl https://reqbin.com/echo -u "login:password"`;
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.sendBody).toBe(false);
expect(parameters.contentType).toBeUndefined();
expect(parameters.sendQuery).toBe(false);
expect(parameters.sendHeaders).toBe(true);
expect(parameters.headerParameters?.parameters[0].name).toBe('authorization');
expect(parameters.headerParameters?.parameters[0].value).toBe(`Basic ${Buffer.from('login:password').toString('base64')}`);
});
test('Should parse location flag with --location', () => {
const curl = `curl https://reqbin.com/echo -u "login:password" --location`;
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.sendBody).toBe(false);
expect(parameters.contentType).toBeUndefined();
expect(parameters.sendQuery).toBe(false);
expect(parameters.sendHeaders).toBe(true);
expect(parameters.headerParameters?.parameters[0].name).toBe('authorization');
expect(parameters.headerParameters?.parameters[0].value).toBe(`Basic ${Buffer.from('login:password').toString('base64')}`);
expect(parameters.options.redirect.redirect.followRedirects).toBe(true);
});
test('Should parse location flag with --L', () => {
const curl = `curl https://reqbin.com/echo -u "login:password" -L`;
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.sendBody).toBe(false);
expect(parameters.contentType).toBeUndefined();
expect(parameters.sendQuery).toBe(false);
expect(parameters.sendHeaders).toBe(true);
expect(parameters.headerParameters?.parameters[0].name).toBe('authorization');
expect(parameters.headerParameters?.parameters[0].value).toBe(`Basic ${Buffer.from('login:password').toString('base64')}`);
expect(parameters.options.redirect.redirect.followRedirects).toBe(true);
});
test('Should parse location and max redirects flags with --location and --max-redirs 10', () => {
const curl = `curl https://reqbin.com/echo -u "login:password" --location --max-redirs 10`;
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.sendBody).toBe(false);
expect(parameters.contentType).toBeUndefined();
expect(parameters.sendQuery).toBe(false);
expect(parameters.sendHeaders).toBe(true);
expect(parameters.headerParameters?.parameters[0].name).toBe('authorization');
expect(parameters.headerParameters?.parameters[0].value).toBe(`Basic ${Buffer.from('login:password').toString('base64')}`);
expect(parameters.options.redirect.redirect.followRedirects).toBe(true);
expect(parameters.options.redirect.redirect.maxRedirects).toBe("10");
});
test('Should parse proxy flag -x', () => {
const curl = `curl https://reqbin.com/echo -u "login:password" -x https://google.com`;
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.sendBody).toBe(false);
expect(parameters.contentType).toBeUndefined();
expect(parameters.sendQuery).toBe(false);
expect(parameters.sendHeaders).toBe(true);
expect(parameters.headerParameters?.parameters[0].name).toBe('authorization');
expect(parameters.headerParameters?.parameters[0].value).toBe(`Basic ${Buffer.from('login:password').toString('base64')}`);
expect(parameters.options.proxy).toBe('https://google.com');
});
test('Should parse proxy flag --proxy', () => {
const curl = `curl https://reqbin.com/echo -u "login:password" -x https://google.com`;
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.sendBody).toBe(false);
expect(parameters.contentType).toBeUndefined();
expect(parameters.sendQuery).toBe(false);
expect(parameters.sendHeaders).toBe(true);
expect(parameters.headerParameters?.parameters[0].name).toBe('authorization');
expect(parameters.headerParameters?.parameters[0].value).toBe(`Basic ${Buffer.from('login:password').toString('base64')}`);
expect(parameters.options.proxy).toBe('https://google.com');
});
test('Should parse include headers on output flag --include', () => {
const curl = `curl https://reqbin.com/echo -u "login:password" --include -x https://google.com`;
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.sendBody).toBe(false);
expect(parameters.contentType).toBeUndefined();
expect(parameters.sendQuery).toBe(false);
expect(parameters.sendHeaders).toBe(true);
expect(parameters.headerParameters?.parameters[0].name).toBe('authorization');
expect(parameters.headerParameters?.parameters[0].value).toBe(`Basic ${Buffer.from('login:password').toString('base64')}`);
expect(parameters.options.response.response.fullResponse).toBe(true);
});
test('Should parse include headers on output flag -i', () => {
const curl = `curl https://reqbin.com/echo -u "login:password" -x https://google.com -i`;
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.sendBody).toBe(false);
expect(parameters.contentType).toBeUndefined();
expect(parameters.sendQuery).toBe(false);
expect(parameters.sendHeaders).toBe(true);
expect(parameters.headerParameters?.parameters[0].name).toBe('authorization');
expect(parameters.headerParameters?.parameters[0].value).toBe(`Basic ${Buffer.from('login:password').toString('base64')}`);
expect(parameters.options.response.response.fullResponse).toBe(true);
});
test('Should parse include request flag -X', () => {
const curl = `curl -X POST https://reqbin.com/echo -u "login:password" -x https://google.com`;
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.method).toBe('POST');
expect(parameters.sendBody).toBe(false);
});
test('Should parse include request flag --request', () => {
const curl = `curl --request POST https://reqbin.com/echo -u "login:password" -x https://google.com`;
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.method).toBe('POST');
expect(parameters.sendBody).toBe(false);
});
test('Should parse include timeout flag --connect-timeout', () => {
const curl = `curl --request POST https://reqbin.com/echo -u "login:password" --connect-timeout 20`;
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.method).toBe('POST');
expect(parameters.sendBody).toBe(false);
expect(parameters.options.timeout).toBe(20000);
});
test('Should parse download file flag -O', () => {
const curl = `curl --request POST https://reqbin.com/echo -u "login:password" -O`;
; const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.method).toBe('POST');
expect(parameters.sendBody).toBe(false);
expect(parameters.options.response.response.responseFormat).toBe('file');
expect(parameters.options.response.response.outputPropertyName).toBe('data');
})
test('Should parse download file flag -o', () => {
const curl = `curl --request POST https://reqbin.com/echo -u "login:password" -o`;
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.method).toBe('POST');
expect(parameters.sendBody).toBe(false);
expect(parameters.options.response.response.responseFormat).toBe('file');
expect(parameters.options.response.response.outputPropertyName).toBe('data');
})
test('Should parse ignore SSL flag -k', () => {
const curl = `curl --request POST https://reqbin.com/echo -u "login:password" -k`;
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.method).toBe('POST');
expect(parameters.sendBody).toBe(false);
expect(parameters.options.allowUnauthorizedCerts).toBe(true);
})
test('Should parse ignore SSL flag --insecure', () => {
const curl = `curl --request POST https://reqbin.com/echo -u "login:password" --insecure`;
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.method).toBe('POST');
expect(parameters.sendBody).toBe(false);
expect(parameters.options.allowUnauthorizedCerts).toBe(true);
})
});

View file

@ -127,7 +127,7 @@ export interface IEndpointOptions {
export interface IUpdateInformation {
name: string;
key: string;
value: string | number; // with null makes problems in NodeSettings.vue
value: string | number | { [key: string]: string | number | boolean }; // with null makes problems in NodeSettings.vue
node?: string;
oldValue?: string | number;
}
@ -924,6 +924,8 @@ export interface IModalState {
open: boolean;
mode?: string | null;
activeId?: string | null;
curlCommand?: string;
httpNodeParameters?: string;
}
export type IRunDataDisplayMode = 'table' | 'json' | 'binary';

View file

@ -0,0 +1,6 @@
import {IRestApiContext} from "@/Interface";
import {makeRestApiRequest} from "@/api/helpers";
export function getCurlToJson(context: IRestApiContext, curlCommand: string): Promise<{ curlCommand: string | null }> {
return makeRestApiRequest(context, 'POST', '/curl-to-json', { curlCommand });
}

View file

@ -0,0 +1,196 @@
<template>
<Modal
width="700px"
:title="$locale.baseText('importCurlModal.title')"
:eventBus="modalBus"
:name="IMPORT_CURL_MODAL_KEY"
:center="true"
>
<template slot="content">
<div :class="$style.container">
<n8n-input-label :label="$locale.baseText('importCurlModal.input.label')">
<n8n-input
:value="curlCommand"
type="textarea"
:rows="5"
:placeholder="$locale.baseText('importCurlModal.input.placeholder')"
@input="onInput"
@focus="$event.target.select()"
ref="input"
/>
</n8n-input-label>
</div>
</template>
<template slot="footer">
<div :class="$style.modalFooter">
<n8n-notice
:class="$style.notice"
:content="$locale.baseText('ImportCurlModal.notice.content')"
/>
<div>
<n8n-button
@click="importCurlCommand"
float="right"
:label="$locale.baseText('importCurlModal.button.label')"
/>
</div>
</div>
</template>
</Modal>
</template>
<script lang="ts">
import Vue from 'vue';
import Modal from './Modal.vue';
import {
IMPORT_CURL_MODAL_KEY,
CURL_IMPORT_NOT_SUPPORTED_PROTOCOLS,
CURL_IMPORT_NODES_PROTOCOLS,
} from '../constants';
import { showMessage } from './mixins/showMessage';
import mixins from 'vue-typed-mixins';
import { INodeUi } from '@/Interface';
export default mixins(showMessage).extend({
name: 'ImportCurlModal',
components: {
Modal,
},
data() {
return {
IMPORT_CURL_MODAL_KEY,
curlCommand: '',
modalBus: new Vue(),
};
},
computed: {
node(): INodeUi {
return this.$store.getters.activeNode;
},
},
methods: {
closeDialog(): void {
this.modalBus.$emit('close');
},
onInput(value: string): void {
this.curlCommand = value;
},
async importCurlCommand(): Promise<void> {
const curlCommand = this.curlCommand;
if (curlCommand === '') return;
try {
const parameters = await this.$store.dispatch('ui/getCurlToJson', curlCommand);
const url = parameters['parameters.url'];
const invalidProtocol = CURL_IMPORT_NOT_SUPPORTED_PROTOCOLS.find((p) =>
url.includes(`${p}://`),
);
if (!invalidProtocol) {
this.$store.dispatch('ui/setHttpNodeParameters', {
parameters: JSON.stringify(parameters),
});
this.closeDialog();
this.sendTelemetry();
return;
// if we have a node that supports the invalid protocol
// suggest that one
} else if (CURL_IMPORT_NODES_PROTOCOLS[invalidProtocol]) {
const useNode = CURL_IMPORT_NODES_PROTOCOLS[invalidProtocol];
this.showProtocolErrorWithSupportedNode(invalidProtocol, useNode);
// we do not have a node that supports the use protocol
} else {
this.showProtocolError(invalidProtocol);
}
this.sendTelemetry({ success: false, invalidProtocol: true, protocol: invalidProtocol });
} catch (e) {
this.showInvalidcURLCommandError();
this.sendTelemetry({ success: false, invalidProtocol: false });
} finally {
this.$store.dispatch('ui/setCurlCommand', { command: this.curlCommand });
}
},
showProtocolErrorWithSupportedNode(protocol: string, node: string): void {
this.$showToast({
title: this.$locale.baseText('importParameter.showError.invalidProtocol1.title', {
interpolate: {
node,
},
}),
message: this.$locale.baseText('importParameter.showError.invalidProtocol.message', {
interpolate: {
protocol: protocol.toUpperCase(),
},
}),
type: 'error',
duration: 0,
});
},
showProtocolError(protocol: string): void {
this.$showToast({
title: this.$locale.baseText('importParameter.showError.invalidProtocol2.title'),
message: this.$locale.baseText('importParameter.showError.invalidProtocol.message', {
interpolate: {
protocol,
},
}),
type: 'error',
duration: 0,
});
},
showInvalidcURLCommandError(): void {
this.$showToast({
title: this.$locale.baseText('importParameter.showError.invalidCurlCommand.title'),
message: this.$locale.baseText('importParameter.showError.invalidCurlCommand.message'),
type: 'error',
duration: 0,
});
},
sendTelemetry(
data: { success: boolean; invalidProtocol: boolean; protocol?: string } = {
success: true,
invalidProtocol: false,
protocol: '',
},
): void {
this.$telemetry.track('User imported curl command', {
success: data.success,
invalidProtocol: data.invalidProtocol,
protocol: data.protocol,
});
},
},
mounted() {
this.curlCommand = this.$store.getters['ui/getCurlCommand'];
setTimeout(() => {
(this.$refs.input as HTMLTextAreaElement).focus();
});
},
});
</script>
<style module lang="scss">
.modalFooter {
justify-content: space-between;
display: flex;
flex-direction: row;
}
.notice {
margin: 0;
}
.container > * {
margin-bottom: var(--spacing-s);
&:last-child {
margin-bottom: 0;
}
}
</style>

View file

@ -0,0 +1,33 @@
<template>
<div :class="$style.importSection">
<n8n-button
type="secondary"
:label="$locale.baseText('importParameter.label')"
size="mini"
@click="onImportCurlClicked"
/>
</div>
</template>
<script lang="ts">
import { IMPORT_CURL_MODAL_KEY } from '@/constants';
import mixins from 'vue-typed-mixins';
import { showMessage } from './mixins/showMessage';
export default mixins(showMessage).extend({
name: 'import-parameter',
methods: {
onImportCurlClicked() {
this.$store.dispatch('ui/openModal', IMPORT_CURL_MODAL_KEY);
},
},
});
</script>
<style module lang="scss">
.importSection {
display: flex;
flex-direction: row-reverse;
margin-top: 10px;
}
</style>

View file

@ -93,6 +93,10 @@
<CommunityPackageInstallModal />
</ModalRoot>
<ModalRoot :name="IMPORT_CURL_MODAL_KEY">
<ImportCurlModal />
</ModalRoot>
<ModalRoot :name="COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY">
<template v-slot="{ modalName, activeId, mode }">
<CommunityPackageManageConfirmModal
@ -128,6 +132,7 @@ import {
WORKFLOW_ACTIVE_MODAL_KEY,
WORKFLOW_OPEN_MODAL_KEY,
WORKFLOW_SETTINGS_MODAL_KEY,
IMPORT_CURL_MODAL_KEY,
} from '@/constants';
import AboutModal from './AboutModal.vue';
@ -150,6 +155,7 @@ import WorkflowOpen from "./WorkflowOpen.vue";
import DeleteUserModal from "./DeleteUserModal.vue";
import ExecutionsList from "./ExecutionsList.vue";
import ActivationModal from "./ActivationModal.vue";
import ImportCurlModal from './ImportCurlModal.vue';
export default Vue.extend({
name: "Modals",
@ -174,6 +180,7 @@ export default Vue.extend({
ValueSurvey,
WorkflowSettings,
WorkflowOpen,
ImportCurlModal,
},
data: () => ({
COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY,
@ -195,6 +202,7 @@ export default Vue.extend({
VALUE_SURVEY_MODAL_KEY,
EXECUTIONS_MODAL_KEY,
WORKFLOW_ACTIVE_MODAL_KEY,
IMPORT_CURL_MODAL_KEY,
}),
});
</script>

View file

@ -3,10 +3,14 @@
'node-settings': true, 'dragging': dragging }" @keydown.stop>
<div :class="$style.header">
<div class="header-side-menu">
<NodeTitle class="node-name" :value="node && node.name" :nodeType="nodeType" @input="nameChanged" :readOnly="isReadOnly"></NodeTitle>
<div
v-if="!isReadOnly"
>
<NodeTitle
class="node-name"
:value="node && node.name"
:nodeType="nodeType"
@input="nameChanged"
:readOnly="isReadOnly"
></NodeTitle>
<div v-if="!isReadOnly">
<NodeExecuteButton
:nodeName="node.name"
:disabled="outputPanelEditMode.enabled"
@ -16,7 +20,12 @@
/>
</div>
</div>
<NodeSettingsTabs v-if="node && nodeValid" v-model="openPanel" :nodeType="nodeType" :sessionId="sessionId" />
<NodeSettingsTabs
v-if="node && nodeValid"
v-model="openPanel"
:nodeType="nodeType"
:sessionId="sessionId"
/>
</div>
<div class="node-is-not-valid" v-if="node && !nodeValid">
<p :class="$style.warningIcon">
@ -57,20 +66,18 @@
</div>
<div class="node-parameters-wrapper" v-if="node && nodeValid">
<div v-show="openPanel === 'params'">
<node-webhooks
:node="node"
:nodeType="nodeType"
/>
<node-webhooks :node="node" :nodeType="nodeType" />
<parameter-input-list
:parameters="parametersNoneSetting"
:hideDelete="true"
:nodeValues="nodeValues" path="parameters" @valueChanged="valueChanged"
:nodeValues="nodeValues"
path="parameters"
@valueChanged="valueChanged"
@activate="onWorkflowActivate"
>
<node-credentials
:node="node"
@credentialSelected="credentialSelected"
/>
<node-credentials :node="node" @credentialSelected="credentialSelected" />
</parameter-input-list>
<div v-if="parametersNoneSetting.length === 0" class="no-parameters">
<n8n-text>
@ -80,17 +87,28 @@
<div v-if="isCustomApiCallSelected(nodeValues)" class="parameter-item parameter-notice">
<n8n-notice
:content="$locale.baseText(
'nodeSettings.useTheHttpRequestNode',
{ interpolate: { nodeTypeDisplayName: nodeType.displayName } }
)"
:content="
$locale.baseText('nodeSettings.useTheHttpRequestNode', {
interpolate: { nodeTypeDisplayName: nodeType.displayName },
})
"
/>
</div>
</div>
<div v-show="openPanel === 'settings'">
<parameter-input-list :parameters="parametersSetting" :nodeValues="nodeValues" path="parameters" @valueChanged="valueChanged" />
<parameter-input-list :parameters="nodeSettings" :hideDelete="true" :nodeValues="nodeValues" path="" @valueChanged="valueChanged" />
<parameter-input-list
:parameters="parametersSetting"
:nodeValues="nodeValues"
path="parameters"
@valueChanged="valueChanged"
/>
<parameter-input-list
:parameters="nodeSettings"
:hideDelete="true"
:nodeValues="nodeValues"
path=""
@valueChanged="valueChanged"
/>
</div>
</div>
</div>
@ -105,16 +123,13 @@ import {
NodeHelpers,
NodeParameterValue,
} from 'n8n-workflow';
import {
INodeUi,
INodeUpdatePropertiesInformation,
IUpdateInformation,
} from '@/Interface';
import { INodeUi, INodeUpdatePropertiesInformation, IUpdateInformation } from '@/Interface';
import {
COMMUNITY_NODES_INSTALLATION_DOCS_URL,
CUSTOM_NODES_DOCS_URL,
MAIN_NODE_PANEL_WIDTH,
IMPORT_CURL_MODAL_KEY,
} from '@/constants';
import NodeTitle from '@/components/NodeTitle.vue';
@ -133,12 +148,7 @@ import mixins from 'vue-typed-mixins';
import NodeExecuteButton from './NodeExecuteButton.vue';
import { isCommunityPackageName } from './helpers';
export default mixins(
externalHooks,
genericHelpers,
nodeHelpers,
)
.extend({
export default mixins(externalHooks, genericHelpers, nodeHelpers).extend({
name: 'NodeSettings',
components: {
NodeTitle,
@ -150,6 +160,9 @@ export default mixins(
NodeExecuteButton,
},
computed: {
isCurlImportModalOpen() {
return this.$store.getters['ui/isModalOpen'](IMPORT_CURL_MODAL_KEY);
},
nodeTypeName(): string {
if (this.nodeType) {
const shortNodeType = this.$locale.shortNodeType(this.nodeType.name);
@ -203,7 +216,7 @@ export default mixins(
return this.nodeType.properties;
},
outputPanelEditMode(): { enabled: boolean; value: string; } {
outputPanelEditMode(): { enabled: boolean; value: string } {
return this.$store.getters['ui/outputPanelEditMode'];
},
isCommunityNode(): boolean {
@ -211,8 +224,7 @@ export default mixins(
},
},
props: {
eventBus: {
},
eventBus: {},
dragging: {
type: Boolean,
},
@ -277,9 +289,7 @@ export default mixins(
default: 3,
displayOptions: {
show: {
retryOnFail: [
true,
],
retryOnFail: [true],
},
},
noDataExpression: true,
@ -296,9 +306,7 @@ export default mixins(
default: 1000,
displayOptions: {
show: {
retryOnFail: [
true,
],
retryOnFail: [true],
},
},
noDataExpression: true,
@ -341,6 +349,28 @@ export default mixins(
node(newNode, oldNode) {
this.setNodeValues();
},
isCurlImportModalOpen(newValue, oldValue) {
if (newValue === false) {
let parameters = this.$store.getters['ui/getHttpNodeParameters'];
if (!parameters) return;
try {
parameters = JSON.parse(parameters) as {
[key: string]: any;
};
//@ts-ignore
this.valueChanged({
node: this.node.name,
name: 'parameters',
value: parameters,
});
this.$store.dispatch('ui/setHttpNodeParameters', { parameters: '' });
} catch (_) {}
}
},
},
methods: {
onWorkflowActivate() {
@ -381,7 +411,9 @@ export default mixins(
if (value === null) {
// Property should be deleted
// @ts-ignore
let tempValue = get(this.nodeValues, nameParts.join('.')) as INodeParameters | NodeParameters[];
let tempValue = get(this.nodeValues, nameParts.join('.')) as
| INodeParameters
| INodeParameters[];
Vue.delete(tempValue as object, lastNamePart as string);
if (isArray === true && (tempValue as INodeParameters[]).length === 0) {
@ -423,14 +455,14 @@ export default mixins(
},
valueChanged(parameterData: IUpdateInformation) {
let newValue: NodeParameterValue;
if (parameterData.hasOwnProperty('value')) {
// New value is given
newValue = parameterData.value;
newValue = parameterData.value as string | number;
} else {
// Get new value from nodeData where it is set already
newValue = get(this.nodeValues, parameterData.name) as NodeParameterValue;
}
// Save the node name before we commit the change because
// we need the old name to rename the node properly
const nodeNameBefore = parameterData.node || this.node.name;
@ -445,17 +477,116 @@ export default mixins(
name: parameterData.name,
};
this.$emit('valueChanged', sendData);
} else if (parameterData.name === 'parameters') {
} else if (parameterData.name.startsWith('parameters.')) {
// A node parameter changed
const nodeType = this.$store.getters['nodeTypes/getNodeType'](node.type, node.typeVersion) as INodeTypeDescription | null;
const nodeType = this.$store.getters['nodeTypes/getNodeType'](
node.type,
node.typeVersion,
) as INodeTypeDescription | null;
if (!nodeType) {
return;
}
// Get only the parameters which are different to the defaults
let nodeParameters = NodeHelpers.getNodeParameters(nodeType.properties, node.parameters, false, false, node);
let nodeParameters = NodeHelpers.getNodeParameters(
nodeType.properties,
node.parameters,
false,
false,
node,
);
const oldNodeParameters = Object.assign({}, nodeParameters);
// Copy the data because it is the data of vuex so make sure that
// we do not edit it directly
nodeParameters = JSON.parse(JSON.stringify(nodeParameters));
for (const parameterName of Object.keys(parameterData.value)) {
//@ts-ignore
newValue = parameterData.value[parameterName];
// Remove the 'parameters.' from the beginning to just have the
// actual parameter name
const parameterPath = parameterName.split('.').slice(1).join('.');
// Check if the path is supposed to change an array and if so get
// the needed data like path and index
const parameterPathArray = parameterPath.match(/(.*)\[(\d+)\]$/);
// Apply the new value
//@ts-ignore
if (parameterData[parameterName] === undefined && parameterPathArray !== null) {
// Delete array item
const path = parameterPathArray[1];
const index = parameterPathArray[2];
const data = get(nodeParameters, path);
if (Array.isArray(data)) {
data.splice(parseInt(index, 10), 1);
Vue.set(nodeParameters as object, path, data);
}
} else {
if (newValue === undefined) {
unset(nodeParameters as object, parameterPath);
} else {
set(nodeParameters as object, parameterPath, newValue);
}
}
this.$externalHooks().run('nodeSettings.valueChanged', {
parameterPath,
newValue,
parameters: this.parameters,
oldNodeParameters,
});
}
// Get the parameters with the now new defaults according to the
// from the user actually defined parameters
nodeParameters = NodeHelpers.getNodeParameters(
nodeType.properties,
nodeParameters as INodeParameters,
true,
false,
node,
);
for (const key of Object.keys(nodeParameters as object)) {
if (nodeParameters && nodeParameters[key] !== null && nodeParameters[key] !== undefined) {
this.setValue(`parameters.${key}`, nodeParameters[key] as string);
}
}
// Update the data in vuex
const updateInformation = {
name: node.name,
value: nodeParameters,
};
this.$store.commit('setNodeParameters', updateInformation);
this.updateNodeParameterIssues(node, nodeType);
this.updateNodeCredentialIssues(node);
} else if (parameterData.name.startsWith('parameters.')) {
// A node parameter changed
const nodeType = this.$store.getters['nodeTypes/getNodeType'](
node.type,
node.typeVersion,
) as INodeTypeDescription | null;
if (!nodeType) {
return;
}
// Get only the parameters which are different to the defaults
let nodeParameters = NodeHelpers.getNodeParameters(
nodeType.properties,
node.parameters,
false,
false,
node,
);
const oldNodeParameters = Object.assign({}, nodeParameters);
// Copy the data because it is the data of vuex so make sure that
@ -491,7 +622,13 @@ export default mixins(
// Get the parameters with the now new defaults according to the
// from the user actually defined parameters
nodeParameters = NodeHelpers.getNodeParameters(nodeType.properties, nodeParameters as INodeParameters, true, false, node);
nodeParameters = NodeHelpers.getNodeParameters(
nodeType.properties,
nodeParameters as INodeParameters,
true,
false,
node,
);
for (const key of Object.keys(nodeParameters as object)) {
if (nodeParameters && nodeParameters[key] !== null && nodeParameters[key] !== undefined) {
@ -507,7 +644,12 @@ export default mixins(
this.$store.commit('setNodeParameters', updateInformation);
this.$externalHooks().run('nodeSettings.valueChanged', { parameterPath, newValue, parameters: this.parameters, oldNodeParameters });
this.$externalHooks().run('nodeSettings.valueChanged', {
parameterPath,
newValue,
parameters: this.parameters,
oldNodeParameters,
});
this.updateNodeParameterIssues(node, nodeType);
this.updateNodeCredentialIssues(node);
@ -599,7 +741,9 @@ export default mixins(
},
onMissingNodeTextClick(event: MouseEvent) {
if ((event.target as Element).localName === 'a') {
this.$telemetry.track('user clicked cnr browse button', { source: 'cnr missing node modal' });
this.$telemetry.track('user clicked cnr browse button', {
source: 'cnr missing node modal',
});
}
},
onMissingNodeLearnMoreLinkClick() {
@ -638,6 +782,7 @@ export default mixins(
</style>
<style lang="scss">
.node-settings {
overflow: hidden;
background-color: var(--color-background-xlight);
@ -697,7 +842,7 @@ export default mixins(
&:before {
display: table;
content: " ";
content: ' ';
position: relative;
box-sizing: border-box;
clear: both;
@ -710,7 +855,6 @@ export default mixins(
.color-reset-button-wrapper {
position: relative;
}
.color-reset-button {
position: absolute;

View file

@ -302,6 +302,7 @@ import {
import CodeEdit from '@/components/CodeEdit.vue';
import CredentialsSelect from '@/components/CredentialsSelect.vue';
import ImportParameter from '@/components/ImportParameter.vue';
import ExpressionEdit from '@/components/ExpressionEdit.vue';
import NodeCredentials from '@/components/NodeCredentials.vue';
import ScopesNotice from '@/components/ScopesNotice.vue';
@ -341,6 +342,7 @@ export default mixins(
ParameterIssues,
ResourceLocator,
TextEdit,
ImportParameter,
},
props: [
'inputSize',

View file

@ -16,6 +16,11 @@
/>
</div>
<import-parameter
v-else-if="parameter.type === 'curlImport' && nodeTypeName === 'n8n-nodes-base.httpRequest' && nodeTypeVersion >= 3"
@valueChanged="valueChanged"
/>
<n8n-notice
v-else-if="parameter.type === 'notice'"
class="parameter-item"
@ -93,7 +98,6 @@
import {
INodeParameters,
INodeProperties,
INodeType,
INodeTypeDescription,
NodeParameterValue,
} from 'n8n-workflow';
@ -104,6 +108,7 @@ import MultipleParameter from '@/components/MultipleParameter.vue';
import { genericHelpers } from '@/components/mixins/genericHelpers';
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
import ParameterInputFull from '@/components/ParameterInputFull.vue';
import ImportParameter from '@/components/ImportParameter.vue';
import { get, set } from 'lodash';
@ -121,6 +126,7 @@ export default mixins(
ParameterInputFull,
FixedCollectionParameter: () => import('./FixedCollectionParameter.vue') as Promise<Component>,
CollectionParameter: () => import('./CollectionParameter.vue') as Promise<Component>,
ImportParameter,
},
props: [
'nodeValues', // INodeParameters
@ -130,6 +136,18 @@ export default mixins(
'indent',
],
computed: {
nodeTypeVersion(): number | null {
if (this.node) {
return this.node.typeVersion;
}
return null;
},
nodeTypeName (): string {
if (this.node) {
return this.node.type;
}
return '';
},
filteredParameters (): INodeProperties[] {
return this.parameters.filter((parameter: INodeProperties) => this.displayNodeParameter(parameter));
},

View file

@ -41,6 +41,7 @@ export const WORKFLOW_ACTIVE_MODAL_KEY = 'activation';
export const ONBOARDING_CALL_SIGNUP_MODAL_KEY = 'onboardingCallSignup';
export const COMMUNITY_PACKAGE_INSTALL_MODAL_KEY = 'communityPackageInstall';
export const COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY = 'communityPackageManageConfirm';
export const IMPORT_CURL_MODAL_KEY = 'importCurl';
export const COMMUNITY_PACKAGE_MANAGE_ACTIONS = {
UNINSTALL: 'uninstall',
@ -350,3 +351,36 @@ export enum EnterpriseEditionFeature {
Sharing = 'sharing',
}
export const MAIN_NODE_PANEL_WIDTH = 360;
export const CURL_IMPORT_NOT_SUPPORTED_PROTOCOLS = [
'ftp',
'ftps',
'dict',
'imap',
'imaps',
'ldap',
'ldaps',
'mqtt',
'pop',
'pop3s',
'rtmp',
'rtsp',
'scp',
'sftp',
'smb',
'smbs',
'smtp',
'smtps',
'telnet',
'tftp',
];
export const CURL_IMPORT_NODES_PROTOCOLS: { [key: string]: string } = {
'ftp': 'FTP',
'ftps': 'FTP',
'ldap': 'LDAP',
'ldaps': 'LDAP',
'mqtt': 'MQTT',
'imap': 'IMAP',
'imaps': 'IMAP',
};

View file

@ -183,6 +183,7 @@ const module: Module<ISettingsState, IRootState> = {
return e;
}
},
async submitValueSurvey(context: ActionContext<ISettingsState, IRootState>, params: IN8nValueSurveyData) {
try {
const instanceId = context.state.settings.instanceId;

View file

@ -1,3 +1,4 @@
import { getCurlToJson } from '@/api/curlHelper';
import { applyForOnboardingCall, fetchNextOnboardingPrompt, submitEmailOnSignup } from '@/api/workflow-webhooks';
import {
ABOUT_MODAL_KEY,
@ -22,6 +23,7 @@ import {
ONBOARDING_CALL_SIGNUP_MODAL_KEY,
FAKE_DOOR_FEATURES,
COMMUNITY_PACKAGE_MANAGE_ACTIONS,
IMPORT_CURL_MODAL_KEY,
} from '@/constants';
import Vue from 'vue';
import { ActionContext, Module } from 'vuex';
@ -100,6 +102,11 @@ const module: Module<IUiState, IRootState> = {
mode: '',
activeId: null,
},
[IMPORT_CURL_MODAL_KEY]: {
open: false,
curlCommand: '',
httpNodeParameters: '',
},
},
modalStack: [],
sidebarMenuCollapsed: true,
@ -167,6 +174,12 @@ const module: Module<IUiState, IRootState> = {
isVersionsOpen: (state: IUiState) => {
return state.modals[VERSIONS_MODAL_KEY].open;
},
getCurlCommand: (state: IUiState) => {
return state.modals[IMPORT_CURL_MODAL_KEY].curlCommand;
},
getHttpNodeParameters: (state: IUiState) => {
return state.modals[IMPORT_CURL_MODAL_KEY].httpNodeParameters;
},
isModalOpen: (state: IUiState) => {
return (name: string) => state.modals[name].open;
},
@ -231,6 +244,14 @@ const module: Module<IUiState, IRootState> = {
const { name, id } = params;
Vue.set(state.modals[name], 'activeId', id);
},
setCurlCommand: (state: IUiState, params: {name: string, command: string}) => {
const { name, command } = params;
Vue.set(state.modals[name], 'curlCommand', command);
},
setHttpNodeParameters: (state: IUiState, params: {name: string, parameters: string}) => {
const { name, parameters } = params;
Vue.set(state.modals[name], 'httpNodeParameters', parameters);
},
openModal: (state: IUiState, name: string) => {
Vue.set(state.modals[name], 'open', true);
state.modalStack = [name].concat(state.modalStack);
@ -323,6 +344,17 @@ const module: Module<IUiState, IRootState> = {
context.commit('setMode', { name: CREDENTIAL_EDIT_MODAL_KEY, mode: 'edit' });
context.commit('openModal', CREDENTIAL_EDIT_MODAL_KEY);
},
setCurlCommand: async (context: ActionContext<IUiState, IRootState>, { command }: {command: string}) => {
context.commit('setCurlCommand', { name: IMPORT_CURL_MODAL_KEY, command });
},
setHttpNodeParameters: async (context: ActionContext<IUiState, IRootState>, { parameters }) => {
context.commit('setHttpNodeParameters', { name: IMPORT_CURL_MODAL_KEY, parameters });
},
openExisitngCredential: async (context: ActionContext<IUiState, IRootState>, { id }: {id: string}) => {
context.commit('setActiveId', { name: CREDENTIAL_EDIT_MODAL_KEY, id });
context.commit('setMode', { name: CREDENTIAL_EDIT_MODAL_KEY, mode: 'edit' });
context.commit('openModal', CREDENTIAL_EDIT_MODAL_KEY);
},
openNewCredential: async (context: ActionContext<IUiState, IRootState>, { type }: {type: string}) => {
context.commit('setActiveId', { name: CREDENTIAL_EDIT_MODAL_KEY, id: type });
context.commit('setMode', { name: CREDENTIAL_EDIT_MODAL_KEY, mode: 'new' });
@ -354,6 +386,9 @@ const module: Module<IUiState, IRootState> = {
context.commit('setMode', { name: COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY, mode: COMMUNITY_PACKAGE_MANAGE_ACTIONS.UPDATE });
context.commit('openModal', COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY);
},
async getCurlToJson(context: ActionContext<IUiState, IRootState>, curlCommand) {
return await getCurlToJson(context.rootGetters['getRestApiContext'], curlCommand);
},
},
};

View file

@ -1114,5 +1114,16 @@
"workflowSettings.showMessage.saveSettings.title": "Workflow settings saved",
"workflowSettings.timeoutAfter": "Timeout After",
"workflowSettings.timeoutWorkflow": "Timeout Workflow",
"workflowSettings.timezone": "Timezone"
"workflowSettings.timezone": "Timezone",
"importCurlModal.title": "Import cURL command",
"importCurlModal.input.label": "cURL Command",
"importCurlModal.input.placeholder": "Paste the cURL command here",
"ImportCurlModal.notice.content": "This will overwrite any changes you have already made",
"importCurlModal.button.label": "Import",
"importParameter.label": "Import cURL",
"importParameter.showError.invalidCurlCommand.title": "Couldnt import cURL command",
"importParameter.showError.invalidCurlCommand.message": "This command is in an unsupported format",
"importParameter.showError.invalidProtocol1.title": "Use the {node} node",
"importParameter.showError.invalidProtocol2.title": "Invalid Protocol",
"importParameter.showError.invalidProtocol.message": "The HTTP node doesnt support {protocol} requests"
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -941,7 +941,8 @@ export type NodePropertyTypes =
| 'options'
| 'string'
| 'credentialsSelect'
| 'resourceLocator';
| 'resourceLocator'
| 'curlImport';
export type CodeAutocompleteTypes = 'function' | 'functionItem';