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": ">=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": { "node_modules/@dabh/diagnostics": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz",
@ -9038,6 +9089,11 @@
"vite": "^2.1.5 || ^3.0.5" "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": { "node_modules/abab": {
"version": "2.0.6", "version": "2.0.6",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
@ -9808,8 +9864,7 @@
"node_modules/asap": { "node_modules/asap": {
"version": "2.0.6", "version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="
"dev": true
}, },
"node_modules/asn1": { "node_modules/asn1": {
"version": "0.2.6", "version": "0.2.6",
@ -13788,6 +13843,59 @@
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
"dev": true "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": { "node_modules/currently-unhandled": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
@ -13958,7 +14066,6 @@
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
"integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==", "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==",
"dev": true,
"engines": { "engines": {
"node": ">=0.10" "node": ">=0.10"
} }
@ -17037,6 +17144,14 @@
"node": ">=8" "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": { "node_modules/finalhandler": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
@ -28003,6 +28118,38 @@
"node": ">=0.10.0" "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": { "node_modules/nwsapi": {
"version": "2.2.2", "version": "2.2.2",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz",
@ -33584,6 +33731,14 @@
"node": ">=4.5" "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": { "node_modules/split-string": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
@ -34265,6 +34420,15 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/string.prototype.trimend": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz",
@ -41556,6 +41720,7 @@
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"crypto-js": "~4.1.1", "crypto-js": "~4.1.1",
"csrf": "^3.1.0", "csrf": "^3.1.0",
"curlconverter": "^3.0.0",
"dotenv": "^8.0.0", "dotenv": "^8.0.0",
"express": "^4.16.4", "express": "^4.16.4",
"express-openapi-validator": "^4.13.6", "express-openapi-validator": "^4.13.6",
@ -44294,6 +44459,47 @@
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
"integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==" "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": { "@dabh/diagnostics": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz",
@ -50563,6 +50769,11 @@
"integrity": "sha512-HlhVMhrOpBHlkkdSm5wQ6YN65ZqdF9YqP7HS1at6NjQgEOIhBiP7tRZ+dYM3ymztKAX0ovhyKCY9ZnQ/GMK0Qg==", "integrity": "sha512-HlhVMhrOpBHlkkdSm5wQ6YN65ZqdF9YqP7HS1at6NjQgEOIhBiP7tRZ+dYM3ymztKAX0ovhyKCY9ZnQ/GMK0Qg==",
"dev": true "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": { "abab": {
"version": "2.0.6", "version": "2.0.6",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
@ -51132,8 +51343,7 @@
"asap": { "asap": {
"version": "2.0.6", "version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="
"dev": true
}, },
"asn1": { "asn1": {
"version": "0.2.6", "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": { "currently-unhandled": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
@ -54463,8 +54710,7 @@
"decode-uri-component": { "decode-uri-component": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
"integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==", "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og=="
"dev": true
}, },
"dedent": { "dedent": {
"version": "0.7.0", "version": "0.7.0",
@ -56777,6 +57023,11 @@
"to-regex-range": "^5.0.1" "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": { "finalhandler": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
@ -64791,6 +65042,7 @@
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"crypto-js": "~4.1.1", "crypto-js": "~4.1.1",
"csrf": "^3.1.0", "csrf": "^3.1.0",
"curlconverter": "^3.0.0",
"dotenv": "^8.0.0", "dotenv": "^8.0.0",
"express": "^4.16.4", "express": "^4.16.4",
"express-openapi-validator": "^4.13.6", "express-openapi-validator": "^4.13.6",
@ -66266,6 +66518,23 @@
"integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==",
"dev": true "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": { "nwsapi": {
"version": "2.2.2", "version": "2.2.2",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz", "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", "resolved": "https://registry.npmjs.org/spex/-/spex-3.2.0.tgz",
"integrity": "sha512-9srjJM7NaymrpwMHvSmpDeIK5GoRMX/Tq0E8aOlDPS54dDnDUIp30DrP9SphMPEETDLzEM9+4qo+KipmbtPecg==" "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": { "split-string": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
@ -71205,6 +71479,15 @@
"es-abstract": "^1.19.1" "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": { "string.prototype.trimend": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", "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", "cookie-parser": "^1.4.6",
"crypto-js": "~4.1.1", "crypto-js": "~4.1.1",
"csrf": "^3.1.0", "csrf": "^3.1.0",
"curlconverter": "^3.0.0",
"dotenv": "^8.0.0", "dotenv": "^8.0.0",
"express": "^4.16.4", "express": "^4.16.4",
"express-openapi-validator": "^4.13.6", "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; 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 // eslint-disable-next-line import/no-extraneous-dependencies
import axios, { AxiosRequestConfig } from 'axios'; import axios, { AxiosRequestConfig } from 'axios';
import clientOAuth1, { RequestOptions } from 'oauth-1.0a'; import clientOAuth1, { RequestOptions } from 'oauth-1.0a';
import curlconverter from 'curlconverter';
// IMPORTANT! Do not switch to anther bcrypt library unless really necessary and // IMPORTANT! Do not switch to anther bcrypt library unless really necessary and
// tested with all possible systems like Windows, Alpine on ARM, FreeBSD, ... // tested with all possible systems like Windows, Alpine on ARM, FreeBSD, ...
import { compare } from 'bcryptjs'; import { compare } from 'bcryptjs';
@ -96,6 +97,7 @@ import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES } from './constants';
import { credentialsController } from './credentials/credentials.controller'; import { credentialsController } from './credentials/credentials.controller';
import { oauth2CredentialController } from './credentials/oauth2Credential.api'; import { oauth2CredentialController } from './credentials/oauth2Credential.api';
import type { import type {
CurlHelper,
ExecutionRequest, ExecutionRequest,
NodeListSearchRequest, NodeListSearchRequest,
NodeParameterOptionsRequest, NodeParameterOptionsRequest,
@ -151,6 +153,8 @@ import {
import glob from 'fast-glob'; import glob from 'fast-glob';
import { ResponseError } from './ResponseHelper'; import { ResponseError } from './ResponseHelper';
import { toHttpNodeParameters } from './CurlConverterHelper';
require('body-parser-xml')(bodyParser); require('body-parser-xml')(bodyParser);
const exec = promisify(callbackExec); 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 // Credential-Types
// ---------------------------------------- // ----------------------------------------

View file

@ -327,3 +327,11 @@ export declare namespace NodeRequest {
type Update = Post; 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 { export interface IUpdateInformation {
name: string; name: string;
key: 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; node?: string;
oldValue?: string | number; oldValue?: string | number;
} }
@ -924,6 +924,8 @@ export interface IModalState {
open: boolean; open: boolean;
mode?: string | null; mode?: string | null;
activeId?: string | null; activeId?: string | null;
curlCommand?: string;
httpNodeParameters?: string;
} }
export type IRunDataDisplayMode = 'table' | 'json' | 'binary'; 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 /> <CommunityPackageInstallModal />
</ModalRoot> </ModalRoot>
<ModalRoot :name="IMPORT_CURL_MODAL_KEY">
<ImportCurlModal />
</ModalRoot>
<ModalRoot :name="COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY"> <ModalRoot :name="COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY">
<template v-slot="{ modalName, activeId, mode }"> <template v-slot="{ modalName, activeId, mode }">
<CommunityPackageManageConfirmModal <CommunityPackageManageConfirmModal
@ -128,6 +132,7 @@ import {
WORKFLOW_ACTIVE_MODAL_KEY, WORKFLOW_ACTIVE_MODAL_KEY,
WORKFLOW_OPEN_MODAL_KEY, WORKFLOW_OPEN_MODAL_KEY,
WORKFLOW_SETTINGS_MODAL_KEY, WORKFLOW_SETTINGS_MODAL_KEY,
IMPORT_CURL_MODAL_KEY,
} from '@/constants'; } from '@/constants';
import AboutModal from './AboutModal.vue'; import AboutModal from './AboutModal.vue';
@ -150,6 +155,7 @@ import WorkflowOpen from "./WorkflowOpen.vue";
import DeleteUserModal from "./DeleteUserModal.vue"; import DeleteUserModal from "./DeleteUserModal.vue";
import ExecutionsList from "./ExecutionsList.vue"; import ExecutionsList from "./ExecutionsList.vue";
import ActivationModal from "./ActivationModal.vue"; import ActivationModal from "./ActivationModal.vue";
import ImportCurlModal from './ImportCurlModal.vue';
export default Vue.extend({ export default Vue.extend({
name: "Modals", name: "Modals",
@ -174,6 +180,7 @@ export default Vue.extend({
ValueSurvey, ValueSurvey,
WorkflowSettings, WorkflowSettings,
WorkflowOpen, WorkflowOpen,
ImportCurlModal,
}, },
data: () => ({ data: () => ({
COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY, COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY,
@ -195,6 +202,7 @@ export default Vue.extend({
VALUE_SURVEY_MODAL_KEY, VALUE_SURVEY_MODAL_KEY,
EXECUTIONS_MODAL_KEY, EXECUTIONS_MODAL_KEY,
WORKFLOW_ACTIVE_MODAL_KEY, WORKFLOW_ACTIVE_MODAL_KEY,
IMPORT_CURL_MODAL_KEY,
}), }),
}); });
</script> </script>

File diff suppressed because it is too large Load diff

View file

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

View file

@ -16,6 +16,11 @@
/> />
</div> </div>
<import-parameter
v-else-if="parameter.type === 'curlImport' && nodeTypeName === 'n8n-nodes-base.httpRequest' && nodeTypeVersion >= 3"
@valueChanged="valueChanged"
/>
<n8n-notice <n8n-notice
v-else-if="parameter.type === 'notice'" v-else-if="parameter.type === 'notice'"
class="parameter-item" class="parameter-item"
@ -93,7 +98,6 @@
import { import {
INodeParameters, INodeParameters,
INodeProperties, INodeProperties,
INodeType,
INodeTypeDescription, INodeTypeDescription,
NodeParameterValue, NodeParameterValue,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -104,6 +108,7 @@ import MultipleParameter from '@/components/MultipleParameter.vue';
import { genericHelpers } from '@/components/mixins/genericHelpers'; import { genericHelpers } from '@/components/mixins/genericHelpers';
import { workflowHelpers } from '@/components/mixins/workflowHelpers'; import { workflowHelpers } from '@/components/mixins/workflowHelpers';
import ParameterInputFull from '@/components/ParameterInputFull.vue'; import ParameterInputFull from '@/components/ParameterInputFull.vue';
import ImportParameter from '@/components/ImportParameter.vue';
import { get, set } from 'lodash'; import { get, set } from 'lodash';
@ -121,6 +126,7 @@ export default mixins(
ParameterInputFull, ParameterInputFull,
FixedCollectionParameter: () => import('./FixedCollectionParameter.vue') as Promise<Component>, FixedCollectionParameter: () => import('./FixedCollectionParameter.vue') as Promise<Component>,
CollectionParameter: () => import('./CollectionParameter.vue') as Promise<Component>, CollectionParameter: () => import('./CollectionParameter.vue') as Promise<Component>,
ImportParameter,
}, },
props: [ props: [
'nodeValues', // INodeParameters 'nodeValues', // INodeParameters
@ -130,6 +136,18 @@ export default mixins(
'indent', 'indent',
], ],
computed: { 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[] { filteredParameters (): INodeProperties[] {
return this.parameters.filter((parameter: INodeProperties) => this.displayNodeParameter(parameter)); 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 ONBOARDING_CALL_SIGNUP_MODAL_KEY = 'onboardingCallSignup';
export const COMMUNITY_PACKAGE_INSTALL_MODAL_KEY = 'communityPackageInstall'; export const COMMUNITY_PACKAGE_INSTALL_MODAL_KEY = 'communityPackageInstall';
export const COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY = 'communityPackageManageConfirm'; export const COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY = 'communityPackageManageConfirm';
export const IMPORT_CURL_MODAL_KEY = 'importCurl';
export const COMMUNITY_PACKAGE_MANAGE_ACTIONS = { export const COMMUNITY_PACKAGE_MANAGE_ACTIONS = {
UNINSTALL: 'uninstall', UNINSTALL: 'uninstall',
@ -350,3 +351,36 @@ export enum EnterpriseEditionFeature {
Sharing = 'sharing', Sharing = 'sharing',
} }
export const MAIN_NODE_PANEL_WIDTH = 360; 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

@ -12,7 +12,7 @@ import Vue from 'vue';
import {CONTACT_PROMPT_MODAL_KEY, EnterpriseEditionFeature, VALUE_SURVEY_MODAL_KEY} from '@/constants'; import {CONTACT_PROMPT_MODAL_KEY, EnterpriseEditionFeature, VALUE_SURVEY_MODAL_KEY} from '@/constants';
import { ITelemetrySettings } from 'n8n-workflow'; import { ITelemetrySettings } from 'n8n-workflow';
import { testHealthEndpoint } from '@/api/templates'; import { testHealthEndpoint } from '@/api/templates';
import {createApiKey, deleteApiKey, getApiKey} from "@/api/api-keys"; import {createApiKey, deleteApiKey, getApiKey } from "@/api/api-keys";
const module: Module<ISettingsState, IRootState> = { const module: Module<ISettingsState, IRootState> = {
namespaced: true, namespaced: true,
@ -183,6 +183,7 @@ const module: Module<ISettingsState, IRootState> = {
return e; return e;
} }
}, },
async submitValueSurvey(context: ActionContext<ISettingsState, IRootState>, params: IN8nValueSurveyData) { async submitValueSurvey(context: ActionContext<ISettingsState, IRootState>, params: IN8nValueSurveyData) {
try { try {
const instanceId = context.state.settings.instanceId; 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 { applyForOnboardingCall, fetchNextOnboardingPrompt, submitEmailOnSignup } from '@/api/workflow-webhooks';
import { import {
ABOUT_MODAL_KEY, ABOUT_MODAL_KEY,
@ -22,6 +23,7 @@ import {
ONBOARDING_CALL_SIGNUP_MODAL_KEY, ONBOARDING_CALL_SIGNUP_MODAL_KEY,
FAKE_DOOR_FEATURES, FAKE_DOOR_FEATURES,
COMMUNITY_PACKAGE_MANAGE_ACTIONS, COMMUNITY_PACKAGE_MANAGE_ACTIONS,
IMPORT_CURL_MODAL_KEY,
} from '@/constants'; } from '@/constants';
import Vue from 'vue'; import Vue from 'vue';
import { ActionContext, Module } from 'vuex'; import { ActionContext, Module } from 'vuex';
@ -100,6 +102,11 @@ const module: Module<IUiState, IRootState> = {
mode: '', mode: '',
activeId: null, activeId: null,
}, },
[IMPORT_CURL_MODAL_KEY]: {
open: false,
curlCommand: '',
httpNodeParameters: '',
},
}, },
modalStack: [], modalStack: [],
sidebarMenuCollapsed: true, sidebarMenuCollapsed: true,
@ -167,6 +174,12 @@ const module: Module<IUiState, IRootState> = {
isVersionsOpen: (state: IUiState) => { isVersionsOpen: (state: IUiState) => {
return state.modals[VERSIONS_MODAL_KEY].open; 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) => { isModalOpen: (state: IUiState) => {
return (name: string) => state.modals[name].open; return (name: string) => state.modals[name].open;
}, },
@ -231,6 +244,14 @@ const module: Module<IUiState, IRootState> = {
const { name, id } = params; const { name, id } = params;
Vue.set(state.modals[name], 'activeId', id); 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) => { openModal: (state: IUiState, name: string) => {
Vue.set(state.modals[name], 'open', true); Vue.set(state.modals[name], 'open', true);
state.modalStack = [name].concat(state.modalStack); 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('setMode', { name: CREDENTIAL_EDIT_MODAL_KEY, mode: 'edit' });
context.commit('openModal', CREDENTIAL_EDIT_MODAL_KEY); 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}) => { openNewCredential: async (context: ActionContext<IUiState, IRootState>, { type }: {type: string}) => {
context.commit('setActiveId', { name: CREDENTIAL_EDIT_MODAL_KEY, id: type }); context.commit('setActiveId', { name: CREDENTIAL_EDIT_MODAL_KEY, id: type });
context.commit('setMode', { name: CREDENTIAL_EDIT_MODAL_KEY, mode: 'new' }); 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('setMode', { name: COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY, mode: COMMUNITY_PACKAGE_MANAGE_ACTIONS.UPDATE });
context.commit('openModal', COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY); 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.showMessage.saveSettings.title": "Workflow settings saved",
"workflowSettings.timeoutAfter": "Timeout After", "workflowSettings.timeoutAfter": "Timeout After",
"workflowSettings.timeoutWorkflow": "Timeout Workflow", "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' | 'options'
| 'string' | 'string'
| 'credentialsSelect' | 'credentialsSelect'
| 'resourceLocator'; | 'resourceLocator'
| 'curlImport';
export type CodeAutocompleteTypes = 'function' | 'functionItem'; export type CodeAutocompleteTypes = 'function' | 'functionItem';