import { PostBulkFetchIssuesRequestBody, PostBulkFetchIssuesResponse, GetIssueResponse, GetIssueTransitionsResponse, GetIssueTypesResponse, PostAddCommentResponse, PostBulkCreateIssueRequestBody, PostBulkCreateIssueResponse, PostCreateIssueRequestBody, PostCreateIssueResponse, PostTransitionIssueRequestBody, PutEditIssueRequestBody, PostBulkTransitionIssuesRequestBody, GetSprintResponse, GetJqlSearchIssueParams, GetJqlSearchIssueResponse } from "@business-logic/types/JiraApi"; import axios from "axios"; import axiosRetry from "axios-retry"; import FormData from 'form-data'; import path from "path"; import fs from 'fs'; const jiraUrl = process.env.JIRA_SERVER!; const jiraUsername = process.env.JIRA_USERNAME!; const jiraApiKey = process.env.JIRA_API_KEY!; const boardId = process.env.JIRA_BOARD_ID!; const encodedAuthKey = Buffer.from(`${jiraUsername}:${jiraApiKey}`).toString('base64'); export default class JiraApiUtil { readonly baseUrl: string; readonly issueUrl: string; readonly bulkIssueCreateUrl: string; readonly bulkIssueFetchUrl: string; readonly bulkTransitionIssuesUrl: string; readonly getCurrentSprintUrl: string; readonly getJqlSearchIssueUrl: string; readonly axiosClient: axios.AxiosInstance; constructor() { this.baseUrl = jiraUrl; this.issueUrl = `${this.baseUrl}/rest/api/3/issue`; this.bulkIssueCreateUrl = `${this.issueUrl}/bulk`; this.bulkIssueFetchUrl = `${this.issueUrl}/bulkfetch`; this.bulkTransitionIssuesUrl = `${this.baseUrl}/rest/api/3/bulk/issues/transition`; this.getCurrentSprintUrl = `${this.baseUrl}/rest/agile/1.0/board/${boardId}/sprint?state=active`; this.getJqlSearchIssueUrl = `${this.baseUrl}/rest/api/3/search/jql`; this.axiosClient = axios.create(); // interceptor to log error message from api this.axiosClient.interceptors.response.use( response => response, error => { console.error('Axios Error:', error?.response?.data || error.message); return Promise.reject(error); } ); // Set up retries if requests are made too quickly axiosRetry(this.axiosClient, { retries: 4, retryDelay: (retryCount) => { return Math.pow(2, retryCount) * 1000; }, // Exponential backoff retryCondition: (error) => { return error.response?.status === 429 } // If rate-limit error }); } getIssue(issueKey: string) { const url = `${this.issueUrl}/${issueKey}`; return this.axiosClient.get(url, { headers: { Authorization: `Basic ${encodedAuthKey}`, Accept: 'application/json' } }); } async getCurrentSprint() { const res = await this.axiosClient.get(this.getCurrentSprintUrl, { headers: { Authorization: `Basic ${encodedAuthKey}`, Accept: 'application/json' } }); const currentSprint = res.data.values.filter(sprint => { return `${sprint.originBoardId}` === boardId; }); if (currentSprint.length === 1) { return currentSprint[0]; } else { console.error(`JiraApiUtil >> Multiple active sprints found for board ${boardId}`); } } getJqlSearchIssue(params: GetJqlSearchIssueParams) { return this.axiosClient.get(this.getJqlSearchIssueUrl, { params: params, headers: { Authorization: `Basic ${encodedAuthKey}`, Accept: 'application/json' } }) } postBulkFetchIssues(requestBody: PostBulkFetchIssuesRequestBody) { return this.axiosClient.post(this.bulkIssueFetchUrl, requestBody, { headers: { Authorization: `Basic ${encodedAuthKey}`, 'Accept': 'application/json', 'Content-Type': 'application/json' } }); } getIssueTransitions(issueKey: string) { const url = `${this.issueUrl}/${issueKey}/transitions` return this.axiosClient.get(url, { headers: { Authorization: `Basic ${encodedAuthKey}`, Accept: 'application/json' } }); } getIssueTypes(projectKey: string) { const url = `${this.baseUrl}/rest/api/3/issue/createmeta/${projectKey}/issuetypes`; return this.axiosClient.get(url, { headers: { Authorization: `Basic ${encodedAuthKey}`, Accept: 'application/json' } }); } postAddComment(issueKey: string, commentBody: PostAddCommentResponse) { const url = `${this.issueUrl}/${issueKey}/comment`; return this.axiosClient.post(url, commentBody, { headers: { Authorization: `Basic ${encodedAuthKey}`, 'Accept': 'application/json', 'Content-Type': 'application/json' } }); } postTransitionIssue(issueKey: string, requestBody: PostTransitionIssueRequestBody) { const transitionUrl = `${this.issueUrl}/${issueKey}/transitions` return this.axiosClient.post(transitionUrl, requestBody, { headers: { Authorization: `Basic ${encodedAuthKey}`, 'Accept': 'application/json', 'Content-Type': 'application/json' }, }); } putEditIssue(issueKey: string, requestBody: PutEditIssueRequestBody) { const editIssueUrl = `${this.issueUrl}/${issueKey}` return this.axiosClient.put(editIssueUrl, requestBody, { headers: { Authorization: `Basic ${encodedAuthKey}`, 'Accept': 'application/json', 'Content-Type': 'application/json' } }); } postBulkTransitionIssues(requestBody: PostBulkTransitionIssuesRequestBody) { return this.axiosClient.post(this.bulkTransitionIssuesUrl, requestBody, { headers: { Authorization: `Basic ${encodedAuthKey}`, 'Accept': 'application/json', 'Content-Type': 'application/json' }, }); } postCreateIssue(requestBody: PostCreateIssueRequestBody) { return this.axiosClient.post(this.issueUrl, requestBody, { headers: { Authorization: `Basic ${encodedAuthKey}`, 'Accept': 'application/json', 'Content-Type': 'application/json' }, }); } postBulkCreateIssue(requestBody: PostBulkCreateIssueRequestBody) { return this.axiosClient.post(this.bulkIssueCreateUrl, requestBody, { headers: { Authorization: `Basic ${encodedAuthKey}`, 'Accept': 'application/json', 'Content-Type': 'application/json' }, }); } postUploadAttachment(issueKey: string, filePath: string) { const url = `${this.baseUrl}/rest/api/3/issue/${issueKey}/attachments` const form = new FormData(); const fileName = path.basename(filePath); form.append('file', fs.createReadStream(filePath), fileName); return this.axiosClient.post(url, form, { headers: { Authorization: `Basic ${encodedAuthKey}`, 'X-Atlassian-Token': 'no-check', ...form.getHeaders() } }); } }