|
| 1 | +import nodefetch, { RequestInit } from "node-fetch"; |
| 2 | + |
| 3 | +interface JiraVersion { |
| 4 | + id: string; |
| 5 | + name: string; |
| 6 | + archived: boolean; |
| 7 | + released: boolean; |
| 8 | +} |
| 9 | + |
| 10 | +interface JiraProject { |
| 11 | + id: string; |
| 12 | + key: string; |
| 13 | + name: string; |
| 14 | +} |
| 15 | + |
| 16 | +interface JiraIssue { |
| 17 | + key: string; |
| 18 | + fields: { |
| 19 | + summary: string; |
| 20 | + }; |
| 21 | +} |
| 22 | + |
| 23 | +export class Jira { |
| 24 | + private projectKey: string; |
| 25 | + private baseUrl: string; |
| 26 | + private apiToken: string; |
| 27 | + |
| 28 | + private projectId: string | undefined; |
| 29 | + private projectVersions: JiraVersion[] | undefined; |
| 30 | + |
| 31 | + constructor(projectKey: string, baseUrl: string, apiToken: string) { |
| 32 | + if (!apiToken) { |
| 33 | + throw new Error("API token is required."); |
| 34 | + } |
| 35 | + this.projectKey = projectKey; |
| 36 | + this.baseUrl = baseUrl; |
| 37 | + |
| 38 | + this.apiToken = Buffer.from(apiToken).toString("base64"); // Convert to Base64 |
| 39 | + } |
| 40 | + |
| 41 | + // Private helper method for making API requests |
| 42 | + private async apiRequest<T = unknown>( |
| 43 | + method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH", |
| 44 | + endpoint: string, |
| 45 | + body?: object |
| 46 | + ): Promise<T> { |
| 47 | + const url = `${this.baseUrl}/rest/api/3${endpoint}`; |
| 48 | + const headers = { Authorization: `Basic ${this.apiToken}` }; |
| 49 | + |
| 50 | + const httpsOptions: RequestInit = { |
| 51 | + method, |
| 52 | + redirect: "follow", |
| 53 | + headers: { |
| 54 | + Accept: "application/json", |
| 55 | + ...headers, |
| 56 | + ...(body && { "Content-Type": "application/json" }) |
| 57 | + }, |
| 58 | + body: body ? JSON.stringify(body) : undefined |
| 59 | + }; |
| 60 | + |
| 61 | + let response; |
| 62 | + try { |
| 63 | + response = await nodefetch(url, httpsOptions); |
| 64 | + } catch (error) { |
| 65 | + throw new Error(`API request failed: ${(error as Error).message}`); |
| 66 | + } |
| 67 | + |
| 68 | + if (!response.ok) { |
| 69 | + throw new Error(`API request failed (${response.status}): ${response.statusText}`); |
| 70 | + } |
| 71 | + |
| 72 | + if (response.status === 204) { |
| 73 | + // No content, return empty object |
| 74 | + return {} as T; |
| 75 | + } |
| 76 | + |
| 77 | + return response.json(); |
| 78 | + } |
| 79 | + |
| 80 | + async initializeProjectData(): Promise<void> { |
| 81 | + const projectData = await this.apiRequest<JiraProject & { versions: JiraVersion[] }>( |
| 82 | + "GET", |
| 83 | + `/project/${this.projectKey}` |
| 84 | + ); |
| 85 | + |
| 86 | + this.projectId = projectData.id; // Save project ID |
| 87 | + this.projectVersions = projectData.versions.reverse(); // Save list of versions |
| 88 | + } |
| 89 | + |
| 90 | + private versions(): JiraVersion[] { |
| 91 | + if (!this.projectVersions) { |
| 92 | + throw new Error("Project versions not initialized. Call initializeProjectData() first."); |
| 93 | + } |
| 94 | + return this.projectVersions; |
| 95 | + } |
| 96 | + |
| 97 | + getVersions(): JiraVersion[] { |
| 98 | + return this.versions(); |
| 99 | + } |
| 100 | + |
| 101 | + findVersion(versionName: string): JiraVersion | undefined { |
| 102 | + return this.versions().find(version => version.name === versionName); |
| 103 | + } |
| 104 | + |
| 105 | + async createVersion(name: string): Promise<JiraVersion> { |
| 106 | + const version = await this.apiRequest<JiraVersion>("POST", `/version`, { |
| 107 | + projectId: this.projectId, |
| 108 | + name |
| 109 | + }); |
| 110 | + |
| 111 | + this.projectVersions!.unshift(version); |
| 112 | + |
| 113 | + return version; |
| 114 | + } |
| 115 | + |
| 116 | + async assignVersionToIssue(versionId: string, issueKey: string): Promise<void> { |
| 117 | + await this.apiRequest("PUT", `/issue/${issueKey}`, { |
| 118 | + fields: { |
| 119 | + fixVersions: [{ id: versionId }] |
| 120 | + } |
| 121 | + }); |
| 122 | + } |
| 123 | + |
| 124 | + async deleteVersion(versionId: string): Promise<void> { |
| 125 | + await this.apiRequest("DELETE", `/version/${versionId}`); |
| 126 | + |
| 127 | + // Remove the version from the cached project versions |
| 128 | + this.projectVersions = this.projectVersions?.filter(version => version.id !== versionId); |
| 129 | + } |
| 130 | + |
| 131 | + async getFixVersionsForIssue(issueKey: string): Promise<JiraVersion[]> { |
| 132 | + const issue = await this.apiRequest<{ fields: { fixVersions: JiraVersion[] } }>( |
| 133 | + "GET", |
| 134 | + `/issue/${issueKey}?fields=fixVersions` |
| 135 | + ); |
| 136 | + |
| 137 | + return issue.fields.fixVersions || []; |
| 138 | + } |
| 139 | + |
| 140 | + async removeFixVersionFromIssue(versionId: string, issueKey: string): Promise<void> { |
| 141 | + // First, get current fix versions |
| 142 | + const currentVersions = await this.getFixVersionsForIssue(issueKey); |
| 143 | + |
| 144 | + // Filter out the version to remove |
| 145 | + const updatedVersions = currentVersions |
| 146 | + .filter(version => version.id !== versionId) |
| 147 | + .map(version => ({ id: version.id })); |
| 148 | + |
| 149 | + // Update the issue with the filtered versions |
| 150 | + await this.apiRequest("PUT", `/issue/${issueKey}`, { |
| 151 | + fields: { |
| 152 | + fixVersions: updatedVersions |
| 153 | + } |
| 154 | + }); |
| 155 | + } |
| 156 | + |
| 157 | + private async getIssuesForVersion(versionId: string): Promise<string[]> { |
| 158 | + const issues = await this.apiRequest<{ issues: Array<{ key: string }> }>( |
| 159 | + "GET", |
| 160 | + `/search?jql=fixVersion=${versionId}` |
| 161 | + ); |
| 162 | + |
| 163 | + return issues.issues.map(issue => issue.key); |
| 164 | + } |
| 165 | + |
| 166 | + async getIssuesWithDetailsForVersion(versionId: string): Promise<JiraIssue[]> { |
| 167 | + const response = await this.apiRequest<{ issues: JiraIssue[] }>( |
| 168 | + "GET", |
| 169 | + `/search?jql=fixVersion=${versionId}&fields=summary` |
| 170 | + ); |
| 171 | + |
| 172 | + return response.issues; |
| 173 | + } |
| 174 | + |
| 175 | + async searchIssueByKey(issueKey: string): Promise<JiraIssue | null> { |
| 176 | + try { |
| 177 | + const issue = await this.apiRequest<JiraIssue>("GET", `/issue/${issueKey}?fields=summary`); |
| 178 | + return issue; |
| 179 | + } catch (_e) { |
| 180 | + // If issue not found or other error |
| 181 | + return null; |
| 182 | + } |
| 183 | + } |
| 184 | +} |
0 commit comments