From 4dc5acb5b60b90f107b29e1a131fb6884eb2004a Mon Sep 17 00:00:00 2001 From: Vladislav Tupikin Date: Fri, 10 Jan 2025 02:11:11 +0300 Subject: [PATCH] Release: v4.0.6 (#349) * #347: resolve file upload issues with ReadableStream and Readable in Node.js (#348) * Changelog added for v4.0.6 --- CHANGELOG.md | 12 + package-lock.json | 274 +++++++++--------- package.json | 11 +- .../parameters/attachTemporaryFile.ts | 4 +- src/serviceDesk/serviceDesk.ts | 98 ++++++- src/version2/issueAttachments.ts | 106 +++++-- src/version2/parameters/addAttachment.ts | 4 +- src/version3/issueAttachments.ts | 94 +++++- src/version3/parameters/addAttachment.ts | 4 +- .../version2/issueAttachments.test.ts | 37 ++- .../version3/issueAttachments.test.ts | 35 +++ 11 files changed, 501 insertions(+), 178 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 852d155d4..fc5dbd251 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,17 @@ # Jira.js changelog +### 4.0.6 + +- **#347:** Fixed an issue with adding attachments of type `Readable` or `ReadableStream` (e.g., `fs.createReadStream`). Thanks to [Lunatic174](https://github.com/Lunatic174) for [reporting the issue](https://github.com/MrRefactoring/jira.js/issues/347). + +--- + ### 4.0.5 - **#344:** Replaced the `mime-types` library with `mime` to ensure browser compatibility, as `mime-types` relies on the `path` module from Node.js. Thanks to [kang](https://github.com/kang8) for [reporting the issue](https://github.com/MrRefactoring/jira.js/issues/344) and proposing the fix. +--- + ### 4.0.4 - **#320:** Resolved a tree-shaking issue where importing a single client would still include all clients in the output bundle when using bundlers. Now, only the required client code is included. Thanks to [Nao Yonashiro](https://github.com/orisano) for [reporting the issue](https://github.com/MrRefactoring/jira.js/issues/320) and proposing a fix. @@ -46,12 +54,16 @@ console.log(attachment[0].mimeType); // Will be 'application/typescript' ``` +--- + ### 4.0.3 - **Bug Fix:** Fixed an issue with the `Users.createUser` method by adding the required `products` property. Thanks to [Appelberg-s](https://github.com/Appelberg-s) for the [fix](https://github.com/MrRefactoring/jira.js/commit/362918093c20036049db334743e2a0f5f41cbcd4#diff-6960050bc2a3d9ffad9eb5e307145969dc4a38eb5434eebf39da545fd18e01b7R12). - **Documentation Update:** Corrected an error in `README.md`. Thanks to [Maurice de Bruyn](https://github.com/ueberBrot) for the [contribution](https://github.com/MrRefactoring/jira.js/commit/fb6151e1a0c7953b9447aaaf99caea5c2f93bb96). - **Dependencies:** Updated all dependencies to their latest versions. +--- + ### 4.0.2 - `getAllProjects` in README and examples replaced to `searchProjects`. Thanks to [Alexander Pivovarov](https://github.com/bladerunner2020) for reporting [the issue](https://github.com/MrRefactoring/jira.js/issues/323). diff --git a/package-lock.json b/package-lock.json index 185c7ba66..73dc08915 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "jira.js", - "version": "4.0.5", + "version": "4.0.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "jira.js", - "version": "4.0.5", + "version": "4.0.6", "license": "MIT", "dependencies": { "axios": "^1.7.9", @@ -17,8 +17,8 @@ "devDependencies": { "@types/node": "^18.19.70", "@types/sinon": "^17.0.3", - "@typescript-eslint/eslint-plugin": "^8.19.0", - "@typescript-eslint/parser": "^8.19.0", + "@typescript-eslint/eslint-plugin": "^8.19.1", + "@typescript-eslint/parser": "^8.19.1", "dotenv": "^16.4.7", "eslint": "^8.57.1", "eslint-config-airbnb-base": "^15.0.0", @@ -28,7 +28,7 @@ "prettier-plugin-jsdoc": "^1.3.2", "sinon": "^18.0.1", "typedoc": "^0.27.6", - "typescript": "^5.7.2", + "typescript": "^5.7.3", "vite-tsconfig-paths": "^5.1.4", "vitest": "^2.1.8" } @@ -641,9 +641,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.30.0.tgz", - "integrity": "sha512-qFcFto9figFLz2g25DxJ1WWL9+c91fTxnGuwhToCl8BaqDsDYMl/kOnBXAyAqkkzAWimYMSWNPWEjt+ADAHuoQ==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.30.1.tgz", + "integrity": "sha512-pSWY+EVt3rJ9fQ3IqlrEUtXh3cGqGtPDH1FQlNZehO2yYxCHEX1SPsz1M//NXwYfbTlcKr9WObLnJX9FsS9K1Q==", "cpu": [ "arm" ], @@ -655,9 +655,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.30.0.tgz", - "integrity": "sha512-vqrQdusvVl7dthqNjWCL043qelBK+gv9v3ZiqdxgaJvmZyIAAXMjeGVSqZynKq69T7062T5VrVTuikKSAAVP6A==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.30.1.tgz", + "integrity": "sha512-/NA2qXxE3D/BRjOJM8wQblmArQq1YoBVJjrjoTSBS09jgUisq7bqxNHJ8kjCHeV21W/9WDGwJEWSN0KQ2mtD/w==", "cpu": [ "arm64" ], @@ -669,9 +669,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.30.0.tgz", - "integrity": "sha512-617pd92LhdA9+wpixnzsyhVft3szYiN16aNUMzVkf2N+yAk8UXY226Bfp36LvxYTUt7MO/ycqGFjQgJ0wlMaWQ==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.30.1.tgz", + "integrity": "sha512-r7FQIXD7gB0WJ5mokTUgUWPl0eYIH0wnxqeSAhuIwvnnpjdVB8cRRClyKLQr7lgzjctkbp5KmswWszlwYln03Q==", "cpu": [ "arm64" ], @@ -683,9 +683,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.30.0.tgz", - "integrity": "sha512-Y3b4oDoaEhCypg8ajPqigKDcpi5ZZovemQl9Edpem0uNv6UUjXv7iySBpGIUTSs2ovWOzYpfw9EbFJXF/fJHWw==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.30.1.tgz", + "integrity": "sha512-x78BavIwSH6sqfP2xeI1hd1GpHL8J4W2BXcVM/5KYKoAD3nNsfitQhvWSw+TFtQTLZ9OmlF+FEInEHyubut2OA==", "cpu": [ "x64" ], @@ -697,9 +697,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.30.0.tgz", - "integrity": "sha512-3REQJ4f90sFIBfa0BUokiCdrV/E4uIjhkWe1bMgCkhFXbf4D8YN6C4zwJL881GM818qVYE9BO3dGwjKhpo2ABA==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.30.1.tgz", + "integrity": "sha512-HYTlUAjbO1z8ywxsDFWADfTRfTIIy/oUlfIDmlHYmjUP2QRDTzBuWXc9O4CXM+bo9qfiCclmHk1x4ogBjOUpUQ==", "cpu": [ "arm64" ], @@ -711,9 +711,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.30.0.tgz", - "integrity": "sha512-ZtY3Y8icbe3Cc+uQicsXG5L+CRGUfLZjW6j2gn5ikpltt3Whqjfo5mkyZ86UiuHF9Q3ZsaQeW7YswlHnN+lAcg==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.30.1.tgz", + "integrity": "sha512-1MEdGqogQLccphhX5myCJqeGNYTNcmTyaic9S7CG3JhwuIByJ7J05vGbZxsizQthP1xpVx7kd3o31eOogfEirw==", "cpu": [ "x64" ], @@ -725,9 +725,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.30.0.tgz", - "integrity": "sha512-bsPGGzfiHXMhQGuFGpmo2PyTwcrh2otL6ycSZAFTESviUoBOuxF7iBbAL5IJXc/69peXl5rAtbewBFeASZ9O0g==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.30.1.tgz", + "integrity": "sha512-PaMRNBSqCx7K3Wc9QZkFx5+CX27WFpAMxJNiYGAXfmMIKC7jstlr32UhTgK6T07OtqR+wYlWm9IxzennjnvdJg==", "cpu": [ "arm" ], @@ -739,9 +739,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.30.0.tgz", - "integrity": "sha512-kvyIECEhs2DrrdfQf++maCWJIQ974EI4txlz1nNSBaCdtf7i5Xf1AQCEJWOC5rEBisdaMFFnOWNLYt7KpFqy5A==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.30.1.tgz", + "integrity": "sha512-B8Rcyj9AV7ZlEFqvB5BubG5iO6ANDsRKlhIxySXcF1axXYUyqwBok+XZPgIYGBgs7LDXfWfifxhw0Ik57T0Yug==", "cpu": [ "arm" ], @@ -753,9 +753,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.30.0.tgz", - "integrity": "sha512-CFE7zDNrokaotXu+shwIrmWrFxllg79vciH4E/zeK7NitVuWEaXRzS0mFfFvyhZfn8WfVOG/1E9u8/DFEgK7WQ==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.30.1.tgz", + "integrity": "sha512-hqVyueGxAj3cBKrAI4aFHLV+h0Lv5VgWZs9CUGqr1z0fZtlADVV1YPOij6AhcK5An33EXaxnDLmJdQikcn5NEw==", "cpu": [ "arm64" ], @@ -767,9 +767,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.30.0.tgz", - "integrity": "sha512-MctNTBlvMcIBP0t8lV/NXiUwFg9oK5F79CxLU+a3xgrdJjfBLVIEHSAjQ9+ipofN2GKaMLnFFXLltg1HEEPaGQ==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.30.1.tgz", + "integrity": "sha512-i4Ab2vnvS1AE1PyOIGp2kXni69gU2DAUVt6FSXeIqUCPIR3ZlheMW3oP2JkukDfu3PsexYRbOiJrY+yVNSk9oA==", "cpu": [ "arm64" ], @@ -781,9 +781,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.30.0.tgz", - "integrity": "sha512-fBpoYwLEPivL3q368+gwn4qnYnr7GVwM6NnMo8rJ4wb0p/Y5lg88vQRRP077gf+tc25akuqd+1Sxbn9meODhwA==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.30.1.tgz", + "integrity": "sha512-fARcF5g296snX0oLGkVxPmysetwUk2zmHcca+e9ObOovBR++9ZPOhqFUM61UUZ2EYpXVPN1redgqVoBB34nTpQ==", "cpu": [ "loong64" ], @@ -795,9 +795,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.30.0.tgz", - "integrity": "sha512-1hiHPV6dUaqIMXrIjN+vgJqtfkLpqHS1Xsg0oUfUVD98xGp1wX89PIXgDF2DWra1nxAd8dfE0Dk59MyeKaBVAw==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.30.1.tgz", + "integrity": "sha512-GLrZraoO3wVT4uFXh67ElpwQY0DIygxdv0BNW9Hkm3X34wu+BkqrDrkcsIapAY+N2ATEbvak0XQ9gxZtCIA5Rw==", "cpu": [ "ppc64" ], @@ -809,9 +809,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.30.0.tgz", - "integrity": "sha512-U0xcC80SMpEbvvLw92emHrNjlS3OXjAM0aVzlWfar6PR0ODWCTQtKeeB+tlAPGfZQXicv1SpWwRz9Hyzq3Jx3g==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.30.1.tgz", + "integrity": "sha512-0WKLaAUUHKBtll0wvOmh6yh3S0wSU9+yas923JIChfxOaaBarmb/lBKPF0w/+jTVozFnOXJeRGZ8NvOxvk/jcw==", "cpu": [ "riscv64" ], @@ -823,9 +823,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.30.0.tgz", - "integrity": "sha512-VU/P/IODrNPasgZDLIFJmMiLGez+BN11DQWfTVlViJVabyF3JaeaJkP6teI8760f18BMGCQOW9gOmuzFaI1pUw==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.30.1.tgz", + "integrity": "sha512-GWFs97Ruxo5Bt+cvVTQkOJ6TIx0xJDD/bMAOXWJg8TCSTEK8RnFeOeiFTxKniTc4vMIaWvCplMAFBt9miGxgkA==", "cpu": [ "s390x" ], @@ -837,9 +837,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.30.0.tgz", - "integrity": "sha512-laQVRvdbKmjXuFA3ZiZj7+U24FcmoPlXEi2OyLfbpY2MW1oxLt9Au8q9eHd0x6Pw/Kw4oe9gwVXWwIf2PVqblg==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.30.1.tgz", + "integrity": "sha512-UtgGb7QGgXDIO+tqqJ5oZRGHsDLO8SlpE4MhqpY9Llpzi5rJMvrK6ZGhsRCST2abZdBqIBeXW6WPD5fGK5SDwg==", "cpu": [ "x64" ], @@ -851,9 +851,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.30.0.tgz", - "integrity": "sha512-3wzKzduS7jzxqcOvy/ocU/gMR3/QrHEFLge5CD7Si9fyHuoXcidyYZ6jyx8OPYmCcGm3uKTUl+9jUSAY74Ln5A==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.30.1.tgz", + "integrity": "sha512-V9U8Ey2UqmQsBT+xTOeMzPzwDzyXmnAoO4edZhL7INkwQcaW1Ckv3WJX3qrrp/VHaDkEWIBWhRwP47r8cdrOow==", "cpu": [ "x64" ], @@ -865,9 +865,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.30.0.tgz", - "integrity": "sha512-jROwnI1+wPyuv696rAFHp5+6RFhXGGwgmgSfzE8e4xfit6oLRg7GyMArVUoM3ChS045OwWr9aTnU+2c1UdBMyw==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.30.1.tgz", + "integrity": "sha512-WabtHWiPaFF47W3PkHnjbmWawnX/aE57K47ZDT1BXTS5GgrBUEpvOzq0FI0V/UYzQJgdb8XlhVNH8/fwV8xDjw==", "cpu": [ "arm64" ], @@ -879,9 +879,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.30.0.tgz", - "integrity": "sha512-duzweyup5WELhcXx5H1jokpr13i3BV9b48FMiikYAwk/MT1LrMYYk2TzenBd0jj4ivQIt58JWSxc19y4SvLP4g==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.30.1.tgz", + "integrity": "sha512-pxHAU+Zv39hLUTdQQHUVHf4P+0C47y/ZloorHpzs2SXMRqeAWmGghzAhfOlzFHHwjvgokdFAhC4V+6kC1lRRfw==", "cpu": [ "ia32" ], @@ -893,9 +893,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.30.0.tgz", - "integrity": "sha512-DYvxS0M07PvgvavMIybCOBYheyrqlui6ZQBHJs6GqduVzHSZ06TPPvlfvnYstjODHQ8UUXFwt5YE+h0jFI8kwg==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.30.1.tgz", + "integrity": "sha512-D6qjsXGcvhTjv0kI4fU8tUuBDF/Ueee4SVX79VfNDXZa64TfCW1Slkb6Z7O1p7vflqZjcmOVdZlqf8gvJxc6og==", "cpu": [ "x64" ], @@ -1077,21 +1077,21 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.0.tgz", - "integrity": "sha512-NggSaEZCdSrFddbctrVjkVZvFC6KGfKfNK0CU7mNK/iKHGKbzT4Wmgm08dKpcZECBu9f5FypndoMyRHkdqfT1Q==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.1.tgz", + "integrity": "sha512-tJzcVyvvb9h/PB96g30MpxACd9IrunT7GF9wfA9/0TJ1LxGOJx1TdPzSbBBnNED7K9Ka8ybJsnEpiXPktolTLg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.19.0", - "@typescript-eslint/type-utils": "8.19.0", - "@typescript-eslint/utils": "8.19.0", - "@typescript-eslint/visitor-keys": "8.19.0", + "@typescript-eslint/scope-manager": "8.19.1", + "@typescript-eslint/type-utils": "8.19.1", + "@typescript-eslint/utils": "8.19.1", + "@typescript-eslint/visitor-keys": "8.19.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1107,16 +1107,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.19.0.tgz", - "integrity": "sha512-6M8taKyOETY1TKHp0x8ndycipTVgmp4xtg5QpEZzXxDhNvvHOJi5rLRkLr8SK3jTgD5l4fTlvBiRdfsuWydxBw==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.19.1.tgz", + "integrity": "sha512-67gbfv8rAwawjYx3fYArwldTQKoYfezNUT4D5ioWetr/xCrxXxvleo3uuiFuKfejipvq+og7mjz3b0G2bVyUCw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.19.0", - "@typescript-eslint/types": "8.19.0", - "@typescript-eslint/typescript-estree": "8.19.0", - "@typescript-eslint/visitor-keys": "8.19.0", + "@typescript-eslint/scope-manager": "8.19.1", + "@typescript-eslint/types": "8.19.1", + "@typescript-eslint/typescript-estree": "8.19.1", + "@typescript-eslint/visitor-keys": "8.19.1", "debug": "^4.3.4" }, "engines": { @@ -1132,14 +1132,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.19.0.tgz", - "integrity": "sha512-hkoJiKQS3GQ13TSMEiuNmSCvhz7ujyqD1x3ShbaETATHrck+9RaDdUbt+osXaUuns9OFwrDTTrjtwsU8gJyyRA==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.19.1.tgz", + "integrity": "sha512-60L9KIuN/xgmsINzonOcMDSB8p82h95hoBfSBtXuO4jlR1R9L1xSkmVZKgCPVfavDlXihh4ARNjXhh1gGnLC7Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.19.0", - "@typescript-eslint/visitor-keys": "8.19.0" + "@typescript-eslint/types": "8.19.1", + "@typescript-eslint/visitor-keys": "8.19.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1150,16 +1150,16 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.19.0.tgz", - "integrity": "sha512-TZs0I0OSbd5Aza4qAMpp1cdCYVnER94IziudE3JU328YUHgWu9gwiwhag+fuLeJ2LkWLXI+F/182TbG+JaBdTg==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.19.1.tgz", + "integrity": "sha512-Rp7k9lhDKBMRJB/nM9Ksp1zs4796wVNyihG9/TU9R6KCJDNkQbc2EOKjrBtLYh3396ZdpXLtr/MkaSEmNMtykw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.19.0", - "@typescript-eslint/utils": "8.19.0", + "@typescript-eslint/typescript-estree": "8.19.1", + "@typescript-eslint/utils": "8.19.1", "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1174,9 +1174,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.19.0.tgz", - "integrity": "sha512-8XQ4Ss7G9WX8oaYvD4OOLCjIQYgRQxO+qCiR2V2s2GxI9AUpo7riNwo6jDhKtTcaJjT8PY54j2Yb33kWtSJsmA==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.19.1.tgz", + "integrity": "sha512-JBVHMLj7B1K1v1051ZaMMgLW4Q/jre5qGK0Ew6UgXz1Rqh+/xPzV1aW581OM00X6iOfyr1be+QyW8LOUf19BbA==", "dev": true, "license": "MIT", "engines": { @@ -1188,20 +1188,20 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.19.0.tgz", - "integrity": "sha512-WW9PpDaLIFW9LCbucMSdYUuGeFUz1OkWYS/5fwZwTA+l2RwlWFdJvReQqMUMBw4yJWJOfqd7An9uwut2Oj8sLw==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.19.1.tgz", + "integrity": "sha512-jk/TZwSMJlxlNnqhy0Eod1PNEvCkpY6MXOXE/WLlblZ6ibb32i2We4uByoKPv1d0OD2xebDv4hbs3fm11SMw8Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.19.0", - "@typescript-eslint/visitor-keys": "8.19.0", + "@typescript-eslint/types": "8.19.1", + "@typescript-eslint/visitor-keys": "8.19.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1215,16 +1215,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.19.0.tgz", - "integrity": "sha512-PTBG+0oEMPH9jCZlfg07LCB2nYI0I317yyvXGfxnvGvw4SHIOuRnQ3kadyyXY6tGdChusIHIbM5zfIbp4M6tCg==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.19.1.tgz", + "integrity": "sha512-IxG5gLO0Ne+KaUc8iW1A+XuKLd63o4wlbI1Zp692n1xojCl/THvgIKXJXBZixTh5dd5+yTJ/VXH7GJaaw21qXA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.19.0", - "@typescript-eslint/types": "8.19.0", - "@typescript-eslint/typescript-estree": "8.19.0" + "@typescript-eslint/scope-manager": "8.19.1", + "@typescript-eslint/types": "8.19.1", + "@typescript-eslint/typescript-estree": "8.19.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1239,13 +1239,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.19.0.tgz", - "integrity": "sha512-mCFtBbFBJDCNCWUl5y6sZSCHXw1DEFEk3c/M3nRK2a4XUB8StGFtmcEMizdjKuBzB6e/smJAAWYug3VrdLMr1w==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.19.1.tgz", + "integrity": "sha512-fzmjU8CHK853V/avYZAvuVut3ZTfwN5YtMaoi+X9Y9MA9keaWNHC3zEQ9zvyX/7Hj+5JkNyK1l7TOR2hevHB6Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.19.0", + "@typescript-eslint/types": "8.19.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -5078,9 +5078,9 @@ } }, "node_modules/rollup": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.30.0.tgz", - "integrity": "sha512-sDnr1pcjTgUT69qBksNF1N1anwfbyYG6TBQ22b03bII8EdiUQ7J0TlozVaTMjT/eEJAO49e1ndV7t+UZfL1+vA==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.30.1.tgz", + "integrity": "sha512-mlJ4glW020fPuLi7DkM/lN97mYEZGWeqBnrljzN0gs7GLctqX3lNWxKQ7Gl712UAX+6fog/L3jh4gb7R6aVi3w==", "dev": true, "license": "MIT", "dependencies": { @@ -5094,25 +5094,25 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.30.0", - "@rollup/rollup-android-arm64": "4.30.0", - "@rollup/rollup-darwin-arm64": "4.30.0", - "@rollup/rollup-darwin-x64": "4.30.0", - "@rollup/rollup-freebsd-arm64": "4.30.0", - "@rollup/rollup-freebsd-x64": "4.30.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.30.0", - "@rollup/rollup-linux-arm-musleabihf": "4.30.0", - "@rollup/rollup-linux-arm64-gnu": "4.30.0", - "@rollup/rollup-linux-arm64-musl": "4.30.0", - "@rollup/rollup-linux-loongarch64-gnu": "4.30.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.30.0", - "@rollup/rollup-linux-riscv64-gnu": "4.30.0", - "@rollup/rollup-linux-s390x-gnu": "4.30.0", - "@rollup/rollup-linux-x64-gnu": "4.30.0", - "@rollup/rollup-linux-x64-musl": "4.30.0", - "@rollup/rollup-win32-arm64-msvc": "4.30.0", - "@rollup/rollup-win32-ia32-msvc": "4.30.0", - "@rollup/rollup-win32-x64-msvc": "4.30.0", + "@rollup/rollup-android-arm-eabi": "4.30.1", + "@rollup/rollup-android-arm64": "4.30.1", + "@rollup/rollup-darwin-arm64": "4.30.1", + "@rollup/rollup-darwin-x64": "4.30.1", + "@rollup/rollup-freebsd-arm64": "4.30.1", + "@rollup/rollup-freebsd-x64": "4.30.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.30.1", + "@rollup/rollup-linux-arm-musleabihf": "4.30.1", + "@rollup/rollup-linux-arm64-gnu": "4.30.1", + "@rollup/rollup-linux-arm64-musl": "4.30.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.30.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.30.1", + "@rollup/rollup-linux-riscv64-gnu": "4.30.1", + "@rollup/rollup-linux-s390x-gnu": "4.30.1", + "@rollup/rollup-linux-x64-gnu": "4.30.1", + "@rollup/rollup-linux-x64-musl": "4.30.1", + "@rollup/rollup-win32-arm64-msvc": "4.30.1", + "@rollup/rollup-win32-ia32-msvc": "4.30.1", + "@rollup/rollup-win32-x64-msvc": "4.30.1", "fsevents": "~2.3.2" } }, @@ -5609,16 +5609,16 @@ } }, "node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.0.tgz", + "integrity": "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18.12" }, "peerDependencies": { - "typescript": ">=4.2.0" + "typescript": ">=4.8.4" } }, "node_modules/tsconfck": { @@ -5799,9 +5799,9 @@ } }, "node_modules/typescript": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", - "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index 70b30e942..20eaf0b90 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jira.js", - "version": "4.0.5", + "version": "4.0.6", "description": "A comprehensive JavaScript/TypeScript library designed for both Node.JS and browsers, facilitating seamless interaction with the Atlassian Jira API.", "main": "out/index.js", "types": "out/index.d.ts", @@ -37,7 +37,7 @@ "test": "npm run test:unit && npm run test:integration", "test:unit": "vitest run tests/unit --maxWorkers=8 --sequence.concurrent", "test:integration": "vitest run tests/integration --bail=1 --no-file-parallelism --max-concurrency 1 -c vitest.config.mts --hookTimeout 100000 --testTimeout 100000", - "replace:all": "npm run replace:permissions:version2 && npm run replace:permissions:version3 && npm run replace:pagination:version2 && npm run replace:pagination:version3 && npm run replace:async:version2 && npm run replace:async:version3 && npm run replace:expansion:version2 && npm run replace:expansion:version3 && npm run replace:ordering:version2 && npm run replace:ordering:version3 && npm run replace:groupMember:version2 && npm run replace:workflowPaginated:version2", + "replace:all": "npm run replace:permissions:version2 && npm run replace:permissions:version3 && npm run replace:pagination:version2 && npm run replace:pagination:version3 && npm run replace:async:version2 && npm run replace:async:version3 && npm run replace:expansion:version2 && npm run replace:expansion:version3 && npm run replace:ordering:version2 && npm run replace:ordering:version3 && npm run replace:groupMember:version2 && npm run replace:workflowPaginated:version2 && npm run replace:attachment:serviceDesk", "replace:permissions:version2": "grep -rl \"(#permissions)\" ./src/version2 | xargs sed -i '' 's/(#permissions)/(https:\\/\\/developer.atlassian.com\\/cloud\\/jira\\/platform\\/rest\\/v2\\/intro\\/#permissions)/g'", "replace:permissions:version3": "grep -rl \"(#permissions)\" ./src/version3 | xargs sed -i '' 's/(#permissions)/(https:\\/\\/developer.atlassian.com\\/cloud\\/jira\\/platform\\/rest\\/v3\\/intro\\/#permissions)/g'", "replace:pagination:version2": "grep -rl \"(#pagination)\" ./src/version2 | xargs sed -i '' 's/(#pagination)/(https:\\/\\/developer.atlassian.com\\/cloud\\/jira\\/platform\\/rest\\/v2\\/intro\\/#pagination)/g'", @@ -50,13 +50,14 @@ "replace:ordering:version3": "grep -rl \"(#ordering)\" ./src/version3 | xargs sed -i '' 's/(#ordering)/(https:\\/\\/developer.atlassian.com\\/cloud\\/jira\\/platform\\/rest\\/v3\\/intro\\/#ordering)/g'", "replace:groupMember:version2": "grep -rl \"(#api-rest-api-2-group-member-get)\" ./src/version2 | xargs sed -i '' 's/(#api-rest-api-2-group-member-get)/(https:\\/\\/developer.atlassian.com\\/cloud\\/jira\\/platform\\/rest\\/v2\\/api-group-groups\\/#api-rest-api-2-group-member-get)/g'", "replace:workflowPaginated:version2": "grep -rl \"(#api-rest-api-2-workflow-search-get)\" ./src/version2 | xargs sed -i '' 's/(#api-rest-api-2-workflow-search-get)/(https:\\/\\/developer.atlassian.com\\/cloud\\/jira\\/platform\\/rest\\/v2\\/api-group-workflows\\/#api-rest-api-2-workflow-search-get)/g'", + "replace:attachment:serviceDesk": "grep -rl \"(#api-request-issueIdOrKey-attachment-post)\" ./src/serviceDesk | xargs sed -i '' 's/(#api-request-issueIdOrKey-attachment-post)/(https:\\/\\/developer.atlassian.com\\/cloud\\/jira\\/service-desk\\/rest\\/api-group-servicedesk\\/#api-rest-servicedeskapi-servicedesk-servicedeskid-attachtemporaryfile-post)/g'", "code:formatting": "npm run replace:all && npm run prettier && npm run lint:fix" }, "devDependencies": { "@types/node": "^18.19.70", "@types/sinon": "^17.0.3", - "@typescript-eslint/eslint-plugin": "^8.19.0", - "@typescript-eslint/parser": "^8.19.0", + "@typescript-eslint/eslint-plugin": "^8.19.1", + "@typescript-eslint/parser": "^8.19.1", "dotenv": "^16.4.7", "eslint": "^8.57.1", "eslint-config-airbnb-base": "^15.0.0", @@ -66,7 +67,7 @@ "prettier-plugin-jsdoc": "^1.3.2", "sinon": "^18.0.1", "typedoc": "^0.27.6", - "typescript": "^5.7.2", + "typescript": "^5.7.3", "vite-tsconfig-paths": "^5.1.4", "vitest": "^2.1.8" }, diff --git a/src/serviceDesk/parameters/attachTemporaryFile.ts b/src/serviceDesk/parameters/attachTemporaryFile.ts index e9d0d7a7c..d68513d96 100644 --- a/src/serviceDesk/parameters/attachTemporaryFile.ts +++ b/src/serviceDesk/parameters/attachTemporaryFile.ts @@ -1,3 +1,5 @@ +import type { Readable } from 'node:stream'; + /** * Represents an attachment to be temporarily attached to a Service Desk. * @@ -35,7 +37,7 @@ export interface Attachment { * const fileContent = Buffer.from('Example content here'); * ``` */ - file: Buffer | ReadableStream | string | Blob | File; + file: Buffer | ReadableStream | Readable | string | Blob | File; /** * Optional MIME type of the attachment. Example values include: diff --git a/src/serviceDesk/serviceDesk.ts b/src/serviceDesk/serviceDesk.ts index f7ec53e7a..ebd4b60e4 100644 --- a/src/serviceDesk/serviceDesk.ts +++ b/src/serviceDesk/serviceDesk.ts @@ -1,4 +1,5 @@ import { FormData, File } from 'formdata-node'; +import type { Mime } from 'mime' with { 'resolution-mode': 'import' }; import * as Models from './models'; import * as Parameters from './parameters'; import { Callback } from '../callback'; @@ -85,7 +86,7 @@ export class ServiceDesk { /** * This method adds one or more temporary attachments to a service desk, which can then be permanently attached to a * customer request using - * [servicedeskapi/request/{issueIdOrKey}/attachment](#api-request-issueIdOrKey-attachment-post). + * [servicedeskapi/request/{issueIdOrKey}/attachment](https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-servicedesk/#api-rest-servicedeskapi-servicedesk-servicedeskid-attachtemporaryfile-post). * * **Note**: It is possible for a service desk administrator to turn off the ability to add attachments to a service * desk. @@ -100,7 +101,7 @@ export class ServiceDesk { /** * This method adds one or more temporary attachments to a service desk, which can then be permanently attached to a * customer request using - * [servicedeskapi/request/{issueIdOrKey}/attachment](#api-request-issueIdOrKey-attachment-post). + * [servicedeskapi/request/{issueIdOrKey}/attachment](https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-servicedesk/#api-rest-servicedeskapi-servicedesk-servicedeskid-attachtemporaryfile-post). * * **Note**: It is possible for a service desk administrator to turn off the ability to add attachments to a service * desk. @@ -118,14 +119,24 @@ export class ServiceDesk { const { default: mime } = await import('mime'); - attachments.forEach(attachment => { - const mimeType = attachment.mimeType ?? (mime.getType(attachment.filename) || undefined); - const file = Buffer.isBuffer(attachment.file) - ? new File([attachment.file], attachment.filename, { type: mimeType }) - : attachment.file; + let Readable: typeof import('stream').Readable | undefined; + + if (typeof window === 'undefined') { + const { Readable: NodeReadable } = await import('stream'); + + Readable = NodeReadable; + } + + // eslint-disable-next-line no-restricted-syntax + for await (const attachment of attachments) { + const file = await this._convertToFile(attachment, mime, Readable); + + if (!(file instanceof File || file instanceof Blob)) { + throw new Error(`Unsupported file type for attachment: ${typeof file}`); + } formData.append('file', file, attachment.filename); - }); + } const config: RequestConfig = { url: `/rest/servicedeskapi/servicedesk/${parameters.serviceDeskId}/attachTemporaryFile`, @@ -808,4 +819,75 @@ export class ServiceDesk { return this.client.sendRequest(config, callback); } + + private async _convertToFile( + attachment: Parameters.Attachment, + mime: Mime, + Readable?: typeof import('stream').Readable, + ): Promise { + const mimeType = attachment.mimeType ?? (mime.getType(attachment.filename) || undefined); + + if (attachment.file instanceof Blob || attachment.file instanceof File) { + return attachment.file; + } + + if (typeof attachment.file === 'string') { + return new File([attachment.file], attachment.filename, { type: mimeType }); + } + + if (Readable && attachment.file instanceof Readable) { + return this._streamToBlob(attachment.file, attachment.filename, mimeType); + } + + if (attachment.file instanceof ReadableStream) { + return this._streamToBlob(attachment.file, attachment.filename, mimeType); + } + + if (ArrayBuffer.isView(attachment.file) || attachment.file instanceof ArrayBuffer) { + return new File([attachment.file], attachment.filename, { type: mimeType }); + } + + throw new Error('Unsupported attachment file type.'); + } + + private async _streamToBlob( + stream: import('stream').Readable | ReadableStream, + filename: string, + mimeType?: string, + ): Promise { + if (typeof window === 'undefined' && stream instanceof (await import('stream')).Readable) { + return new Promise((resolve, reject) => { + const chunks: Uint8Array[] = []; + + stream.on('data', chunk => chunks.push(chunk)); + stream.on('end', () => { + const blob = new Blob(chunks, { type: mimeType }); + + resolve(new File([blob], filename, { type: mimeType })); + }); + stream.on('error', reject); + }); + } + + if (stream instanceof ReadableStream) { + const reader = stream.getReader(); + const chunks: Uint8Array[] = []; + + let done = false; + + while (!done) { + // eslint-disable-next-line no-await-in-loop + const { value, done: streamDone } = await reader.read(); + + if (value) chunks.push(value); + done = streamDone; + } + + const blob = new Blob(chunks, { type: mimeType }); + + return new File([blob], filename, { type: mimeType }); + } + + throw new Error('Unsupported stream type.'); + } } diff --git a/src/version2/issueAttachments.ts b/src/version2/issueAttachments.ts index c2dbd4052..5295ebd49 100644 --- a/src/version2/issueAttachments.ts +++ b/src/version2/issueAttachments.ts @@ -1,4 +1,5 @@ import { FormData, File } from 'formdata-node'; +import type { Mime } from 'mime' with { 'resolution-mode': 'import' }; import * as Models from './models'; import * as Parameters from './parameters'; import { Callback } from '../callback'; @@ -379,12 +380,6 @@ export class IssueAttachments { * Adds one or more attachments to an issue. Attachments are posted as multipart/form-data ([RFC * 1867](https://www.ietf.org/rfc/rfc1867.txt)). * - * Note that: - * - * - The request must have a `X-Atlassian-Token: no-check` header, if not it is blocked. See [Special - * headers](#special-request-headers) for more information. - * - The name of the multipart/form-data parameter that contains the attachments must be `file`. - * * This operation can be accessed anonymously. * * **[Permissions](https://developer.atlassian.com/cloud/jira/platform/rest/v2/intro/#permissions) required:** @@ -402,12 +397,6 @@ export class IssueAttachments { * Adds one or more attachments to an issue. Attachments are posted as multipart/form-data ([RFC * 1867](https://www.ietf.org/rfc/rfc1867.txt)). * - * Note that: - * - * - The request must have a `X-Atlassian-Token: no-check` header, if not it is blocked. See [Special - * headers](#special-request-headers) for more information. - * - The name of the multipart/form-data parameter that contains the attachments must be `file`. - * * This operation can be accessed anonymously. * * **[Permissions](https://developer.atlassian.com/cloud/jira/platform/rest/v2/intro/#permissions) required:** @@ -427,14 +416,24 @@ export class IssueAttachments { const { default: mime } = await import('mime'); - attachments.forEach(attachment => { - const mimeType = attachment.mimeType ?? (mime.getType(attachment.filename) || undefined); - const file = Buffer.isBuffer(attachment.file) - ? new File([attachment.file], attachment.filename, { type: mimeType }) - : attachment.file; + let Readable: typeof import('stream').Readable | undefined; + + if (typeof window === 'undefined') { + const { Readable: NodeReadable } = await import('stream'); + + Readable = NodeReadable; + } + + // eslint-disable-next-line no-restricted-syntax + for await (const attachment of attachments) { + const file = await this._convertToFile(attachment, mime, Readable); + + if (!(file instanceof File || file instanceof Blob)) { + throw new Error(`Unsupported file type for attachment: ${typeof file}`); + } formData.append('file', file, attachment.filename); - }); + } const config: RequestConfig = { url: `/rest/api/2/issue/${parameters.issueIdOrKey}/attachments`, @@ -450,4 +449,75 @@ export class IssueAttachments { return this.client.sendRequest(config, callback); } + + private async _convertToFile( + attachment: Parameters.Attachment, + mime: Mime, + Readable?: typeof import('stream').Readable, + ): Promise { + const mimeType = attachment.mimeType ?? (mime.getType(attachment.filename) || undefined); + + if (attachment.file instanceof Blob || attachment.file instanceof File) { + return attachment.file; + } + + if (typeof attachment.file === 'string') { + return new File([attachment.file], attachment.filename, { type: mimeType }); + } + + if (Readable && attachment.file instanceof Readable) { + return this._streamToBlob(attachment.file, attachment.filename, mimeType); + } + + if (attachment.file instanceof ReadableStream) { + return this._streamToBlob(attachment.file, attachment.filename, mimeType); + } + + if (ArrayBuffer.isView(attachment.file) || attachment.file instanceof ArrayBuffer) { + return new File([attachment.file], attachment.filename, { type: mimeType }); + } + + throw new Error('Unsupported attachment file type.'); + } + + private async _streamToBlob( + stream: import('stream').Readable | ReadableStream, + filename: string, + mimeType?: string, + ): Promise { + if (typeof window === 'undefined' && stream instanceof (await import('stream')).Readable) { + return new Promise((resolve, reject) => { + const chunks: Uint8Array[] = []; + + stream.on('data', chunk => chunks.push(chunk)); + stream.on('end', () => { + const blob = new Blob(chunks, { type: mimeType }); + + resolve(new File([blob], filename, { type: mimeType })); + }); + stream.on('error', reject); + }); + } + + if (stream instanceof ReadableStream) { + const reader = stream.getReader(); + const chunks: Uint8Array[] = []; + + let done = false; + + while (!done) { + // eslint-disable-next-line no-await-in-loop + const { value, done: streamDone } = await reader.read(); + + if (value) chunks.push(value); + done = streamDone; + } + + const blob = new Blob(chunks, { type: mimeType }); + + return new File([blob], filename, { type: mimeType }); + } + + throw new Error('Unsupported stream type.'); + } } diff --git a/src/version2/parameters/addAttachment.ts b/src/version2/parameters/addAttachment.ts index 5caab857b..7a0682f58 100644 --- a/src/version2/parameters/addAttachment.ts +++ b/src/version2/parameters/addAttachment.ts @@ -1,3 +1,5 @@ +import type { Readable } from 'node:stream'; + /** * Represents an attachment to be added to an issue. * @@ -35,7 +37,7 @@ export interface Attachment { * const fileContent = fs.readFileSync('./document.pdf'); * ``` */ - file: Buffer | ReadableStream | string | Blob | File; + file: Buffer | ReadableStream | Readable | string | Blob | File; /** * Optional MIME type of the attachment. Example values include: diff --git a/src/version3/issueAttachments.ts b/src/version3/issueAttachments.ts index 4c83e6fb1..954d64c28 100644 --- a/src/version3/issueAttachments.ts +++ b/src/version3/issueAttachments.ts @@ -1,4 +1,5 @@ import { FormData, File } from 'formdata-node'; +import type { Mime } from 'mime' with { 'resolution-mode': 'import' }; import * as Models from './models'; import * as Parameters from './parameters'; import { Callback } from '../callback'; @@ -427,14 +428,24 @@ export class IssueAttachments { const { default: mime } = await import('mime'); - attachments.forEach(attachment => { - const mimeType = attachment.mimeType ?? (mime.getType(attachment.filename) || undefined); - const file = Buffer.isBuffer(attachment.file) - ? new File([attachment.file], attachment.filename, { type: mimeType }) - : attachment.file; + let Readable: typeof import('stream').Readable | undefined; + + if (typeof window === 'undefined') { + const { Readable: NodeReadable } = await import('stream'); + + Readable = NodeReadable; + } + + // eslint-disable-next-line no-restricted-syntax + for await (const attachment of attachments) { + const file = await this._convertToFile(attachment, mime, Readable); + + if (!(file instanceof File || file instanceof Blob)) { + throw new Error(`Unsupported file type for attachment: ${typeof file}`); + } formData.append('file', file, attachment.filename); - }); + } const config: RequestConfig = { url: `/rest/api/3/issue/${parameters.issueIdOrKey}/attachments`, @@ -450,4 +461,75 @@ export class IssueAttachments { return this.client.sendRequest(config, callback); } + + private async _convertToFile( + attachment: Parameters.Attachment, + mime: Mime, + Readable?: typeof import('stream').Readable, + ): Promise { + const mimeType = attachment.mimeType ?? (mime.getType(attachment.filename) || undefined); + + if (attachment.file instanceof Blob || attachment.file instanceof File) { + return attachment.file; + } + + if (typeof attachment.file === 'string') { + return new File([attachment.file], attachment.filename, { type: mimeType }); + } + + if (Readable && attachment.file instanceof Readable) { + return this._streamToBlob(attachment.file, attachment.filename, mimeType); + } + + if (attachment.file instanceof ReadableStream) { + return this._streamToBlob(attachment.file, attachment.filename, mimeType); + } + + if (ArrayBuffer.isView(attachment.file) || attachment.file instanceof ArrayBuffer) { + return new File([attachment.file], attachment.filename, { type: mimeType }); + } + + throw new Error('Unsupported attachment file type.'); + } + + private async _streamToBlob( + stream: import('stream').Readable | ReadableStream, + filename: string, + mimeType?: string, + ): Promise { + if (typeof window === 'undefined' && stream instanceof (await import('stream')).Readable) { + return new Promise((resolve, reject) => { + const chunks: Uint8Array[] = []; + + stream.on('data', chunk => chunks.push(chunk)); + stream.on('end', () => { + const blob = new Blob(chunks, { type: mimeType }); + + resolve(new File([blob], filename, { type: mimeType })); + }); + stream.on('error', reject); + }); + } + + if (stream instanceof ReadableStream) { + const reader = stream.getReader(); + const chunks: Uint8Array[] = []; + + let done = false; + + while (!done) { + // eslint-disable-next-line no-await-in-loop + const { value, done: streamDone } = await reader.read(); + + if (value) chunks.push(value); + done = streamDone; + } + + const blob = new Blob(chunks, { type: mimeType }); + + return new File([blob], filename, { type: mimeType }); + } + + throw new Error('Unsupported stream type.'); + } } diff --git a/src/version3/parameters/addAttachment.ts b/src/version3/parameters/addAttachment.ts index e372d3896..d30367c87 100644 --- a/src/version3/parameters/addAttachment.ts +++ b/src/version3/parameters/addAttachment.ts @@ -1,3 +1,5 @@ +import type { Readable } from 'node:stream'; + /** * Represents an attachment to be added to an issue. * @@ -35,7 +37,7 @@ export interface Attachment { * const fileContent = fs.readFileSync('./document.pdf'); * ``` */ - file: Buffer | ReadableStream | string | Blob | File; + file: Buffer | ReadableStream | Readable | string | Blob | File; /** * Optional MIME type of the attachment. Example values include: diff --git a/tests/integration/version2/issueAttachments.test.ts b/tests/integration/version2/issueAttachments.test.ts index 195d035a8..8b5b22724 100644 --- a/tests/integration/version2/issueAttachments.test.ts +++ b/tests/integration/version2/issueAttachments.test.ts @@ -1,8 +1,9 @@ -import * as fs from 'fs'; +import * as fs from 'node:fs'; import { afterAll, beforeAll, test } from 'vitest'; import type { Attachment, Issue } from '@jirajs/version2/models'; import { Constants } from '@tests/integration/constants'; import { cleanupEnvironment, getVersion2Client, prepareEnvironment } from '@tests/integration/utils'; +import { Readable } from 'node:stream'; const client = getVersion2Client({ noCheckAtlassianToken: true }); @@ -62,6 +63,40 @@ test.sequential('should add attachment with custom MIME type', async ({ expect } expect(customAttachment[0].mimeType).toBe(customMimeType); }); +test.sequential('should add attachment with ReadableStream', async ({ expect }) => { + const readableStream = Readable.from(['This is a test content for ReadableStream.']); + + attachments = await client.issueAttachments.addAttachment({ + issueIdOrKey: issue.key, + attachment: { + filename: 'readableStreamAttachment.txt', + file: readableStream, + }, + }); + + expect(!!attachments).toBeTruthy(); + expect(attachments[0].filename).toBe('readableStreamAttachment.txt'); + expect(attachments[0].mimeType).toBe('text/plain'); +}); + +test.sequential('should add attachment with fs.createReadStream', async ({ expect }) => { + const customMimeType = 'application/typescript'; + const fileStream = fs.createReadStream('./tests/integration/version2/issueAttachments.test.ts'); + + attachments = await client.issueAttachments.addAttachment({ + issueIdOrKey: issue.key, + attachment: { + filename: 'fsReadStreamAttachment.ts', + file: fileStream, + mimeType: customMimeType, + }, + }); + + expect(!!attachments).toBeTruthy(); + expect(attachments[0].filename).toBe('fsReadStreamAttachment.ts'); + expect(attachments[0].mimeType).toBe(customMimeType); +}); + test.sequential('should getAttachmentContent', async ({ expect }) => { const content = await client.issueAttachments.getAttachmentContent({ id: attachments[0].id }); diff --git a/tests/integration/version3/issueAttachments.test.ts b/tests/integration/version3/issueAttachments.test.ts index 4a2f21043..863e41131 100644 --- a/tests/integration/version3/issueAttachments.test.ts +++ b/tests/integration/version3/issueAttachments.test.ts @@ -3,6 +3,7 @@ import { afterAll, beforeAll, test } from 'vitest'; import type { Attachment, Issue } from '@jirajs/version3/models'; import { Constants } from '@tests/integration/constants'; import { cleanupEnvironment, getVersion3Client, prepareEnvironment } from '@tests/integration/utils'; +import { Readable } from 'node:stream'; const client = getVersion3Client({ noCheckAtlassianToken: true }); @@ -62,6 +63,40 @@ test.sequential('should add attachment with custom MIME type', async ({ expect } expect(customAttachment[0].mimeType).toBe(customMimeType); }); +test.sequential('should add attachment with ReadableStream', async ({ expect }) => { + const readableStream = Readable.from(['This is a test content for ReadableStream.']); + + attachments = await client.issueAttachments.addAttachment({ + issueIdOrKey: issue.key, + attachment: { + filename: 'readableStreamAttachment.txt', + file: readableStream, + }, + }); + + expect(!!attachments).toBeTruthy(); + expect(attachments[0].filename).toBe('readableStreamAttachment.txt'); + expect(attachments[0].mimeType).toBe('text/plain'); +}); + +test.sequential('should add attachment with fs.createReadStream', async ({ expect }) => { + const customMimeType = 'application/typescript'; + const fileStream = fs.createReadStream('./tests/integration/version2/issueAttachments.test.ts'); + + attachments = await client.issueAttachments.addAttachment({ + issueIdOrKey: issue.key, + attachment: { + filename: 'fsReadStreamAttachment.ts', + file: fileStream, + mimeType: customMimeType, + }, + }); + + expect(!!attachments).toBeTruthy(); + expect(attachments[0].filename).toBe('fsReadStreamAttachment.ts'); + expect(attachments[0].mimeType).toBe(customMimeType); +}); + test.sequential('should getAttachmentContent', async ({ expect }) => { const content = await client.issueAttachments.getAttachmentContent({ id: attachments[0].id });