diff --git a/components/jira_service_desk/actions/create-comment-on-request/create-comment-on-request.mjs b/components/jira_service_desk/actions/create-comment-on-request/create-comment-on-request.mjs index f328bce0ffd21..0d80f43f41459 100644 --- a/components/jira_service_desk/actions/create-comment-on-request/create-comment-on-request.mjs +++ b/components/jira_service_desk/actions/create-comment-on-request/create-comment-on-request.mjs @@ -4,7 +4,7 @@ export default { key: "jira_service_desk-create-comment-on-request", name: "Create Comment on Request", description: "Create a comment on a customer request. [See the documentation](https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-request/#api-rest-servicedeskapi-request-issueidorkey-comment-post)", - version: "0.1.3", + version: "0.1.4", annotations: { destructiveHint: false, openWorldHint: true, diff --git a/components/jira_service_desk/actions/create-request/create-request.mjs b/components/jira_service_desk/actions/create-request/create-request.mjs index cec6a2d7a9647..f8b1006f51fb2 100644 --- a/components/jira_service_desk/actions/create-request/create-request.mjs +++ b/components/jira_service_desk/actions/create-request/create-request.mjs @@ -12,9 +12,10 @@ export default { + " Use **List Sites** to get `cloudId`, **List Service Desks** to get `serviceDeskId`, and **List Request Types** to choose the `requestTypeId` whose name and description match the user's intent." + " Call **List Request Type Fields** to see which fields that request type requires; pass anything beyond summary and description in `additionalFieldValues`, keyed by Jira field ID." + " Worked example: on service desk `1`, request type `4` (\"Onboard new employees\") requires `summary` and also accepts a `duedate`, so call with Summary `Joseph Wilson starts on September 1`, Description `Needs a laptop and an email account`, and Additional Field Values `{ \"duedate\": \"2026-09-01\" }`." - + " Returns the created request including its `issueKey` and `issueId`." + + " Optionally attach one or more files at creation time via `attachments`; to add, replace, or delete attachments on a request that already exists, use **Manage Request Attachment** instead." + + " Returns the created request including its `issueKey` and `issueId`. If `attachments` is set, the response also includes either an `attachments` array (on success) or an `attachmentError` string (if the request was created but the attachment step failed) — the request itself is never rolled back because of an attachment failure." + " [See the documentation](https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-request/#api-rest-servicedeskapi-request-post)", - version: "1.0.3", + version: "1.1.0", annotations: { destructiveHint: false, openWorldHint: true, @@ -95,6 +96,26 @@ export default { description: "Extra information about the channel the request came in on. Marked experimental by Atlassian.", optional: true, }, + attachments: { + type: "string[]", + label: "Attachments", + description: "File(s) to attach to the request as it's created. Provide file URLs or paths to files in the /tmp directory (e.g. `/tmp/myFile.pdf`).", + format: "file-ref", + optional: true, + }, + attachmentsPublic: { + type: "boolean", + label: "Attachments Public", + description: "Whether the attached file(s) are visible to the customer who raised the request. Defaults to `true`; set to `false` to attach internal-only files.", + optional: true, + default: true, + }, + syncDir: { + type: "dir", + accessMode: "read", + sync: true, + optional: true, + }, }, async run({ $ }) { const { @@ -110,6 +131,8 @@ export default { form, isAdfRequest, channel, + attachments, + attachmentsPublic, } = this; let extraFields = additionalFieldValues; @@ -172,7 +195,32 @@ export default { }, }); - $.export("$summary", `Successfully created request ${response.issueKey}`); - return response; + if (!attachments?.length) { + $.export("$summary", `Successfully created request ${response.issueKey}`); + return response; + } + + try { + const attachResponse = await jiraServiceDesk.attachFilesToRequestFromSource({ + $, + cloudId, + serviceDeskId, + issueIdOrKey: response.issueKey, + files: attachments, + isPublic: attachmentsPublic, + }); + + $.export("$summary", `Created request ${response.issueKey} with ${attachments.length} attachment(s)`); + return { + ...response, + attachments: attachResponse.attachments.values, + }; + } catch (error) { + $.export("$summary", `Created request ${response.issueKey}, but attaching file(s) failed: ${error.message}`); + return { + ...response, + attachmentError: error.message, + }; + } }, }; diff --git a/components/jira_service_desk/actions/download-issue-attachment/download-issue-attachment.mjs b/components/jira_service_desk/actions/download-issue-attachment/download-issue-attachment.mjs index dd11a9c0fc29f..9fa58e78a89b8 100644 --- a/components/jira_service_desk/actions/download-issue-attachment/download-issue-attachment.mjs +++ b/components/jira_service_desk/actions/download-issue-attachment/download-issue-attachment.mjs @@ -12,7 +12,7 @@ export default { key: "jira_service_desk-download-issue-attachment", name: "Download Issue Attachment", description: "Download the binary content of a Jira Service Desk attachment to the file-stash directory, returning the saved path plus the attachment metadata (`filename`, `mimeType`, `size`). Run **List Issue Attachments** first to obtain the attachment `id`, then pass both it and the same `issueIdOrKey` here. Example: passing `issueIdOrKey` `IT-42` and `attachmentId` `10042` downloads `screenshot.png` to `/tmp/10042-screenshot.png` and returns `{ \"filedata\": [\"10042-screenshot.png\", \"/tmp/10042-screenshot.png\"], \"attachment\": { \"id\": \"10042\", \"filename\": \"screenshot.png\", \"mimeType\": \"image/png\", \"size\": 84213 } }`. [See the documentation](https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-request/#api-rest-servicedeskapi-request-issueidorkey-attachment-attachmentid-get)", - version: "0.0.2", + version: "0.0.3", type: "action", ai: "optimized", annotations: { diff --git a/components/jira_service_desk/actions/get-current-user/get-current-user.mjs b/components/jira_service_desk/actions/get-current-user/get-current-user.mjs index fdf1a83a30972..cb0a51917b2f6 100644 --- a/components/jira_service_desk/actions/get-current-user/get-current-user.mjs +++ b/components/jira_service_desk/actions/get-current-user/get-current-user.mjs @@ -8,7 +8,7 @@ export default { + " Use this to identify who is logged in, or to filter requests by the current user's `account_id`." + " No `cloudId` required — this uses the Atlassian Identity API directly." + " [See the documentation](https://developer.atlassian.com/cloud/jira/platform/oauth-2-3lo-apps/)", - version: "0.0.6", + version: "0.0.7", type: "action", ai: "optimized", annotations: { diff --git a/components/jira_service_desk/actions/get-request-status/get-request-status.mjs b/components/jira_service_desk/actions/get-request-status/get-request-status.mjs index 9b4f3175721f6..febbfc9fb99a4 100644 --- a/components/jira_service_desk/actions/get-request-status/get-request-status.mjs +++ b/components/jira_service_desk/actions/get-request-status/get-request-status.mjs @@ -11,7 +11,7 @@ export default { + " Use **List Sites** first to obtain the required `cloudId`." + " Use **List My Requests** to find the `issueKey` (e.g. `IT-42`)." + " [See the documentation](https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-request/#api-rest-servicedeskapi-request-issueidorkey-status-get)", - version: "1.1.3", + version: "1.1.4", type: "action", ai: "optimized", annotations: { diff --git a/components/jira_service_desk/actions/get-request/get-request.mjs b/components/jira_service_desk/actions/get-request/get-request.mjs index 27fcf9b9c5847..901086eff3e42 100644 --- a/components/jira_service_desk/actions/get-request/get-request.mjs +++ b/components/jira_service_desk/actions/get-request/get-request.mjs @@ -10,7 +10,7 @@ export default { + " Use **List Sites** first to obtain the required `cloudId`." + " Use **List My Requests** to find the `issueKey` of a request (e.g. `IT-42`)." + " [See the documentation](https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-request/#api-rest-servicedeskapi-request-issueidorkey-get)", - version: "0.2.3", + version: "0.2.4", type: "action", ai: "optimized", annotations: { diff --git a/components/jira_service_desk/actions/list-cloud-id-options/list-cloud-id-options.mjs b/components/jira_service_desk/actions/list-cloud-id-options/list-cloud-id-options.mjs index 396b9669bf136..1153fac9df3b0 100644 --- a/components/jira_service_desk/actions/list-cloud-id-options/list-cloud-id-options.mjs +++ b/components/jira_service_desk/actions/list-cloud-id-options/list-cloud-id-options.mjs @@ -4,7 +4,7 @@ export default { key: "jira_service_desk-list-cloud-id-options", name: "List Cloud ID Options", description: "Lists the Atlassian sites you can raise requests on, as `{label, value}` options, to discover the `cloudId` every other Jira Service Desk tool needs. Takes no input beyond the account. Example: returns `[{ \"label\": \"acme\", \"value\": \"822faf0d-5427-420e-9016-999d3dc76918\" }]`. Use **List Sites** instead if you want the full site records. [See the documentation](https://developer.atlassian.com/cloud/jira/platform/oauth-2-3lo-apps/#3-1-get-the-cloudid-for-your-site)", - version: "0.1.2", + version: "0.1.3", type: "action", annotations: { destructiveHint: false, diff --git a/components/jira_service_desk/actions/list-issue-attachments/list-issue-attachments.mjs b/components/jira_service_desk/actions/list-issue-attachments/list-issue-attachments.mjs index 3911030f722ae..a30bbe053b9e1 100644 --- a/components/jira_service_desk/actions/list-issue-attachments/list-issue-attachments.mjs +++ b/components/jira_service_desk/actions/list-issue-attachments/list-issue-attachments.mjs @@ -5,7 +5,7 @@ export default { key: "jira_service_desk-list-issue-attachments", name: "List Issue Attachments", description: "List metadata for every attachment on a Jira Service Desk request. `issueIdOrKey` accepts either a Jira issue key (e.g. `IT-42`) or a numeric Jira issue ID (e.g. `10001`). Results are paginated automatically up to `maxResults`. Returns `{ attachments, truncated }`, where each attachment includes `id`, `filename`, `size` (bytes), `mimeType`, and `content` (an opaque reference URL whose exact shape varies by account access level and is **not** directly fetchable through this connection's authentication), and `truncated` is `true` when more attachments remained unfetched. Use **Download Issue Attachment** with an attachment `id` and the same `issueIdOrKey` to fetch the binary content — do not call `content` directly. If this connection has customer-level access rather than agent access, only public attachments are returned; internal attachments exist but won't appear here. Returns `{ attachments: [], truncated: false }` (no error) when the request exists but has no visible attachments. Example: issue `IT-42` with one attachment returns `{ \"attachments\": [{ \"id\": \"10042\", \"filename\": \"screenshot.png\", \"size\": 84213, \"mimeType\": \"image/png\", \"content\": \"\" }], \"truncated\": false }`. [See the documentation](https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-request/#api-rest-servicedeskapi-request-issueidorkey-attachment-get)", - version: "0.0.2", + version: "0.0.3", type: "action", ai: "optimized", annotations: { diff --git a/components/jira_service_desk/actions/list-my-requests/list-my-requests.mjs b/components/jira_service_desk/actions/list-my-requests/list-my-requests.mjs index a95fb3c23985f..6c6cca7933021 100644 --- a/components/jira_service_desk/actions/list-my-requests/list-my-requests.mjs +++ b/components/jira_service_desk/actions/list-my-requests/list-my-requests.mjs @@ -13,7 +13,7 @@ export default { + " `requestStatus`: `OPEN_REQUESTS` (default), `CLOSED_REQUESTS`, or `ALL_REQUESTS`." + " `requestOwnership`: `OWNED_REQUESTS` (default) or `PARTICIPATED_REQUESTS`." + " [See the documentation](https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-request/#api-rest-servicedeskapi-request-get)", - version: "1.1.3", + version: "1.1.4", type: "action", ai: "optimized", annotations: { diff --git a/components/jira_service_desk/actions/list-request-transitions/list-request-transitions.mjs b/components/jira_service_desk/actions/list-request-transitions/list-request-transitions.mjs index 9736e3f5a3d56..5b91950b23d8a 100644 --- a/components/jira_service_desk/actions/list-request-transitions/list-request-transitions.mjs +++ b/components/jira_service_desk/actions/list-request-transitions/list-request-transitions.mjs @@ -11,7 +11,7 @@ export default { + " Use **List Sites** first to obtain the required `cloudId`." + " Use **List My Requests** or **Get Request** to find the `issueKey` (e.g. `IT-42`)." + " [See the documentation](https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-request/#api-rest-servicedeskapi-request-issueidorkey-transition-get)", - version: "1.1.3", + version: "1.1.4", type: "action", ai: "optimized", annotations: { diff --git a/components/jira_service_desk/actions/list-request-type-fields/list-request-type-fields.mjs b/components/jira_service_desk/actions/list-request-type-fields/list-request-type-fields.mjs index 3ac1b9174258e..e6508c52fa110 100644 --- a/components/jira_service_desk/actions/list-request-type-fields/list-request-type-fields.mjs +++ b/components/jira_service_desk/actions/list-request-type-fields/list-request-type-fields.mjs @@ -11,7 +11,7 @@ export default { + " Also returns `canRaiseOnBehalfOf` and `canAddRequestParticipants`, which tell you whether the `raiseOnBehalfOf` and `requestParticipants` arguments of **Create Request** are usable with this account." + " Hidden fields are only visible to service desk administrators." + " [See the documentation](https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-servicedesk/#api-rest-servicedeskapi-servicedesk-servicedeskid-requesttype-requesttypeid-field-get)", - version: "0.0.4", + version: "0.0.5", type: "action", ai: "optimized", annotations: { diff --git a/components/jira_service_desk/actions/list-request-types/list-request-types.mjs b/components/jira_service_desk/actions/list-request-types/list-request-types.mjs index 64b49a717b0a8..1bc39b48645ed 100644 --- a/components/jira_service_desk/actions/list-request-types/list-request-types.mjs +++ b/components/jira_service_desk/actions/list-request-types/list-request-types.mjs @@ -14,7 +14,7 @@ export default { + " Types with `canCreateRequest: false` cannot be used to raise a request." + " Then call **List Request Type Fields** to see what the chosen type requires." + " [See the documentation](https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-servicedesk/#api-rest-servicedeskapi-servicedesk-servicedeskid-requesttype-get)", - version: "0.0.4", + version: "0.0.5", type: "action", ai: "optimized", annotations: { diff --git a/components/jira_service_desk/actions/list-service-desks/list-service-desks.mjs b/components/jira_service_desk/actions/list-service-desks/list-service-desks.mjs index 5930f4adae269..fda645389befc 100644 --- a/components/jira_service_desk/actions/list-service-desks/list-service-desks.mjs +++ b/components/jira_service_desk/actions/list-service-desks/list-service-desks.mjs @@ -11,7 +11,7 @@ export default { + " Returns `{ serviceDesks, truncated }`, where `truncated` is `true` when more desks remained unfetched." + " Example: a site with one desk returns `{ \"serviceDesks\": [{ \"id\": \"1\", \"projectName\": \"Support\", \"projectKey\": \"SUP\" }], \"truncated\": false }`." + " [See the documentation](https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-servicedesk/#api-rest-servicedeskapi-servicedesk-get)", - version: "0.0.4", + version: "0.0.5", type: "action", ai: "optimized", annotations: { diff --git a/components/jira_service_desk/actions/list-sites/list-sites.mjs b/components/jira_service_desk/actions/list-sites/list-sites.mjs index 5754ae50a2da6..81ca7e439a83e 100644 --- a/components/jira_service_desk/actions/list-sites/list-sites.mjs +++ b/components/jira_service_desk/actions/list-sites/list-sites.mjs @@ -8,7 +8,7 @@ export default { + " **Call this tool first** to obtain the `cloudId` (returned as `id`) required by every other Jira Service Desk tool." + " Each site includes its `id` (cloudId), `name`, and `url`." + " [See the documentation](https://developer.atlassian.com/cloud/jira/platform/oauth-2-3lo-apps/#3-1-get-the-cloudid-for-your-site)", - version: "0.0.6", + version: "0.0.7", type: "action", ai: "optimized", annotations: { diff --git a/components/jira_service_desk/actions/manage-request-attachment/manage-request-attachment.mjs b/components/jira_service_desk/actions/manage-request-attachment/manage-request-attachment.mjs new file mode 100644 index 0000000000000..11d9161158a87 --- /dev/null +++ b/components/jira_service_desk/actions/manage-request-attachment/manage-request-attachment.mjs @@ -0,0 +1,173 @@ +import { ConfigurationError } from "@pipedream/platform"; +import jiraServiceDesk from "../../jira_service_desk.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "jira_service_desk-manage-request-attachment", + name: "Manage Request Attachment", + description: + "Adds, replaces, or deletes a single attachment on a customer request (ticket) that already exists." + + " To attach file(s) while creating a brand-new request, use **Create Request**'s `attachments` prop instead — use this tool only for requests that already exist." + + " Set `operation` to `add` to attach a new file, `update` to replace an existing attachment with a different file (there's no in-place replace in the API, so this uploads the new file and only deletes the old one once the new one is confirmed attached), or `delete` to remove an attachment." + + " `add` and `update` require `serviceDeskId` (the temp-file upload step is scoped by service desk, not by issue) — use **List Service Desks** to find it, or read it off the response of the request that created the ticket." + + " `update` and `delete` require `attachmentId`, the numeric ID of the attachment being replaced or removed; the `add`/`update` response doesn't expose this as a plain field, so use **List Issue Attachments** on the same request to look it up." + + " Worked example: to replace an attachment `10050` on request `HD-12` with a new file, call with Operation `update`, Issue ID Or Key `HD-12`, Service Desk ID `1`, Attachment ID `10050`, and File `/tmp/revised-report.pdf`." + + " For `update`, if the new file attaches successfully but removing the old attachment then fails, the response includes a `deleteError` string alongside the successful attachment data — the request ends up with both files rather than losing either one, and the error is never masked as a full failure." + + " By default the attachment is visible to the customer who raised the request (`public: true`); set `public` to `false` to attach an internal-only file." + + " [See the documentation](https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-request/#api-rest-servicedeskapi-request-issueidorkey-attachment-post)", + version: "0.0.1", + annotations: { + destructiveHint: true, + openWorldHint: true, + readOnlyHint: false, + }, + type: "action", + ai: "optimized", + props: { + jiraServiceDesk, + cloudId: { + propDefinition: [ + jiraServiceDesk, + "cloudId", + ], + }, + operation: { + type: "string", + label: "Operation", + description: "Whether to add a new attachment, replace (`update`) an existing one, or delete one.", + options: [ + "add", + "update", + "delete", + ], + }, + issueIdOrKey: { + propDefinition: [ + jiraServiceDesk, + "issueIdOrKey", + ], + }, + serviceDeskId: { + propDefinition: [ + jiraServiceDesk, + "serviceDeskId", + ], + description: "Required for `add` and `update`. The service desk the request belongs to. Use **List Service Desks** to find valid IDs (e.g. `1`).", + optional: true, + }, + file: { + type: "string", + label: "File", + description: "Required for `add` and `update`. The file to attach, as a file URL or a path to a file in the /tmp directory (e.g. `/tmp/myFile.pdf`).", + format: "file-ref", + optional: true, + }, + attachmentId: { + type: "string", + label: "Attachment ID", + description: "Required for `update` and `delete`. The numeric ID of the attachment being replaced or removed, e.g. `10050`. The `add`/`update` response doesn't expose this as a plain field — the ID is only embedded in a `_links.jiraRest` URL nested under `attachments.values[]`. Use **List Issue Attachments** instead, which extracts the ID for you.", + optional: true, + }, + public: { + type: "boolean", + label: "Public", + description: "Whether the attached file is visible to the customer who raised the request. Defaults to `true`; set to `false` to attach an internal-only file. Only applies to `add` and `update`.", + optional: true, + default: true, + }, + syncDir: { + type: "dir", + accessMode: "read", + sync: true, + optional: true, + }, + }, + async run({ $ }) { + const { + jiraServiceDesk, + cloudId, + operation, + issueIdOrKey, + serviceDeskId, + file, + attachmentId, + public: isPublic, + } = this; + + const verifyAttachmentBelongsToRequest = async () => { + const { attachments } = await jiraServiceDesk.getIssueAttachments({ + $, + cloudId, + issueIdOrKey, + maxResults: constants.MAX_RESULTS_MAX, + }); + if (!attachments.some(({ id }) => id === attachmentId)) { + throw new ConfigurationError(`Attachment ID "${attachmentId}" was not found on request "${issueIdOrKey}". Run List Issue Attachments to confirm the ID and issue match.`); + } + }; + + if (operation === "delete") { + if (!attachmentId) { + throw new ConfigurationError("Attachment ID is required to delete an attachment."); + } + await verifyAttachmentBelongsToRequest(); + await jiraServiceDesk.deleteAttachment({ + $, + cloudId, + attachmentId, + }); + $.export("$summary", `Deleted attachment ${attachmentId} from request ${issueIdOrKey}`); + return { + deleted: true, + attachmentId, + }; + } + + if (!serviceDeskId) { + throw new ConfigurationError("Service Desk ID is required to add or update an attachment."); + } + if (!file) { + throw new ConfigurationError("File is required to add or update an attachment."); + } + if (operation === "update") { + if (!attachmentId) { + throw new ConfigurationError("Attachment ID is required to update (replace) an attachment."); + } + await verifyAttachmentBelongsToRequest(); + } + + const attachResponse = await jiraServiceDesk.attachFilesToRequestFromSource({ + $, + cloudId, + serviceDeskId, + issueIdOrKey, + files: [ + file, + ], + isPublic, + }); + + // The new attachment must succeed before the old one is removed — deleting first + // would leave the request with no attachment at all if the upload then failed. + if (operation === "update") { + try { + await jiraServiceDesk.deleteAttachment({ + $, + cloudId, + attachmentId, + }); + } catch (error) { + $.export("$summary", `Added the new attachment on request ${issueIdOrKey}, but the old attachment (${attachmentId}) could not be removed: ${error.message}`); + return { + ...attachResponse, + deleteError: error.message, + }; + } + } + + $.export("$summary", `${operation === "update" + ? "Replaced" + : "Added"} attachment on request ${issueIdOrKey}`); + return attachResponse; + }, +}; diff --git a/components/jira_service_desk/actions/transition-request/transition-request.mjs b/components/jira_service_desk/actions/transition-request/transition-request.mjs index 14b7f872fe435..240e9e86e52a7 100644 --- a/components/jira_service_desk/actions/transition-request/transition-request.mjs +++ b/components/jira_service_desk/actions/transition-request/transition-request.mjs @@ -10,7 +10,7 @@ export default { + " Use **List My Requests** or **Get Request** to find the `issueKey` (e.g. `IT-42`)." + " Optionally include a comment to explain the transition." + " [See the documentation](https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-request/#api-rest-servicedeskapi-request-issueidorkey-transition-post)", - version: "0.1.3", + version: "0.1.4", type: "action", ai: "optimized", annotations: { diff --git a/components/jira_service_desk/actions/update-issue-fields/update-issue-fields.mjs b/components/jira_service_desk/actions/update-issue-fields/update-issue-fields.mjs index cc13f64fbd77f..4e626745a27c5 100644 --- a/components/jira_service_desk/actions/update-issue-fields/update-issue-fields.mjs +++ b/components/jira_service_desk/actions/update-issue-fields/update-issue-fields.mjs @@ -11,7 +11,7 @@ export default { + " `fields` is a JSON object of field name-value pairs." + " Example: `{\"summary\": \"Updated title\", \"priority\": {\"name\": \"High\"}}`." + " [See the documentation](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-issueidorkey-put)", - version: "0.1.3", + version: "0.1.4", type: "action", ai: "optimized", annotations: { diff --git a/components/jira_service_desk/jira_service_desk.app.mjs b/components/jira_service_desk/jira_service_desk.app.mjs index 0e54cbeb78c29..821a0300b1099 100644 --- a/components/jira_service_desk/jira_service_desk.app.mjs +++ b/components/jira_service_desk/jira_service_desk.app.mjs @@ -1,5 +1,7 @@ -// x-pd-ai: optimized -import { axios } from "@pipedream/platform"; +import { + axios, getFileStreamAndMetadata, +} from "@pipedream/platform"; +import FormData from "form-data"; import constants from "./common/constants.mjs"; export default { @@ -280,5 +282,89 @@ export default { responseType: constants.STREAM_RESPONSE_TYPE, }); }, + async uploadTemporaryFile({ + cloudId, serviceDeskId, ...opts + }) { + return this._makeRequest({ + ...opts, + method: "POST", + path: `/ex/jira/${cloudId}/rest/servicedeskapi/servicedesk/${serviceDeskId}/attachTemporaryFile`, + }); + }, + async attachFilesToRequest({ + cloudId, issueIdOrKey, ...opts + }) { + return this._makeRequest({ + ...opts, + method: "POST", + path: `/ex/jira/${cloudId}/rest/servicedeskapi/request/${issueIdOrKey}/attachment`, + }); + }, + async deleteAttachment({ + cloudId, attachmentId, ...opts + }) { + return this._makeRequest({ + ...opts, + method: "DELETE", + path: `/ex/jira/${cloudId}/rest/api/3/attachment/${attachmentId}`, + }); + }, + /** + * Uploads one or more local/remote files as temporary attachments scoped + * to `serviceDeskId`, then attaches them to `issueIdOrKey`. Two-step dance + * required by the JSM API: a file can't be attached to a request directly. + */ + async attachFilesToRequestFromSource({ + $, cloudId, serviceDeskId, issueIdOrKey, files, isPublic, + }) { + // Uploaded one file at a time (rather than buffering every file into one shared + // FormData first) so only the small temporaryAttachmentId is retained across + // iterations instead of holding every file's full content in memory at once. + const temporaryAttachmentIds = []; + for (const file of files) { + const { + stream, metadata, + } = await getFileStreamAndMetadata(file); + // Buffered rather than piped as a live stream: attachTemporaryFile consistently + // 500'd at the Atlassian edge when the multipart body was a live Readable (verified + // against the JSM API directly), even with Content-Length set from a known size. + const chunks = []; + for await (const chunk of stream) { + chunks.push(chunk); + } + const data = new FormData(); + data.append("file", Buffer.concat(chunks), { + contentType: metadata.contentType, + filename: metadata.name, + }); + + const uploadResponse = await this.uploadTemporaryFile({ + $, + cloudId, + serviceDeskId, + headers: { + "Content-Type": `multipart/form-data; boundary=${data._boundary}`, + "Content-Length": data.getLengthSync(), + "X-Atlassian-Token": "no-check", + }, + data, + }); + temporaryAttachmentIds.push( + ...uploadResponse.temporaryAttachments.map( + ({ temporaryAttachmentId }) => temporaryAttachmentId, + ), + ); + } + + return this.attachFilesToRequest({ + $, + cloudId, + issueIdOrKey, + data: { + temporaryAttachmentIds, + public: isPublic ?? true, + }, + }); + }, }, }; diff --git a/components/jira_service_desk/package.json b/components/jira_service_desk/package.json index 775ca8df949b4..a144d6bf13a8b 100644 --- a/components/jira_service_desk/package.json +++ b/components/jira_service_desk/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/jira_service_desk", - "version": "2.1.1", + "version": "2.3.0", "description": "Pipedream Jira Service Desk Components", "main": "jira_service_desk.app.mjs", "keywords": [ @@ -13,6 +13,7 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^3.4.0" + "@pipedream/platform": "^3.4.0", + "form-data": "^4.0.4" } } diff --git a/components/jira_service_desk/sources/new-request-created/new-request-created.mjs b/components/jira_service_desk/sources/new-request-created/new-request-created.mjs index 06553fcceb252..3a3c8ea84802d 100644 --- a/components/jira_service_desk/sources/new-request-created/new-request-created.mjs +++ b/components/jira_service_desk/sources/new-request-created/new-request-created.mjs @@ -6,7 +6,7 @@ export default { name: "New Request Created", description: "Emit new event when a customer request is created. [See the documentation](https://developer.atlassian.com/cloud/jira/service-desk/rest/#api-rest-servicedeskapi-request-get)", - version: "0.1.2", + version: "0.1.3", type: "source", dedupe: "unique", methods: { diff --git a/components/jira_service_desk/sources/request-status-updated/request-status-updated.mjs b/components/jira_service_desk/sources/request-status-updated/request-status-updated.mjs index c6c923e2a8a47..4119bca43b310 100644 --- a/components/jira_service_desk/sources/request-status-updated/request-status-updated.mjs +++ b/components/jira_service_desk/sources/request-status-updated/request-status-updated.mjs @@ -6,7 +6,7 @@ export default { name: "Request Status Updated", description: "Emit new event when a customer request is updated. [See the documentation](https://developer.atlassian.com/cloud/jira/service-desk/rest/#api-rest-servicedeskapi-request-get)", - version: "0.1.2", + version: "0.1.3", type: "source", dedupe: "unique", methods: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4790c4340cf7e..29b44aa30b027 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8310,6 +8310,9 @@ importers: '@pipedream/platform': specifier: ^3.4.0 version: 3.4.0 + form-data: + specifier: 4.0.4 + version: 4.0.4 components/jlcpcb: {}