From 723d35e77638effd80d08fccc640353f759dfd00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ru=CC=88sch?= Date: Sat, 11 Nov 2023 22:13:56 +0100 Subject: [PATCH 01/19] Add axios as a dependency --- package.json | 1 + yarn.lock | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/package.json b/package.json index fa4b0f52..2d64d99f 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@tanstack/react-query-devtools": "^4.23.0", "@types/file-saver": "^2.0.5", "@types/react-beautiful-dnd": "^13.1.3", + "axios": "^1.6.1", "cross-fetch": "^3.1.5", "dayjs": "^1.11.7", "dotenv": "^16.0.3", diff --git a/yarn.lock b/yarn.lock index 67344ac0..5ed5e3dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2416,6 +2416,15 @@ axe-core@^4.6.2: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.8.2.tgz#2f6f3cde40935825cf4465e3c1c9e77b240ff6ae" integrity sha512-/dlp0fxyM3R8YW7MFzaHWXrf4zzbr0vaYb23VBFCl83R7nWNPg/yaQw2Dc8jzCMmDVLhSdzH8MjrsuIUuvX+6g== +axios@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.1.tgz#76550d644bf0a2d469a01f9244db6753208397d7" + integrity sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + axobject-query@^3.1.1: version "3.2.1" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a" @@ -4275,6 +4284,11 @@ flora-colossus@^2.0.0: debug "^4.3.4" fs-extra "^10.1.0" +follow-redirects@^1.15.0: + version "1.15.3" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" + integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -6892,6 +6906,11 @@ proxy-addr@^2.0.7, proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + psl@^1.1.33: version "1.9.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" From b8bffe89c0d7f9835fda42ba693f2dd217056dbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ru=CC=88sch?= Date: Sat, 11 Nov 2023 22:14:34 +0100 Subject: [PATCH 02/19] Add jira cloud API clients --- .../jira-cloud-provider/JiraCloudProvider.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/electron/providers/jira-cloud-provider/JiraCloudProvider.ts b/electron/providers/jira-cloud-provider/JiraCloudProvider.ts index 3d65ba79..9d6f01a9 100644 --- a/electron/providers/jira-cloud-provider/JiraCloudProvider.ts +++ b/electron/providers/jira-cloud-provider/JiraCloudProvider.ts @@ -19,6 +19,7 @@ import { } from "../../../types/jira" import { IProvider } from "../base-provider" import { getAccessToken, refreshTokens } from "./getAccessToken" +import { Axios } from "axios"; export class JiraCloudProvider implements IProvider { public accessToken: string | undefined @@ -31,6 +32,27 @@ export class JiraCloudProvider implements IProvider { private reversedCustomFields = new Map() + private constructRestBasedClient(basePath: string, version: string) { + // TODO define validateStatus + // TODO catch errors and handle common status codes + return new Axios({ + baseURL: `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/${basePath}/${version}`, + headers: { + Accept: "application/json", + Authorization: `Bearer ${this.accessToken}`, + "Content-Type": "application/json", + } + }) + } + + private getRestApiClient(version: number) { + return this.constructRestBasedClient('api', version.toString()); + } + + private getAgileRestApiClient(version: string) { + return this.constructRestBasedClient('agile', version); + } + offsetDate(date: Date) { if (!date) { return date From 08f8328a9edaa15ffc1c186b813d5bb9c376a91d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ru=CC=88sch?= Date: Sat, 11 Nov 2023 22:14:52 +0100 Subject: [PATCH 03/19] Migrate trivial requests --- .../jira-cloud-provider/JiraCloudProvider.ts | 754 +++++------------- 1 file changed, 217 insertions(+), 537 deletions(-) diff --git a/electron/providers/jira-cloud-provider/JiraCloudProvider.ts b/electron/providers/jira-cloud-provider/JiraCloudProvider.ts index 9d6f01a9..9929b522 100644 --- a/electron/providers/jira-cloud-provider/JiraCloudProvider.ts +++ b/electron/providers/jira-cloud-provider/JiraCloudProvider.ts @@ -138,24 +138,17 @@ export class JiraCloudProvider implements IProvider { async mapCustomFields(): Promise { return new Promise((resolve, reject) => { - fetch( - `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/api/3/field`, - { - headers: { - Accept: "application/json", - Authorization: `Bearer ${this.accessToken}`, - }, - } - ) - .then(async (data) => { - const fetchedFields = await data.json() - if (data.status === 200) { + this.getRestApiClient(3) + .get('/field') + .then(async (response) => { + const fetchedFields = JSON.parse(response.data) + if (response.status === 200) { fetchedFields.forEach((field: { name: string; id: string }) => { this.customFields.set(field.name, field.id) this.reversedCustomFields.set(field.id, field.name) }) resolve() - } else if (data.status === 401) { + } else if (response.status === 401) { reject(new Error(`User not authenticated: ${fetchedFields}`)) } else { reject(new Error(`Unknown error: ${fetchedFields}`)) @@ -169,17 +162,10 @@ export class JiraCloudProvider implements IProvider { async getProjects(): Promise { return new Promise((resolve, reject) => { - fetch( - `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/api/3/project/search?expand=description,lead,issueTypes,url,projectKeys,permissions,insight`, - { - headers: { - Accept: "application/json", - Authorization: `Bearer ${this.accessToken}`, - }, - } - ) + this.getRestApiClient(3) + .get('/project/search?expand=description,lead,issueTypes,url,projectKeys,permissions,insight') .then(async (response) => { - const data = await response.json() + const data = JSON.parse(response.data) if (response.status === 200) { const projects = data.values.map((project: JiraProject) => ({ key: project.key, @@ -211,31 +197,21 @@ export class JiraCloudProvider implements IProvider { async getIssueTypesByProject(projectIdOrKey: string): Promise { return new Promise((resolve, reject) => { - fetch( - `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/api/2/project/${projectIdOrKey}/statuses`, - { - headers: { - Accept: "application/json", - Authorization: `Bearer ${this.accessToken}`, - }, - } - ) + this.getRestApiClient(2) + .get(`/project/${projectIdOrKey}/statuses`) .then(async (response) => { + const data = JSON.parse(response.data) if (response.status === 200) { - const issueTypes: JiraIssueType[] = await response.json() + const issueTypes: JiraIssueType[] = data resolve(issueTypes as IssueType[]) } else if (response.status === 401) { - reject( - new Error(`User not authenticated: ${await response.json()}`) - ) + reject(new Error(`User not authenticated: ${data}`)) } else if (response.status === 404) { reject( - new Error( - `The project was not found or the user does not have permission to view it: ${await response.json()}` - ) + new Error(`The project was not found or the user does not have permission to view it: ${data}`) ) } else { - reject(new Error(`Unknown error: ${await response.json()}`)) + reject(new Error(`Unknown error: ${data}`)) } }) .catch((error) => @@ -246,17 +222,10 @@ export class JiraCloudProvider implements IProvider { async getIssueTypesWithFieldsMap(): Promise<{ [key: string]: string[] }> { return new Promise((resolve, reject) => { - fetch( - `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/api/3/issue/createmeta?expand=projects.issuetypes.fields`, - { - headers: { - Accept: "application/json", - Authorization: `Bearer ${this.accessToken}`, - }, - } - ) + this.getRestApiClient(3) + .get('/issue/createmeta?expand=projects.issuetypes.fields') .then(async (response) => { - const metadata = await response.json() + const metadata = JSON.parse(response.data) if (response.status === 200) { const issueTypeToFieldsMap: { [key: string]: string[] } = {} metadata.projects.forEach( @@ -267,9 +236,9 @@ export class JiraCloudProvider implements IProvider { id: string }[] }) => { - project.issuetypes.forEach((issuetype) => { - const fieldKeys = Object.keys(issuetype.fields) - issueTypeToFieldsMap[issuetype.id] = fieldKeys.map( + project.issuetypes.forEach((issueType) => { + const fieldKeys = Object.keys(issueType.fields) + issueTypeToFieldsMap[issueType.id] = fieldKeys.map( (fieldKey) => this.reversedCustomFields.get(fieldKey)! ) }) @@ -290,17 +259,10 @@ export class JiraCloudProvider implements IProvider { async getEditableIssueFields(issueIdOrKey: string): Promise { return new Promise((resolve, reject) => { - fetch( - `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/api/3/issue/${issueIdOrKey}/editmeta`, - { - headers: { - Accept: "application/json", - Authorization: `Bearer ${this.accessToken}`, - }, - } - ) + this.getRestApiClient(3) + .get(`/issue/${issueIdOrKey}/editmeta`) .then(async (response) => { - const metadata = await response.json() + const metadata = JSON.parse(response.data) if (response.status === 200) { const fieldKeys = Object.keys(metadata.fields).map( (fieldKey) => this.reversedCustomFields.get(fieldKey)! @@ -320,44 +282,30 @@ export class JiraCloudProvider implements IProvider { async getAssignableUsersByProject(projectIdOrKey: string): Promise { return new Promise((resolve, reject) => { - fetch( - `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/api/3/user/assignable/search?project=${projectIdOrKey}`, - { - headers: { - Accept: "application/json", - Authorization: `Bearer ${this.accessToken}`, - }, - } - ) + this.getRestApiClient(3) + .get(`/user/assignable/search?project=${projectIdOrKey}`) .then(async (response) => { + const data = JSON.parse(response.data) if (response.status === 200) { - const users: User[] = await response.json() + const users: User[] = data resolve(users as User[]) } else if (response.status === 400) { - reject( - new Error(`Some infos are missing: ${await response.json()}`) - ) + reject(new Error(`Some infos are missing: ${data}`)) } else if (response.status === 401) { - reject( - new Error(`User not authenticated: ${await response.json()}`) - ) + reject(new Error(`User not authenticated: ${data}`)) } else if (response.status === 404) { reject( - new Error( - `Project, issue, or transition were not found: ${await response.json()}` - ) + new Error(`Project, issue, or transition were not found: ${data}`) ) } else if (response.status === 429) { - reject(new Error(`Rate limit exceeded: ${await response.json()}`)) + reject(new Error(`Rate limit exceeded: ${data}`)) } else { - reject(new Error(`Unknown error: ${await response.json()}`)) + reject(new Error(`Unknown error: ${data}`)) } }) .catch((error) => reject( - new Error( - `Error in fetching the assignable users for the project ${projectIdOrKey}: ${error}` - ) + new Error(`Error in fetching the assignable users for the project ${projectIdOrKey}: ${error}`) ) ) }) @@ -365,25 +313,17 @@ export class JiraCloudProvider implements IProvider { async getCurrentUser(): Promise { return new Promise((resolve, reject) => { - fetch( - `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/api/3/myself`, - { - headers: { - Accept: "application/json", - Authorization: `Bearer ${this.accessToken}`, - }, - } - ) + this.getRestApiClient(3) + .get('/myself') .then(async (response) => { + const data = response.data; if (response.status === 200) { - const user: User = await response.json() + const user: User = data resolve(user as User) } else if (response.status === 401) { - reject( - new Error(`User not authenticated: ${await response.json()}`) - ) + reject(new Error(`User not authenticated: ${data}`)) } else { - reject(new Error(`Unknown error: ${await response.json()}`)) + reject(new Error(`Unknown error: ${data}`)) } }) .catch((error) => @@ -394,26 +334,17 @@ export class JiraCloudProvider implements IProvider { async getIssueReporter(issueIdOrKey: string): Promise { return new Promise((resolve, reject) => { - fetch( - `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/api/3/issue/${issueIdOrKey}?fields=reporter`, - { - headers: { - Accept: "application/json", - Authorization: `Bearer ${this.accessToken}`, - }, - } - ) + this.getRestApiClient(3) + .get(`/issue/${issueIdOrKey}?fields=reporter`) .then(async (response) => { - const user = await response.json() + const user = JSON.parse(response.data) if (response.status === 200) { resolve(user.fields.reporter as User) } else if (response.status === 401) { reject(new Error(`User not authenticated: ${user}`)) } else if (response.status === 404) { reject( - new Error( - `The issue was not found or the user does not have permission to view it: ${user}` - ) + new Error(`The issue was not found or the user does not have permission to view it: ${user}`) ) } else { reject(new Error(`Unknown error: ${user}`)) @@ -427,17 +358,10 @@ export class JiraCloudProvider implements IProvider { async getBoardIds(project: string): Promise { return new Promise((resolve, reject) => { - fetch( - `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/agile/1.0/board?projectKeyOrId=${project}`, - { - headers: { - Accept: "application/json", - Authorization: `Bearer ${this.accessToken}`, - }, - } - ) + this.getAgileRestApiClient('1.0') + .get(`/board?projectKeyOrId=${project}`) .then(async (response) => { - const data = await response.json() + const data = JSON.parse(response.data) if (response.status === 200) { const boardIds: number[] = data.values.map( (element: { id: number; name: string }) => element.id @@ -461,17 +385,10 @@ export class JiraCloudProvider implements IProvider { async getSprints(boardId: number): Promise { return new Promise((resolve, reject) => { - fetch( - `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/agile/1.0/board/${boardId}/sprint`, - { - headers: { - Accept: "application/json", - Authorization: `Bearer ${this.accessToken}`, - }, - } - ) + this.getAgileRestApiClient('1.0') + .get('/board/${boardId}/sprint') .then(async (response) => { - const data = await response.json() + const data = JSON.parse(response.data) if (response.status === 200) { const sprints: Sprint[] = data.values .filter( @@ -632,106 +549,67 @@ export class JiraCloudProvider implements IProvider { ): Promise { return new Promise((resolve, reject) => { const rankCustomField = this.customFields.get("Rank") - const body = { - rankCustomFieldId: rankCustomField!.match(/_(\d+)/)![1], - issues: [issue], - ...(rankAfter ? { rankAfterIssue: rankAfter } : {}), - ...(rankBefore ? { rankBeforeIssue: rankBefore } : {}), - } - fetch( - `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/agile/1.0/sprint/${sprint}/issue`, - { - method: "POST", - headers: { - Accept: "application/json", - Authorization: `Bearer ${this.accessToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify(body), - } - ) + this.getAgileRestApiClient('1.0') + .post( + `/sprint/${sprint}/issue`, + { + rankCustomFieldId: rankCustomField!.match(/_(\d+)/)![1], + issues: [issue], + ...(rankAfter ? { rankAfterIssue: rankAfter } : {}), + ...(rankBefore ? { rankBeforeIssue: rankBefore } : {}), + } + ) .then(async (response) => { + const data = JSON.parse(response.data) if (response.status === 204) { resolve() } else if (response.status === 400) { - reject(new Error(`Invalid request: ${await response.json()}`)) + reject(new Error(`Invalid request: ${data}`)) } else if (response.status === 401) { - reject( - new Error(`User not authenticated: ${await response.json()}`) - ) + reject(new Error(`User not authenticated: ${data}`)) } else if (response.status === 403) { - reject( - new Error( - `User does not have a valid licence or permissions to assign issues: ${await response.json()}` - ) - ) + reject(new Error(`User does not have a valid licence or permissions to assign issues: ${data}`)) } else if (response.status === 404) { reject( - new Error( - `The sprint does not exist or the user does not have permission to view it: ${await response.json()}` - ) + new Error(`The sprint does not exist or the user does not have permission to view it: ${data}`) ) } else { - reject(new Error(`Unknown error: ${await response.json()}`)) + reject(new Error(`Unknown error: ${data}`)) } }) .catch((error) => { - reject( - new Error( - `Error in moving this issue to the Sprint with id ${sprint}: ${error}` - ) - ) + reject(new Error(`Error in moving this issue to the Sprint with id ${sprint}: ${error}`)) }) }) } async moveIssueToBacklog(issue: string): Promise { return new Promise((resolve, reject) => { - fetch( - `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/agile/1.0/backlog/issue`, - { - method: "POST", - headers: { - Accept: "application/json", - Authorization: `Bearer ${this.accessToken}`, - "Content-Type": "application/json", - }, - body: `{ - "issues": [ - "${issue}" - ] - }`, - } - ) + this.getAgileRestApiClient('1.0') + .post( + '/backlog/issue', + { issues: [issue] } + ) .then(async (response) => { + const data = JSON.parse(response.data) if (response.status === 204) { resolve() } else if (response.status === 400) { - reject(new Error(`Invalid request: ${await response.json()}`)) + reject(new Error(`Invalid request: ${data}`)) } else if (response.status === 401) { - reject( - new Error(`User not authenticated: ${await response.json()}`) - ) + reject(new Error(`User not authenticated: ${data}`)) } else if (response.status === 403) { - reject( - new Error( - `User does not have a valid licence or permissions to assign issues: ${await response.json()}` - ) - ) + reject(new Error(`User does not have a valid licence or permissions to assign issues: ${data}`)) } else if (response.status === 404) { reject( - new Error( - `The board does not exist or the user does not have permission to view it: ${await response.json()}` - ) + new Error(`The board does not exist or the user does not have permission to view it: ${data}`) ) } else { - reject(new Error(`Unknown error: ${await response.json()}`)) + reject(new Error(`Unknown error: ${data}`)) } }) .catch((error) => { - reject( - new Error(`Error in moving this issue to the backlog: ${error}`) - ) + reject(new Error(`Error in moving this issue to the backlog: ${error}`)) }) }) } @@ -759,19 +637,10 @@ export class JiraCloudProvider implements IProvider { body.rankAfterIssue = rankAfter } - fetch( - `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/agile/1.0/issue/rank`, - { - method: "PUT", - headers: { - Accept: "application/json", - Authorization: `Bearer ${this.accessToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify(body), - } - ) + this.getAgileRestApiClient('1.0') + .put('/issue/rank', body) .then(async (response) => { + const data = JSON.parse(response.data) if (response.status === 204) { resolve() } else if (response.status === 207) { @@ -779,42 +648,27 @@ export class JiraCloudProvider implements IProvider { // see documentation: https://developer.atlassian.com/cloud/jira/software/rest/api-group-issue/#api-rest-agile-1-0-issue-rank-put-responses resolve() } else if (response.status === 400) { - reject(new Error(`Invalid request: ${await response.json()}`)) + reject(new Error(`Invalid request: ${data}`)) } else if (response.status === 401) { - reject( - new Error(`User not authenticated: ${await response.json()}`) - ) + reject(new Error(`User not authenticated: ${data}`)) } else if (response.status === 403) { - reject( - new Error( - `User does not have a valid licence or permissions to rank issues: ${await response.json()}` - ) - ) + reject(new Error(`User does not have a valid licence or permissions to rank issues: ${data}`)) } else { - reject(new Error(`Unknown error: ${await response.json()}`)) + reject(new Error(`Unknown error: ${data}`)) } }) .catch((error) => { - reject( - new Error(`Error in moving this issue to the backlog: ${error}`) - ) + reject(new Error(`Error in moving this issue to the backlog: ${error}`)) }) }) } async getIssueStoryPointsEstimate(issue: string): Promise { return new Promise((resolve, reject) => { - fetch( - `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/api/3/issue/${issue}`, - { - headers: { - Accept: "application/json", - Authorization: `Bearer ${this.accessToken}`, - }, - } - ) + this.getRestApiClient(3) + .get(`/issue/${issue}`) .then(async (response) => { - const data = await response.json() + const data = JSON.parse(response.data) if (response.status === 200) { const customField = this.customFields.get("Story point estimate") const points: number = data.fields[customField!] @@ -824,20 +678,14 @@ export class JiraCloudProvider implements IProvider { reject(new Error(`User not authenticated: ${data}`)) } else if (response.status === 404) { reject( - new Error( - `The issue was not found or the user does not have permission to view it: ${data}` - ) + new Error(`The issue was not found or the user does not have permission to view it: ${data}`) ) } else { reject(new Error(`Unknown error: ${data}`)) } }) .catch((error) => - reject( - new Error( - `Error in getting the story points for issue: ${issue}: ${error}` - ) - ) + reject(new Error(`Error in getting the story points for issue: ${issue}: ${error}`)) ) }) } @@ -862,16 +710,10 @@ export class JiraCloudProvider implements IProvider { const offsetDueDate = this.offsetDate(dueDate) return new Promise((resolve, reject) => { - fetch( - `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/api/3/issue`, - { - method: "POST", - headers: { - Accept: "application/json", - Authorization: `Bearer ${this.accessToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ + this.getRestApiClient(3) + .post( + `/issue`, + { fields: { summary, parent: { key: epic }, @@ -912,31 +754,28 @@ export class JiraCloudProvider implements IProvider { }), ...(storyPointsEstimate && { [this.customFields.get("Story point estimate")!]: - storyPointsEstimate, + storyPointsEstimate, }), // ...(files && { // [this.customFields.get("Attachment")!]: files, // }), }, - }), - } - ) - .then(async (data) => { - const createdIssue = await data.json() - if (data.status === 201) { + } + ) + .then(async (response) => { + const createdIssue = JSON.parse(response.data) + if (response.status === 201) { resolve(JSON.stringify(createdIssue.key)) this.setTransition(createdIssue.id, status) } - if (data.status === 400) { + if (response.status === 400) { reject(new Error(createdIssue)) } - if (data.status === 401) { + if (response.status === 401) { reject(new Error("User not authenticated")) } - if (data.status === 403) { - reject( - new Error("The user does not have the necessary permissions") - ) + if (response.status === 403) { + reject(new Error("The user does not have the necessary permissions")) } }) .catch((error) => { @@ -967,16 +806,10 @@ export class JiraCloudProvider implements IProvider { const offsetDueDate = this.offsetDate(dueDate) return new Promise((resolve, reject) => { - fetch( - `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/api/3/issue/${issueIdOrKey}`, - { - method: "PUT", - headers: { - Accept: "application/json", - Authorization: `Bearer ${this.accessToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ + this.getRestApiClient(3) + .put( + `/issue/${issueIdOrKey}`, + { fields: { ...(summary && { summary, @@ -1033,37 +866,26 @@ export class JiraCloudProvider implements IProvider { }), ...(storyPointsEstimate !== undefined && { [this.customFields.get("Story point estimate")!]: - storyPointsEstimate, + storyPointsEstimate, }), }, - }), - } - ) + } + ) .then(async (data) => { if (data.status === 204) { resolve() } if (data.status === 400) { - reject( - new Error( - "400 Error: consult the atlassian rest api v3 under Edit issue for information" - ) - ) + reject(new Error("400 Error: consult the atlassian rest api v3 under Edit issue for information")) } if (data.status === 401) { reject(new Error("User not authenticated")) } if (data.status === 403) { - reject( - new Error("The user does not have the necessary permissions") - ) + reject(new Error("The user does not have the necessary permissions")) } if (data.status === 404) { - reject( - new Error( - "The issue was not found or the user does not have the necessary permissions" - ) - ) + reject(new Error("The issue was not found or the user does not have the necessary permissions")) } }) .catch(async (error) => { @@ -1074,50 +896,29 @@ export class JiraCloudProvider implements IProvider { async setTransition(issueKey: string, status: string): Promise { const transitions = new Map() - const transitonResponse = await fetch( - `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/api/3/issue/${issueKey}/transitions`, - { - headers: { - Accept: "application/json", - Authorization: `Bearer ${this.accessToken}`, - }, - } + const transitionResponse = await this.getRestApiClient(3).get( + `/issue/${issueKey}/transitions`, ) - const data = await transitonResponse.json() + const data = await JSON.parse(transitionResponse.data) data.transitions.forEach((field: { name: string; id: string }) => { transitions.set(field.name, field.id) }) const transitionId = +transitions.get(status)! - fetch( - `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/api/3/issue/${issueKey}/transitions`, - { - method: "POST", - headers: { - Accept: "application/json", - Authorization: `Bearer ${this.accessToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ transition: { id: transitionId } }), - } + this.getRestApiClient(3).post( + `/issue/${issueKey}/transitions`, + { transition: { id: transitionId } } ) } async getEpicsByProject(projectIdOrKey: string): Promise { return new Promise((resolve, reject) => { - fetch( - `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/api/3/search?jql=issuetype = Epic AND project = ${projectIdOrKey}`, - { - headers: { - Accept: "application/json", - Authorization: `Bearer ${this.accessToken}`, - }, - } - ) + this.getRestApiClient(3) + .get(`search?jql=issuetype = Epic AND project = ${projectIdOrKey}`) .then(async (response) => { - const epicData = await response.json() + const epicData = JSON.parse(response.data) if (response.status === 200) { const epics: Promise = Promise.all( epicData.issues.map(async (element: JiraIssue) => ({ @@ -1148,32 +949,19 @@ export class JiraCloudProvider implements IProvider { } }) .catch((error) => - reject( - new Error( - `Error in fetching the epics for the project ${projectIdOrKey}: ${error}` - ) - ) + reject(new Error(`Error in fetching the epics for the project ${projectIdOrKey}: ${error}`)) ) }) } async getLabels(): Promise { return new Promise((resolve, reject) => { - fetch( - `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/api/3/label`, - { - headers: { - Accept: "application/json", - Authorization: `Bearer ${this.accessToken}`, - }, - } - ) + this.getRestApiClient(3) + .get('/label') .then(async (response) => { - const labelData = await response.json() + const labelData = JSON.parse(response.data) if (response.status === 200) { - const labels: Promise = labelData.values - - resolve(labels) + resolve(labelData.values) } else if (response.status === 401) { reject(new Error(`User not authenticated: ${labelData}`)) } else { @@ -1190,26 +978,17 @@ export class JiraCloudProvider implements IProvider { // WARNING: currently (15.03.2023) GET /rest/api/3/priority is deprecated // and GET /rest/api/3/priority/search is experimental return new Promise((resolve, reject) => { - fetch( - `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/api/3/priority/search`, - { - headers: { - Accept: "application/json", - Authorization: `Bearer ${this.accessToken}`, - }, - } - ) + this.getRestApiClient(3) + .get('/priority/search') .then(async (response) => { + const priorityData: JiraPriority = JSON.parse(response.data) if (response.status === 200) { - const priorityData: JiraPriority = await response.json() const priorities: Priority[] = priorityData.values resolve(priorities) } else if (response.status === 401) { - reject( - new Error(`User not authenticated: ${await response.json()}`) - ) + reject(new Error(`User not authenticated: ${priorityData}`)) } else { - reject(new Error(`Unknown error: ${await response.json()}`)) + reject(new Error(`Unknown error: ${priorityData}`)) } }) .catch((error) => @@ -1222,36 +1001,28 @@ export class JiraCloudProvider implements IProvider { issueIdOrKey: string, commentText: string ): Promise { - const bodyData = `{ - "body": { - "content": [ + return new Promise((resolve, reject) => { + this.getRestApiClient(3) + .post( + `/issue/${issueIdOrKey}/comment`, { - "content": [ - { - "text": "${commentText.replace(/\n/g, " ")}", - "type": "text" - } - ], - "type": "paragraph" + body: { + content: [ + { + content: [ + { + text: commentText.replace(/\n/g, " "), + type: "text" + } + ], + type: "paragraph" + } + ], + type: "doc", + version: 1 + } } - ], - "type": "doc", - "version": 1 - }}` - - return new Promise((resolve, reject) => { - fetch( - `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/api/3/issue/${issueIdOrKey}/comment`, - { - method: "POST", - headers: { - Accept: "application/json", - Authorization: `Bearer ${this.accessToken}`, - "Content-Type": "application/json", - }, - body: bodyData, - } - ) + ) .then(async (data) => { if (data.status === 201) { resolve() @@ -1263,19 +1034,11 @@ export class JiraCloudProvider implements IProvider { reject(new Error("User not authenticated")) } if (data.status === 404) { - reject( - new Error( - "The issue was not found or the user does not have the necessary permissions" - ) - ) + reject(new Error("The issue was not found or the user does not have the necessary permissions")) } }) .catch(async (error) => { - reject( - new Error( - `Error adding a comment to the issue ${issueIdOrKey}: ${error}` - ) - ) + reject(new Error(`Error adding a comment to the issue ${issueIdOrKey}: ${error}`)) }) }) } @@ -1285,165 +1048,103 @@ export class JiraCloudProvider implements IProvider { commentId: string, commentText: string ): Promise { - const bodyData = `{ - "body": { - "content": [ + return new Promise((resolve, reject) => { + this.getRestApiClient(3) + .put( + `/issue/${issueIdOrKey}/comment/${commentId}`, { - "content": [ - { - "text": "${commentText.replace(/\n/g, " ")}", - "type": "text" - } - ], - "type": "paragraph" + body: { + content: [ + { + content: [ + { + text: commentText.replace(/\n/g, " "), + type: "text" + } + ], + type: "paragraph" + } + ], + type: "doc", + version: 1 + } } - ], - "type": "doc", - "version": 1 - }}` - - return new Promise((resolve, reject) => { - fetch( - `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/api/3/issue/${issueIdOrKey}/comment/${commentId}`, - { - method: "PUT", - headers: { - Accept: "application/json", - Authorization: `Bearer ${this.accessToken}`, - "Content-Type": "application/json", - }, - body: bodyData, - } - ) + ) .then(async (data) => { if (data.status === 200) { resolve() } if (data.status === 400) { reject( - new Error( - "The user does not have permission to edit the comment or the request is invalid" - ) + new Error("The user does not have permission to edit the comment or the request is invalid") ) } if (data.status === 401) { reject(new Error("User not authenticated")) } if (data.status === 404) { - reject( - new Error( - "The issue was not found or the user does not have the necessary permissions" - ) - ) + reject(new Error("The issue was not found or the user does not have the necessary permissions")) } }) .catch(async (error) => { - reject( - new Error( - `Error editing the comment in issue ${issueIdOrKey}: ${error}` - ) - ) + reject(new Error(`Error editing the comment in issue ${issueIdOrKey}: ${error}`)) }) }) } deleteIssueComment(issueIdOrKey: string, commentId: string): Promise { return new Promise((resolve, reject) => { - fetch( - `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/api/3/issue/${issueIdOrKey}/comment/${commentId}`, - { - method: "DELETE", - headers: { - Accept: "application/json", - Authorization: `Bearer ${this.accessToken}`, - }, - } - ) + this.getRestApiClient(3) + .delete(`/issue/${issueIdOrKey}/comment/${commentId}`) .then(async (data) => { if (data.status === 204) { resolve() } if (data.status === 400) { - reject( - new Error( - "The user does not have permission to delete the comment" - ) - ) + reject(new Error("The user does not have permission to delete the comment")) } if (data.status === 401) { reject(new Error("User not authenticated")) } if (data.status === 404) { - reject( - new Error( - "The issue was not found or the user does not have the necessary permissions" - ) - ) + reject(new Error("The issue was not found or the user does not have the necessary permissions")) } if (data.status === 405) { - reject( - new Error("An anonymous call has been made to the operation") - ) + reject(new Error("An anonymous call has been made to the operation")) } }) .catch(async (error) => { - reject( - new Error( - `Error editing the comment in issue ${issueIdOrKey}: ${error}` - ) - ) + reject(new Error(`Error editing the comment in issue ${issueIdOrKey}: ${error}`)) }) }) } deleteIssue(issueIdOrKey: string): Promise { return new Promise((resolve, reject) => { - fetch( - `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/api/2/issue/${issueIdOrKey}`, - { - method: "DELETE", - headers: { - Accept: "application/json", - Authorization: `Bearer ${this.accessToken}`, - }, - } - ) + this.getRestApiClient(2) + .delete(`/issue/${issueIdOrKey}`) .then(async (data) => { if (data.status === 204) { resolve() } if (data.status === 400) { - reject( - new Error( - "The issue has subtasks and deleteSubtasks is not set to true" - ) - ) + reject(new Error("The issue has subtasks and deleteSubtasks is not set to true")) } if (data.status === 401) { reject(new Error("User not authenticated")) } if (data.status === 403) { - reject( - new Error("The user does not have permission to delete the issue") - ) + reject(new Error("The user does not have permission to delete the issue")) } if (data.status === 404) { - reject( - new Error( - "The issue was not found or the user does not have the necessary permissions" - ) - ) + reject(new Error("The issue was not found or the user does not have the necessary permissions")) } if (data.status === 405) { - reject( - new Error("An anonymous call has been made to the operation") - ) + reject(new Error("An anonymous call has been made to the operation")) } }) .catch(async (error) => { - reject( - new Error(`Error deleting the subtask ${issueIdOrKey}: ${error}`) - ) + reject(new Error(`Error deleting the subtask ${issueIdOrKey}: ${error}`)) }) }) } @@ -1455,16 +1156,10 @@ export class JiraCloudProvider implements IProvider { subtaskIssueTypeId: string ): Promise<{ id: string; key: string }> { return new Promise((resolve, reject) => { - fetch( - `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/api/3/issue/`, - { - method: "POST", - headers: { - Accept: "application/json", - Authorization: `Bearer ${this.accessToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ + this.getRestApiClient(3) + .post( + '/issue', + { fields: { summary: subtaskSummary, issuetype: { @@ -1477,26 +1172,20 @@ export class JiraCloudProvider implements IProvider { id: projectId, }, }, - }), - } - ) - .then(async (data) => { - if (data.status === 201) { - const createdSubtask: { id: string; key: string } = - await data.json() + } + ) + .then(async (response) => { + const createdSubtask: { id: string; key: string } = JSON.parse(response.data) + if (response.status === 201) { resolve(createdSubtask) - } else if (data.status === 400) { - reject(new Error(`Invalid request: ${await data.json()}`)) - } else if (data.status === 401) { - reject(new Error(`User not authenticated: ${await data.json()}`)) - } else if (data.status === 403) { - reject( - new Error( - `User does not have a valid licence: ${await data.json()}` - ) - ) + } else if (response.status === 400) { + reject(new Error(`Invalid request: ${createdSubtask}`)) + } else if (response.status === 401) { + reject(new Error(`User not authenticated: ${createdSubtask}`)) + } else if (response.status === 403) { + reject(new Error(`User does not have a valid licence: ${createdSubtask}`)) } else { - reject(new Error(`Unknown error: ${await data.json()}`)) + reject(new Error(`Unknown error: ${createdSubtask}`)) } }) .catch((error) => { @@ -1530,16 +1219,10 @@ export class JiraCloudProvider implements IProvider { const offsetEndDate = this.offsetDate(endDate) return new Promise((resolve, reject) => { - fetch( - `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/agile/1.0/sprint`, - { - method: "POST", - headers: { - Accept: "application/json", - Authorization: `Bearer ${this.accessToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ + this.getAgileRestApiClient('1.0') + .post( + '/sprint', + { name, originBoardId, ...(offsetStartDate && { @@ -1549,9 +1232,8 @@ export class JiraCloudProvider implements IProvider { endDate: offsetEndDate, }), ...(goal && { goal }), - }), - } - ) + } + ) .then(async (data) => { if (data.status === 201) { resolve() @@ -1563,9 +1245,7 @@ export class JiraCloudProvider implements IProvider { reject(new Error("User not authenticated")) } if (data.status === 403) { - reject( - new Error("The user does not have the necessary permissions") - ) + reject(new Error("The user does not have the necessary permissions")) } if (data.status === 404) { reject( From fe98be662b81cf31d896bb224f0d2cc13a9eca57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ru=CC=88sch?= Date: Sat, 11 Nov 2023 22:33:25 +0100 Subject: [PATCH 04/19] Migrate non-trivial issue functionality --- .../jira-cloud-provider/JiraCloudProvider.ts | 122 ++++++++---------- 1 file changed, 52 insertions(+), 70 deletions(-) diff --git a/electron/providers/jira-cloud-provider/JiraCloudProvider.ts b/electron/providers/jira-cloud-provider/JiraCloudProvider.ts index 9929b522..51bdd504 100644 --- a/electron/providers/jira-cloud-provider/JiraCloudProvider.ts +++ b/electron/providers/jira-cloud-provider/JiraCloudProvider.ts @@ -19,7 +19,7 @@ import { } from "../../../types/jira" import { IProvider } from "../base-provider" import { getAccessToken, refreshTokens } from "./getAccessToken" -import { Axios } from "axios"; +import { Axios, AxiosResponse } from "axios"; export class JiraCloudProvider implements IProvider { public accessToken: string | undefined @@ -436,11 +436,10 @@ export class JiraCloudProvider implements IProvider { async getIssuesByProject(project: string): Promise { return new Promise((resolve, reject) => { - this.fetchIssues( - `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/api/3/search?jql=project=${project}&maxResults=10000` - ) + this.getRestApiClient(3) + .get(`/search?jql=project=${project}&maxResults=10000`) .then(async (response) => { - resolve(response) + resolve(this.fetchIssues(response)) }) .catch((error) => { reject(new Error(`Error fetching issues by project: ${error}`)) @@ -450,11 +449,10 @@ export class JiraCloudProvider implements IProvider { async getIssuesBySprint(sprintId: number): Promise { return new Promise((resolve, reject) => { - this.fetchIssues( - `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/agile/1.0/sprint/${sprintId}/issue` - ) + this.getAgileRestApiClient('1.0') + .get(`/sprint/${sprintId}/issue`) .then(async (response) => { - resolve(response) + resolve(this.fetchIssues(response)) }) .catch((error) => { reject(new Error(`Error fetching issues by sprint: ${error}`)) @@ -467,11 +465,10 @@ export class JiraCloudProvider implements IProvider { boardId: number ): Promise { return new Promise((resolve, reject) => { - this.fetchIssues( - `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/agile/1.0/board/${boardId}/backlog?jql=project=${project}&maxResults=500` - ) + this.getAgileRestApiClient('1.0') + .get(`/board/${boardId}/backlog?jql=project=${project}&maxResults=500`) .then(async (response) => { - resolve(response) + resolve(this.fetchIssues(response)) }) .catch((error) => { reject(new Error(`Error fetching issues by project: ${error}`)) @@ -479,65 +476,50 @@ export class JiraCloudProvider implements IProvider { }) } - async fetchIssues(url: string): Promise { + async fetchIssues(response: AxiosResponse): Promise { const rankCustomField = this.customFields.get("Rank") || "" return new Promise((resolve, reject) => { - fetch(url, { - headers: { - Accept: "application/json", - Authorization: `Bearer ${this.accessToken}`, - }, - }) - .then(async (response) => { - const data = await response.json() - if (response.status === 200) { - const issues: Promise = Promise.all( - data.issues.map(async (element: JiraIssue) => ({ - issueKey: element.key, - summary: element.fields.summary, - creator: element.fields.creator.displayName, - status: element.fields.status.name, - type: element.fields.issuetype.name, - storyPointsEstimate: await this.getIssueStoryPointsEstimate( - element.key - ), - epic: element.fields.parent?.fields.summary, - labels: element.fields.labels, - assignee: { - displayName: element.fields.assignee?.displayName, - avatarUrls: element.fields.assignee?.avatarUrls, - }, - rank: element.fields[rankCustomField], - description: element.fields.description, - subtasks: element.fields.subtasks, - created: element.fields.created, - updated: element.fields.updated, - comment: element.fields.comment, - projectId: element.fields.project.id, - sprint: element.fields.sprint, - attachments: element.fields.attachment, - })) - ) - resolve(issues) - } else if (response.status === 400) { - reject(new Error(`Invalid request: ${data}`)) - } else if (response.status === 401) { - reject(new Error(`User not authenticated: ${data}`)) - } else if (response.status === 403) { - reject(new Error(`User does not have a valid licence: ${data}`)) - } else if (response.status === 404) { - reject( - new Error( - `The board does not exist or the user does not have permission to view it: ${data}` - ) - ) - } else { - reject(new Error(`Unknown error: ${data}`)) - } - }) - .catch((error) => { - reject(new Error(`Error fetching issues: ${error}`)) - }) + const data = JSON.parse(response.data) + if (response.status === 200) { + const issues: Promise = Promise.all( + data.issues.map(async (element: JiraIssue) => ({ + issueKey: element.key, + summary: element.fields.summary, + creator: element.fields.creator.displayName, + status: element.fields.status.name, + type: element.fields.issuetype.name, + storyPointsEstimate: await this.getIssueStoryPointsEstimate(element.key), + epic: element.fields.parent?.fields.summary, + labels: element.fields.labels, + assignee: { + displayName: element.fields.assignee?.displayName, + avatarUrls: element.fields.assignee?.avatarUrls, + }, + rank: element.fields[rankCustomField], + description: element.fields.description, + subtasks: element.fields.subtasks, + created: element.fields.created, + updated: element.fields.updated, + comment: element.fields.comment, + projectId: element.fields.project.id, + sprint: element.fields.sprint, + attachments: element.fields.attachment, + })) + ) + resolve(issues) + } else if (response.status === 400) { + reject(new Error(`Invalid request: ${data}`)) + } else if (response.status === 401) { + reject(new Error(`User not authenticated: ${data}`)) + } else if (response.status === 403) { + reject(new Error(`User does not have a valid licence: ${data}`)) + } else if (response.status === 404) { + reject( + new Error(`The board does not exist or the user does not have permission to view it: ${data}`) + ) + } else { + reject(new Error(`Unknown error: ${data}`)) + } }) } From c2f73d2f4365b84f762aa4f1f28bc1c99e652b5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ru=CC=88sch?= Date: Sat, 11 Nov 2023 23:13:44 +0100 Subject: [PATCH 05/19] Resolve issues with default axios handlers --- .../jira-cloud-provider/JiraCloudProvider.ts | 135 +++++++++--------- 1 file changed, 67 insertions(+), 68 deletions(-) diff --git a/electron/providers/jira-cloud-provider/JiraCloudProvider.ts b/electron/providers/jira-cloud-provider/JiraCloudProvider.ts index 51bdd504..93a7c30d 100644 --- a/electron/providers/jira-cloud-provider/JiraCloudProvider.ts +++ b/electron/providers/jira-cloud-provider/JiraCloudProvider.ts @@ -19,7 +19,8 @@ import { } from "../../../types/jira" import { IProvider } from "../base-provider" import { getAccessToken, refreshTokens } from "./getAccessToken" -import { Axios, AxiosResponse } from "axios"; +import { AxiosResponse } from "axios" +import axios from "axios" export class JiraCloudProvider implements IProvider { public accessToken: string | undefined @@ -35,7 +36,7 @@ export class JiraCloudProvider implements IProvider { private constructRestBasedClient(basePath: string, version: string) { // TODO define validateStatus // TODO catch errors and handle common status codes - return new Axios({ + return axios.create({ baseURL: `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/${basePath}/${version}`, headers: { Accept: "application/json", @@ -141,17 +142,16 @@ export class JiraCloudProvider implements IProvider { this.getRestApiClient(3) .get('/field') .then(async (response) => { - const fetchedFields = JSON.parse(response.data) if (response.status === 200) { - fetchedFields.forEach((field: { name: string; id: string }) => { + response.data.forEach((field: { name: string; id: string }) => { this.customFields.set(field.name, field.id) this.reversedCustomFields.set(field.id, field.name) }) resolve() } else if (response.status === 401) { - reject(new Error(`User not authenticated: ${fetchedFields}`)) + reject(new Error(`User not authenticated: ${response.data}`)) } else { - reject(new Error(`Unknown error: ${fetchedFields}`)) + reject(new Error(`Unknown error: ${response.data}`)) } }) .catch((error) => { @@ -165,7 +165,7 @@ export class JiraCloudProvider implements IProvider { this.getRestApiClient(3) .get('/project/search?expand=description,lead,issueTypes,url,projectKeys,permissions,insight') .then(async (response) => { - const data = JSON.parse(response.data) + const data = response.data if (response.status === 200) { const projects = data.values.map((project: JiraProject) => ({ key: project.key, @@ -200,7 +200,7 @@ export class JiraCloudProvider implements IProvider { this.getRestApiClient(2) .get(`/project/${projectIdOrKey}/statuses`) .then(async (response) => { - const data = JSON.parse(response.data) + const data = response.data if (response.status === 200) { const issueTypes: JiraIssueType[] = data resolve(issueTypes as IssueType[]) @@ -225,7 +225,7 @@ export class JiraCloudProvider implements IProvider { this.getRestApiClient(3) .get('/issue/createmeta?expand=projects.issuetypes.fields') .then(async (response) => { - const metadata = JSON.parse(response.data) + const metadata = response.data if (response.status === 200) { const issueTypeToFieldsMap: { [key: string]: string[] } = {} metadata.projects.forEach( @@ -262,7 +262,7 @@ export class JiraCloudProvider implements IProvider { this.getRestApiClient(3) .get(`/issue/${issueIdOrKey}/editmeta`) .then(async (response) => { - const metadata = JSON.parse(response.data) + const metadata = response.data if (response.status === 200) { const fieldKeys = Object.keys(metadata.fields).map( (fieldKey) => this.reversedCustomFields.get(fieldKey)! @@ -285,7 +285,7 @@ export class JiraCloudProvider implements IProvider { this.getRestApiClient(3) .get(`/user/assignable/search?project=${projectIdOrKey}`) .then(async (response) => { - const data = JSON.parse(response.data) + const data = response.data if (response.status === 200) { const users: User[] = data resolve(users as User[]) @@ -316,7 +316,7 @@ export class JiraCloudProvider implements IProvider { this.getRestApiClient(3) .get('/myself') .then(async (response) => { - const data = response.data; + const data = response.data if (response.status === 200) { const user: User = data resolve(user as User) @@ -337,7 +337,7 @@ export class JiraCloudProvider implements IProvider { this.getRestApiClient(3) .get(`/issue/${issueIdOrKey}?fields=reporter`) .then(async (response) => { - const user = JSON.parse(response.data) + const user = response.data if (response.status === 200) { resolve(user.fields.reporter as User) } else if (response.status === 401) { @@ -361,7 +361,7 @@ export class JiraCloudProvider implements IProvider { this.getAgileRestApiClient('1.0') .get(`/board?projectKeyOrId=${project}`) .then(async (response) => { - const data = JSON.parse(response.data) + const data = response.data if (response.status === 200) { const boardIds: number[] = data.values.map( (element: { id: number; name: string }) => element.id @@ -386,11 +386,10 @@ export class JiraCloudProvider implements IProvider { async getSprints(boardId: number): Promise { return new Promise((resolve, reject) => { this.getAgileRestApiClient('1.0') - .get('/board/${boardId}/sprint') + .get(`/board/${boardId}/sprint`) .then(async (response) => { - const data = JSON.parse(response.data) if (response.status === 200) { - const sprints: Sprint[] = data.values + const sprints: Sprint[] = response.data.values .filter( (element: { state: string }) => element.state !== "closed" ) @@ -413,19 +412,19 @@ export class JiraCloudProvider implements IProvider { }) resolve(sprints) } else if (response.status === 400) { - reject(new Error(`Invalid request: ${data}`)) + reject(new Error(`Invalid request: ${response.data}`)) } else if (response.status === 401) { - reject(new Error(`User not authenticated: ${data}`)) + reject(new Error(`User not authenticated: ${response.data}`)) } else if (response.status === 403) { - reject(new Error(`User does not have a valid licence: ${data}`)) + reject(new Error(`User does not have a valid licence: ${response.data}`)) } else if (response.status === 404) { reject( new Error( - `The board does not exist or the user does not have permission to view it: ${data}` + `The board does not exist or the user does not have permission to view it: ${response.data}` ) ) } else { - reject(new Error(`Unknown error: ${data}`)) + reject(new Error(`Unknown error: ${response.data}`)) } }) .catch((error) => { @@ -479,7 +478,7 @@ export class JiraCloudProvider implements IProvider { async fetchIssues(response: AxiosResponse): Promise { const rankCustomField = this.customFields.get("Rank") || "" return new Promise((resolve, reject) => { - const data = JSON.parse(response.data) + const data = response.data if (response.status === 200) { const issues: Promise = Promise.all( data.issues.map(async (element: JiraIssue) => ({ @@ -542,7 +541,7 @@ export class JiraCloudProvider implements IProvider { } ) .then(async (response) => { - const data = JSON.parse(response.data) + const data = response.data if (response.status === 204) { resolve() } else if (response.status === 400) { @@ -573,7 +572,7 @@ export class JiraCloudProvider implements IProvider { { issues: [issue] } ) .then(async (response) => { - const data = JSON.parse(response.data) + const data = response.data if (response.status === 204) { resolve() } else if (response.status === 400) { @@ -622,7 +621,7 @@ export class JiraCloudProvider implements IProvider { this.getAgileRestApiClient('1.0') .put('/issue/rank', body) .then(async (response) => { - const data = JSON.parse(response.data) + const data = response.data if (response.status === 204) { resolve() } else if (response.status === 207) { @@ -650,7 +649,7 @@ export class JiraCloudProvider implements IProvider { this.getRestApiClient(3) .get(`/issue/${issue}`) .then(async (response) => { - const data = JSON.parse(response.data) + const data = response.data if (response.status === 200) { const customField = this.customFields.get("Story point estimate") const points: number = data.fields[customField!] @@ -745,7 +744,7 @@ export class JiraCloudProvider implements IProvider { } ) .then(async (response) => { - const createdIssue = JSON.parse(response.data) + const createdIssue = response.data if (response.status === 201) { resolve(JSON.stringify(createdIssue.key)) this.setTransition(createdIssue.id, status) @@ -853,20 +852,20 @@ export class JiraCloudProvider implements IProvider { }, } ) - .then(async (data) => { - if (data.status === 204) { + .then(async (response) => { + if (response.status === 204) { resolve() } - if (data.status === 400) { + if (response.status === 400) { reject(new Error("400 Error: consult the atlassian rest api v3 under Edit issue for information")) } - if (data.status === 401) { + if (response.status === 401) { reject(new Error("User not authenticated")) } - if (data.status === 403) { + if (response.status === 403) { reject(new Error("The user does not have the necessary permissions")) } - if (data.status === 404) { + if (response.status === 404) { reject(new Error("The issue was not found or the user does not have the necessary permissions")) } }) @@ -882,7 +881,7 @@ export class JiraCloudProvider implements IProvider { `/issue/${issueKey}/transitions`, ) - const data = await JSON.parse(transitionResponse.data) + const data = transitionResponse.data data.transitions.forEach((field: { name: string; id: string }) => { transitions.set(field.name, field.id) @@ -900,7 +899,7 @@ export class JiraCloudProvider implements IProvider { this.getRestApiClient(3) .get(`search?jql=issuetype = Epic AND project = ${projectIdOrKey}`) .then(async (response) => { - const epicData = JSON.parse(response.data) + const epicData = response.data if (response.status === 200) { const epics: Promise = Promise.all( epicData.issues.map(async (element: JiraIssue) => ({ @@ -941,7 +940,7 @@ export class JiraCloudProvider implements IProvider { this.getRestApiClient(3) .get('/label') .then(async (response) => { - const labelData = JSON.parse(response.data) + const labelData = response.data if (response.status === 200) { resolve(labelData.values) } else if (response.status === 401) { @@ -963,7 +962,7 @@ export class JiraCloudProvider implements IProvider { this.getRestApiClient(3) .get('/priority/search') .then(async (response) => { - const priorityData: JiraPriority = JSON.parse(response.data) + const priorityData: JiraPriority = response.data if (response.status === 200) { const priorities: Priority[] = priorityData.values resolve(priorities) @@ -1005,17 +1004,17 @@ export class JiraCloudProvider implements IProvider { } } ) - .then(async (data) => { - if (data.status === 201) { + .then(async (response) => { + if (response.status === 201) { resolve() } - if (data.status === 400) { + if (response.status === 400) { reject(new Error("Invalid api request")) } - if (data.status === 401) { + if (response.status === 401) { reject(new Error("User not authenticated")) } - if (data.status === 404) { + if (response.status === 404) { reject(new Error("The issue was not found or the user does not have the necessary permissions")) } }) @@ -1052,19 +1051,19 @@ export class JiraCloudProvider implements IProvider { } } ) - .then(async (data) => { - if (data.status === 200) { + .then(async (response) => { + if (response.status === 200) { resolve() } - if (data.status === 400) { + if (response.status === 400) { reject( new Error("The user does not have permission to edit the comment or the request is invalid") ) } - if (data.status === 401) { + if (response.status === 401) { reject(new Error("User not authenticated")) } - if (data.status === 404) { + if (response.status === 404) { reject(new Error("The issue was not found or the user does not have the necessary permissions")) } }) @@ -1078,20 +1077,20 @@ export class JiraCloudProvider implements IProvider { return new Promise((resolve, reject) => { this.getRestApiClient(3) .delete(`/issue/${issueIdOrKey}/comment/${commentId}`) - .then(async (data) => { - if (data.status === 204) { + .then(async (response) => { + if (response.status === 204) { resolve() } - if (data.status === 400) { + if (response.status === 400) { reject(new Error("The user does not have permission to delete the comment")) } - if (data.status === 401) { + if (response.status === 401) { reject(new Error("User not authenticated")) } - if (data.status === 404) { + if (response.status === 404) { reject(new Error("The issue was not found or the user does not have the necessary permissions")) } - if (data.status === 405) { + if (response.status === 405) { reject(new Error("An anonymous call has been made to the operation")) } }) @@ -1105,23 +1104,23 @@ export class JiraCloudProvider implements IProvider { return new Promise((resolve, reject) => { this.getRestApiClient(2) .delete(`/issue/${issueIdOrKey}`) - .then(async (data) => { - if (data.status === 204) { + .then(async (response) => { + if (response.status === 204) { resolve() } - if (data.status === 400) { + if (response.status === 400) { reject(new Error("The issue has subtasks and deleteSubtasks is not set to true")) } - if (data.status === 401) { + if (response.status === 401) { reject(new Error("User not authenticated")) } - if (data.status === 403) { + if (response.status === 403) { reject(new Error("The user does not have permission to delete the issue")) } - if (data.status === 404) { + if (response.status === 404) { reject(new Error("The issue was not found or the user does not have the necessary permissions")) } - if (data.status === 405) { + if (response.status === 405) { reject(new Error("An anonymous call has been made to the operation")) } }) @@ -1157,7 +1156,7 @@ export class JiraCloudProvider implements IProvider { } ) .then(async (response) => { - const createdSubtask: { id: string; key: string } = JSON.parse(response.data) + const createdSubtask: { id: string; key: string } = response.data if (response.status === 201) { resolve(createdSubtask) } else if (response.status === 400) { @@ -1216,20 +1215,20 @@ export class JiraCloudProvider implements IProvider { ...(goal && { goal }), } ) - .then(async (data) => { - if (data.status === 201) { + .then(async (response) => { + if (response.status === 201) { resolve() } - if (data.status === 400) { + if (response.status === 400) { reject(new Error("Invalid request")) } - if (data.status === 401) { + if (response.status === 401) { reject(new Error("User not authenticated")) } - if (data.status === 403) { + if (response.status === 403) { reject(new Error("The user does not have the necessary permissions")) } - if (data.status === 404) { + if (response.status === 404) { reject( new Error( "The Board does not exists or the user does not have the necessary permissions to view it" From 359f1bb3b3ee42c5f19f05649ce1dcc91019d188 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ru=CC=88sch?= Date: Sat, 11 Nov 2023 23:32:19 +0100 Subject: [PATCH 06/19] Universally handle 401 errors --- .../jira-cloud-provider/JiraCloudProvider.ts | 121 ++++-------------- 1 file changed, 28 insertions(+), 93 deletions(-) diff --git a/electron/providers/jira-cloud-provider/JiraCloudProvider.ts b/electron/providers/jira-cloud-provider/JiraCloudProvider.ts index 93a7c30d..018e0d6d 100644 --- a/electron/providers/jira-cloud-provider/JiraCloudProvider.ts +++ b/electron/providers/jira-cloud-provider/JiraCloudProvider.ts @@ -34,16 +34,26 @@ export class JiraCloudProvider implements IProvider { private reversedCustomFields = new Map() private constructRestBasedClient(basePath: string, version: string) { - // TODO define validateStatus // TODO catch errors and handle common status codes - return axios.create({ + const instance = axios.create({ baseURL: `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/${basePath}/${version}`, headers: { Accept: "application/json", Authorization: `Bearer ${this.accessToken}`, "Content-Type": "application/json", + }, + validateStatus: (statusCode) => statusCode < 500, + }) + + instance.interceptors.response.use((response) => { + if (response.status === 401) { + return Promise.reject(new Error(`User not authenticated: ${JSON.stringify(response.data)}`)) } + + return response }) + + return instance } private getRestApiClient(version: number) { @@ -148,8 +158,6 @@ export class JiraCloudProvider implements IProvider { this.reversedCustomFields.set(field.id, field.name) }) resolve() - } else if (response.status === 401) { - reject(new Error(`User not authenticated: ${response.data}`)) } else { reject(new Error(`Unknown error: ${response.data}`)) } @@ -177,8 +185,6 @@ export class JiraCloudProvider implements IProvider { resolve(projects) } else if (response.status === 400) { reject(new Error(`Invalid request: ${data}`)) - } else if (response.status === 401) { - reject(new Error(`User not authenticated: ${data}`)) } else if (response.status === 404) { reject( new Error( @@ -204,8 +210,6 @@ export class JiraCloudProvider implements IProvider { if (response.status === 200) { const issueTypes: JiraIssueType[] = data resolve(issueTypes as IssueType[]) - } else if (response.status === 401) { - reject(new Error(`User not authenticated: ${data}`)) } else if (response.status === 404) { reject( new Error(`The project was not found or the user does not have permission to view it: ${data}`) @@ -245,8 +249,6 @@ export class JiraCloudProvider implements IProvider { } ) resolve(issueTypeToFieldsMap) - } else if (response.status === 401) { - reject(new Error(`User not authenticated: ${metadata}`)) } else { reject(new Error(`Unknown error: ${metadata}`)) } @@ -268,8 +270,6 @@ export class JiraCloudProvider implements IProvider { (fieldKey) => this.reversedCustomFields.get(fieldKey)! ) resolve(fieldKeys) - } else if (response.status === 401) { - reject(new Error(`User not authenticated: ${metadata}`)) } else { reject(new Error(`Unknown error: ${metadata}`)) } @@ -291,8 +291,6 @@ export class JiraCloudProvider implements IProvider { resolve(users as User[]) } else if (response.status === 400) { reject(new Error(`Some infos are missing: ${data}`)) - } else if (response.status === 401) { - reject(new Error(`User not authenticated: ${data}`)) } else if (response.status === 404) { reject( new Error(`Project, issue, or transition were not found: ${data}`) @@ -320,8 +318,6 @@ export class JiraCloudProvider implements IProvider { if (response.status === 200) { const user: User = data resolve(user as User) - } else if (response.status === 401) { - reject(new Error(`User not authenticated: ${data}`)) } else { reject(new Error(`Unknown error: ${data}`)) } @@ -340,8 +336,6 @@ export class JiraCloudProvider implements IProvider { const user = response.data if (response.status === 200) { resolve(user.fields.reporter as User) - } else if (response.status === 401) { - reject(new Error(`User not authenticated: ${user}`)) } else if (response.status === 404) { reject( new Error(`The issue was not found or the user does not have permission to view it: ${user}`) @@ -369,8 +363,6 @@ export class JiraCloudProvider implements IProvider { resolve(boardIds) } else if (response.status === 400) { reject(new Error(`Invalid request: ${data}`)) - } else if (response.status === 401) { - reject(new Error(`User not authenticated: ${data}`)) } else if (response.status === 403) { reject(new Error(`User does not have a valid licence: ${data}`)) } else { @@ -413,8 +405,6 @@ export class JiraCloudProvider implements IProvider { resolve(sprints) } else if (response.status === 400) { reject(new Error(`Invalid request: ${response.data}`)) - } else if (response.status === 401) { - reject(new Error(`User not authenticated: ${response.data}`)) } else if (response.status === 403) { reject(new Error(`User does not have a valid licence: ${response.data}`)) } else if (response.status === 404) { @@ -508,8 +498,6 @@ export class JiraCloudProvider implements IProvider { resolve(issues) } else if (response.status === 400) { reject(new Error(`Invalid request: ${data}`)) - } else if (response.status === 401) { - reject(new Error(`User not authenticated: ${data}`)) } else if (response.status === 403) { reject(new Error(`User does not have a valid licence: ${data}`)) } else if (response.status === 404) { @@ -546,8 +534,6 @@ export class JiraCloudProvider implements IProvider { resolve() } else if (response.status === 400) { reject(new Error(`Invalid request: ${data}`)) - } else if (response.status === 401) { - reject(new Error(`User not authenticated: ${data}`)) } else if (response.status === 403) { reject(new Error(`User does not have a valid licence or permissions to assign issues: ${data}`)) } else if (response.status === 404) { @@ -577,8 +563,6 @@ export class JiraCloudProvider implements IProvider { resolve() } else if (response.status === 400) { reject(new Error(`Invalid request: ${data}`)) - } else if (response.status === 401) { - reject(new Error(`User not authenticated: ${data}`)) } else if (response.status === 403) { reject(new Error(`User does not have a valid licence or permissions to assign issues: ${data}`)) } else if (response.status === 404) { @@ -630,8 +614,6 @@ export class JiraCloudProvider implements IProvider { resolve() } else if (response.status === 400) { reject(new Error(`Invalid request: ${data}`)) - } else if (response.status === 401) { - reject(new Error(`User not authenticated: ${data}`)) } else if (response.status === 403) { reject(new Error(`User does not have a valid licence or permissions to rank issues: ${data}`)) } else { @@ -655,8 +637,6 @@ export class JiraCloudProvider implements IProvider { const points: number = data.fields[customField!] resolve(points) - } else if (response.status === 401) { - reject(new Error(`User not authenticated: ${data}`)) } else if (response.status === 404) { reject( new Error(`The issue was not found or the user does not have permission to view it: ${data}`) @@ -748,14 +728,9 @@ export class JiraCloudProvider implements IProvider { if (response.status === 201) { resolve(JSON.stringify(createdIssue.key)) this.setTransition(createdIssue.id, status) - } - if (response.status === 400) { + } else if (response.status === 400) { reject(new Error(createdIssue)) - } - if (response.status === 401) { - reject(new Error("User not authenticated")) - } - if (response.status === 403) { + } else if (response.status === 403) { reject(new Error("The user does not have the necessary permissions")) } }) @@ -859,9 +834,6 @@ export class JiraCloudProvider implements IProvider { if (response.status === 400) { reject(new Error("400 Error: consult the atlassian rest api v3 under Edit issue for information")) } - if (response.status === 401) { - reject(new Error("User not authenticated")) - } if (response.status === 403) { reject(new Error("The user does not have the necessary permissions")) } @@ -915,8 +887,6 @@ export class JiraCloudProvider implements IProvider { resolve(epics) } else if (response.status === 400) { reject(new Error(`Invalid request: ${epicData}`)) - } else if (response.status === 401) { - reject(new Error(`User not authenticated: ${epicData}`)) } else if (response.status === 403) { reject(new Error(`User does not have a valid licence: ${epicData}`)) } else if (response.status === 404) { @@ -943,8 +913,6 @@ export class JiraCloudProvider implements IProvider { const labelData = response.data if (response.status === 200) { resolve(labelData.values) - } else if (response.status === 401) { - reject(new Error(`User not authenticated: ${labelData}`)) } else { reject(new Error(`Unknown error: ${labelData}`)) } @@ -966,8 +934,6 @@ export class JiraCloudProvider implements IProvider { if (response.status === 200) { const priorities: Priority[] = priorityData.values resolve(priorities) - } else if (response.status === 401) { - reject(new Error(`User not authenticated: ${priorityData}`)) } else { reject(new Error(`Unknown error: ${priorityData}`)) } @@ -1007,14 +973,9 @@ export class JiraCloudProvider implements IProvider { .then(async (response) => { if (response.status === 201) { resolve() - } - if (response.status === 400) { + } else if (response.status === 400) { reject(new Error("Invalid api request")) - } - if (response.status === 401) { - reject(new Error("User not authenticated")) - } - if (response.status === 404) { + } else if (response.status === 404) { reject(new Error("The issue was not found or the user does not have the necessary permissions")) } }) @@ -1054,16 +1015,11 @@ export class JiraCloudProvider implements IProvider { .then(async (response) => { if (response.status === 200) { resolve() - } - if (response.status === 400) { + } else if (response.status === 400) { reject( new Error("The user does not have permission to edit the comment or the request is invalid") ) - } - if (response.status === 401) { - reject(new Error("User not authenticated")) - } - if (response.status === 404) { + } else if (response.status === 404) { reject(new Error("The issue was not found or the user does not have the necessary permissions")) } }) @@ -1080,17 +1036,11 @@ export class JiraCloudProvider implements IProvider { .then(async (response) => { if (response.status === 204) { resolve() - } - if (response.status === 400) { + } else if (response.status === 400) { reject(new Error("The user does not have permission to delete the comment")) - } - if (response.status === 401) { - reject(new Error("User not authenticated")) - } - if (response.status === 404) { + } else if (response.status === 404) { reject(new Error("The issue was not found or the user does not have the necessary permissions")) - } - if (response.status === 405) { + } else if (response.status === 405) { reject(new Error("An anonymous call has been made to the operation")) } }) @@ -1107,20 +1057,13 @@ export class JiraCloudProvider implements IProvider { .then(async (response) => { if (response.status === 204) { resolve() - } - if (response.status === 400) { + } else if (response.status === 400) { reject(new Error("The issue has subtasks and deleteSubtasks is not set to true")) - } - if (response.status === 401) { - reject(new Error("User not authenticated")) - } - if (response.status === 403) { + } else if (response.status === 403) { reject(new Error("The user does not have permission to delete the issue")) - } - if (response.status === 404) { + } else if (response.status === 404) { reject(new Error("The issue was not found or the user does not have the necessary permissions")) - } - if (response.status === 405) { + } else if (response.status === 405) { reject(new Error("An anonymous call has been made to the operation")) } }) @@ -1161,8 +1104,6 @@ export class JiraCloudProvider implements IProvider { resolve(createdSubtask) } else if (response.status === 400) { reject(new Error(`Invalid request: ${createdSubtask}`)) - } else if (response.status === 401) { - reject(new Error(`User not authenticated: ${createdSubtask}`)) } else if (response.status === 403) { reject(new Error(`User does not have a valid licence: ${createdSubtask}`)) } else { @@ -1218,17 +1159,11 @@ export class JiraCloudProvider implements IProvider { .then(async (response) => { if (response.status === 201) { resolve() - } - if (response.status === 400) { + } else if (response.status === 400) { reject(new Error("Invalid request")) - } - if (response.status === 401) { - reject(new Error("User not authenticated")) - } - if (response.status === 403) { + } else if (response.status === 403) { reject(new Error("The user does not have the necessary permissions")) - } - if (response.status === 404) { + } else if (response.status === 404) { reject( new Error( "The Board does not exists or the user does not have the necessary permissions to view it" From ec0abf46ccc1550585f2b37b30a6e68052826042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ru=CC=88sch?= Date: Sun, 12 Nov 2023 16:33:42 +0100 Subject: [PATCH 07/19] Improve error handling --- .../jira-cloud-provider/JiraCloudProvider.ts | 799 +++++++++--------- 1 file changed, 388 insertions(+), 411 deletions(-) diff --git a/electron/providers/jira-cloud-provider/JiraCloudProvider.ts b/electron/providers/jira-cloud-provider/JiraCloudProvider.ts index 018e0d6d..18fc70b8 100644 --- a/electron/providers/jira-cloud-provider/JiraCloudProvider.ts +++ b/electron/providers/jira-cloud-provider/JiraCloudProvider.ts @@ -19,7 +19,7 @@ import { } from "../../../types/jira" import { IProvider } from "../base-provider" import { getAccessToken, refreshTokens } from "./getAccessToken" -import { AxiosResponse } from "axios" +import { AxiosError, AxiosResponse } from "axios"; import axios from "axios" export class JiraCloudProvider implements IProvider { @@ -34,7 +34,6 @@ export class JiraCloudProvider implements IProvider { private reversedCustomFields = new Map() private constructRestBasedClient(basePath: string, version: string) { - // TODO catch errors and handle common status codes const instance = axios.create({ baseURL: `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/${basePath}/${version}`, headers: { @@ -42,16 +41,21 @@ export class JiraCloudProvider implements IProvider { Authorization: `Bearer ${this.accessToken}`, "Content-Type": "application/json", }, - validateStatus: (statusCode) => statusCode < 500, }) - instance.interceptors.response.use((response) => { - if (response.status === 401) { - return Promise.reject(new Error(`User not authenticated: ${JSON.stringify(response.data)}`)) - } + instance.interceptors.response.use( + (response) => response, + (error) => { + if (error.response) { + const statusCode = error.response.status + if (statusCode === 401) { + return Promise.reject(new Error(`User not authenticated: ${JSON.stringify(error.response.data)}`)) + } + } - return response - }) + return Promise.reject(error) + } + ) return instance } @@ -152,15 +156,11 @@ export class JiraCloudProvider implements IProvider { this.getRestApiClient(3) .get('/field') .then(async (response) => { - if (response.status === 200) { - response.data.forEach((field: { name: string; id: string }) => { - this.customFields.set(field.name, field.id) - this.reversedCustomFields.set(field.id, field.name) - }) - resolve() - } else { - reject(new Error(`Unknown error: ${response.data}`)) - } + response.data.forEach((field: { name: string; id: string }) => { + this.customFields.set(field.name, field.id) + this.reversedCustomFields.set(field.id, field.name) + }) + resolve() }) .catch((error) => { reject(new Error(`Error creating issue: ${error}`)) @@ -171,32 +171,28 @@ export class JiraCloudProvider implements IProvider { async getProjects(): Promise { return new Promise((resolve, reject) => { this.getRestApiClient(3) - .get('/project/search?expand=description,lead,issueTypes,url,projectKeys,permissions,insight') + .get('/project/searchs?expand=description,lead,issueTypes,url,projectKeys,permissions,insight') .then(async (response) => { - const data = response.data - if (response.status === 200) { - const projects = data.values.map((project: JiraProject) => ({ - key: project.key, - name: project.name, - id: project.id, - lead: project.lead.displayName, - type: project.projectTypeKey, - })) - resolve(projects) - } else if (response.status === 400) { - reject(new Error(`Invalid request: ${data}`)) - } else if (response.status === 404) { - reject( - new Error( - `No projects matching the search criteria were found: ${data}` - ) - ) - } else { - reject(new Error(`Unknown error: ${data}`)) - } + const projects = response.data.values.map((project: JiraProject) => ({ + key: project.key, + name: project.name, + id: project.id, + lead: project.lead.displayName, + type: project.projectTypeKey, + })) + resolve(projects) }) .catch((error) => { - reject(new Error(`Error getting projects: ${error}`)) + let specificError = error + if (error.response) { + if (error.response.status === 400) { + specificError = new Error(`Invalid request: ${error.response.data}`) + } else if (error.response.status === 404) { + specificError = new Error(`No projects matching the search criteria were found: ${error.response.data}`) + } + } + + reject(new Error(`Error getting projects: ${specificError}`)) }) }) } @@ -206,21 +202,21 @@ export class JiraCloudProvider implements IProvider { this.getRestApiClient(2) .get(`/project/${projectIdOrKey}/statuses`) .then(async (response) => { - const data = response.data - if (response.status === 200) { - const issueTypes: JiraIssueType[] = data - resolve(issueTypes as IssueType[]) - } else if (response.status === 404) { - reject( - new Error(`The project was not found or the user does not have permission to view it: ${data}`) - ) - } else { - reject(new Error(`Unknown error: ${data}`)) + const issueTypes: JiraIssueType[] = response.data + resolve(issueTypes as IssueType[]) + }) + .catch((error) => { + let specificError = error + if (error.response) { + if (error.response.status === 404) { + specificError = new Error( + `The project was not found or the user does not have permission to view it: ${error.response.data}` + ) + } } + + reject(new Error(`Error in fetching the issue types: ${specificError}`)) }) - .catch((error) => - reject(new Error(`Error in fetching the issue types: ${error}`)) - ) }) } @@ -229,29 +225,24 @@ export class JiraCloudProvider implements IProvider { this.getRestApiClient(3) .get('/issue/createmeta?expand=projects.issuetypes.fields') .then(async (response) => { - const metadata = response.data - if (response.status === 200) { - const issueTypeToFieldsMap: { [key: string]: string[] } = {} - metadata.projects.forEach( - (project: { + const issueTypeToFieldsMap: { [key: string]: string[] } = {} + response.data.projects.forEach( + (project: { + id: string + issuetypes: { + fields: {} id: string - issuetypes: { - fields: {} - id: string - }[] - }) => { - project.issuetypes.forEach((issueType) => { - const fieldKeys = Object.keys(issueType.fields) - issueTypeToFieldsMap[issueType.id] = fieldKeys.map( - (fieldKey) => this.reversedCustomFields.get(fieldKey)! - ) - }) - } - ) - resolve(issueTypeToFieldsMap) - } else { - reject(new Error(`Unknown error: ${metadata}`)) - } + }[] + }) => { + project.issuetypes.forEach((issueType) => { + const fieldKeys = Object.keys(issueType.fields) + issueTypeToFieldsMap[issueType.id] = fieldKeys.map( + (fieldKey) => this.reversedCustomFields.get(fieldKey)! + ) + }) + } + ) + resolve(issueTypeToFieldsMap) }) .catch((error) => reject(new Error(`Error in fetching the issue types map: ${error}`)) @@ -264,15 +255,10 @@ export class JiraCloudProvider implements IProvider { this.getRestApiClient(3) .get(`/issue/${issueIdOrKey}/editmeta`) .then(async (response) => { - const metadata = response.data - if (response.status === 200) { - const fieldKeys = Object.keys(metadata.fields).map( - (fieldKey) => this.reversedCustomFields.get(fieldKey)! - ) - resolve(fieldKeys) - } else { - reject(new Error(`Unknown error: ${metadata}`)) - } + const fieldKeys = Object.keys(response.data.fields).map( + (fieldKey) => this.reversedCustomFields.get(fieldKey)! + ) + resolve(fieldKeys) }) .catch((error) => reject(new Error(`Error in fetching the issue types map: ${error}`)) @@ -285,27 +271,22 @@ export class JiraCloudProvider implements IProvider { this.getRestApiClient(3) .get(`/user/assignable/search?project=${projectIdOrKey}`) .then(async (response) => { - const data = response.data - if (response.status === 200) { - const users: User[] = data - resolve(users as User[]) - } else if (response.status === 400) { - reject(new Error(`Some infos are missing: ${data}`)) - } else if (response.status === 404) { - reject( - new Error(`Project, issue, or transition were not found: ${data}`) - ) - } else if (response.status === 429) { - reject(new Error(`Rate limit exceeded: ${data}`)) - } else { - reject(new Error(`Unknown error: ${data}`)) + resolve(response.data as User[]) + }) + .catch((error) => { + let specificError = error + if (error.response) { + if (error.response.status === 400) { + specificError = new Error(`Some infos are missing: ${error.response.data}`) + } else if (error.response.status === 404) { + specificError = new Error(`Project, issue, or transition were not found: ${error.response.data}`) + } else if (error.response.status === 429) { + specificError = new Error(`Rate limit exceeded: ${error.response.data}`) + } } + + reject(new Error(`Error in fetching the assignable users for the project ${projectIdOrKey}: ${specificError}`)) }) - .catch((error) => - reject( - new Error(`Error in fetching the assignable users for the project ${projectIdOrKey}: ${error}`) - ) - ) }) } @@ -314,13 +295,7 @@ export class JiraCloudProvider implements IProvider { this.getRestApiClient(3) .get('/myself') .then(async (response) => { - const data = response.data - if (response.status === 200) { - const user: User = data - resolve(user as User) - } else { - reject(new Error(`Unknown error: ${data}`)) - } + resolve(response.data as User) }) .catch((error) => reject(new Error(`Error in fetching the current user: ${error}`)) @@ -333,20 +308,20 @@ export class JiraCloudProvider implements IProvider { this.getRestApiClient(3) .get(`/issue/${issueIdOrKey}?fields=reporter`) .then(async (response) => { - const user = response.data - if (response.status === 200) { - resolve(user.fields.reporter as User) - } else if (response.status === 404) { - reject( - new Error(`The issue was not found or the user does not have permission to view it: ${user}`) - ) - } else { - reject(new Error(`Unknown error: ${user}`)) + resolve(response.data.fields.reporter as User) + }) + .catch((error) => { + let specificError = error + if (error.response) { + if (error.response.status === 404) { + specificError = new Error( + `The issue was not found or the user does not have permission to view it: ${error.response.data}` + ) + } } + + reject(new Error(`Error in fetching the issue reporter: ${specificError}`)) }) - .catch((error) => - reject(new Error(`Error in fetching the current user: ${error}`)) - ) }) } @@ -355,22 +330,22 @@ export class JiraCloudProvider implements IProvider { this.getAgileRestApiClient('1.0') .get(`/board?projectKeyOrId=${project}`) .then(async (response) => { - const data = response.data - if (response.status === 200) { - const boardIds: number[] = data.values.map( - (element: { id: number; name: string }) => element.id - ) - resolve(boardIds) - } else if (response.status === 400) { - reject(new Error(`Invalid request: ${data}`)) - } else if (response.status === 403) { - reject(new Error(`User does not have a valid licence: ${data}`)) - } else { - reject(new Error(`Unknown error: ${data}`)) - } + const boardIds: number[] = response.data.values.map( + (element: { id: number; name: string }) => element.id + ) + resolve(boardIds) }) .catch((error) => { - reject(new Error(`Error getting projects: ${error}`)) + let specificError = error + if (error.response) { + if (error.response.status === 400) { + specificError = new Error(`Invalid request: ${error.response.data}`) + } else if (error.response.status === 403) { + specificError = new Error(`User does not have a valid licence: ${error.response.data}`) + } + } + + reject(new Error(`Error getting projects: ${specificError}`)) }) }) } @@ -380,45 +355,44 @@ export class JiraCloudProvider implements IProvider { this.getAgileRestApiClient('1.0') .get(`/board/${boardId}/sprint`) .then(async (response) => { - if (response.status === 200) { - const sprints: Sprint[] = response.data.values - .filter( - (element: { state: string }) => element.state !== "closed" - ) - .map((element: JiraSprint) => { - const sDate = new Date(element.startDate) - const startDate = Number.isNaN(sDate.getTime()) - ? "Invalid Date" - : dateTimeFormat.format(sDate) - const eDate = new Date(element.endDate) - const endDate = Number.isNaN(eDate.getTime()) - ? "Invalid Date" - : dateTimeFormat.format(eDate) - return { - id: element.id, - name: element.name, - state: element.state, - startDate, - endDate, - } - }) - resolve(sprints) - } else if (response.status === 400) { - reject(new Error(`Invalid request: ${response.data}`)) - } else if (response.status === 403) { - reject(new Error(`User does not have a valid licence: ${response.data}`)) - } else if (response.status === 404) { - reject( - new Error( - `The board does not exist or the user does not have permission to view it: ${response.data}` - ) + const sprints: Sprint[] = response.data.values + .filter( + (element: { state: string }) => element.state !== "closed" ) - } else { - reject(new Error(`Unknown error: ${response.data}`)) - } + .map((element: JiraSprint) => { + const sDate = new Date(element.startDate) + const startDate = Number.isNaN(sDate.getTime()) + ? "Invalid Date" + : dateTimeFormat.format(sDate) + const eDate = new Date(element.endDate) + const endDate = Number.isNaN(eDate.getTime()) + ? "Invalid Date" + : dateTimeFormat.format(eDate) + return { + id: element.id, + name: element.name, + state: element.state, + startDate, + endDate, + } + }) + resolve(sprints) }) .catch((error) => { - reject(new Error(`Error fetching the sprints: ${error}`)) + let specificError = error + if (error.response) { + if (error.response.status === 400) { + specificError = new Error(`Invalid request: ${error.response.data}`) + } else if (error.response.status === 403) { + specificError = new Error(`User does not have a valid licence: ${error.response.data}`) + } else if (error.response.status === 404) { + specificError = new Error( + `The board does not exist or the user does not have permission to view it: ${error.response.data}` + ) + } + } + + reject(new Error(`Error fetching the sprints: ${specificError}`)) }) }) } @@ -431,7 +405,7 @@ export class JiraCloudProvider implements IProvider { resolve(this.fetchIssues(response)) }) .catch((error) => { - reject(new Error(`Error fetching issues by project: ${error}`)) + reject(new Error(`Error fetching issues by project: ${this.handleFetchIssuesError(error)}`)) }) }) } @@ -444,7 +418,7 @@ export class JiraCloudProvider implements IProvider { resolve(this.fetchIssues(response)) }) .catch((error) => { - reject(new Error(`Error fetching issues by sprint: ${error}`)) + reject(new Error(`Error fetching issues by sprint: ${this.handleFetchIssuesError(error)}`)) }) }) } @@ -460,56 +434,61 @@ export class JiraCloudProvider implements IProvider { resolve(this.fetchIssues(response)) }) .catch((error) => { - reject(new Error(`Error fetching issues by project: ${error}`)) + reject(new Error(`Error fetching issues by project: ${this.handleFetchIssuesError(error)}`)) }) }) } async fetchIssues(response: AxiosResponse): Promise { const rankCustomField = this.customFields.get("Rank") || "" - return new Promise((resolve, reject) => { - const data = response.data - if (response.status === 200) { - const issues: Promise = Promise.all( - data.issues.map(async (element: JiraIssue) => ({ - issueKey: element.key, - summary: element.fields.summary, - creator: element.fields.creator.displayName, - status: element.fields.status.name, - type: element.fields.issuetype.name, - storyPointsEstimate: await this.getIssueStoryPointsEstimate(element.key), - epic: element.fields.parent?.fields.summary, - labels: element.fields.labels, - assignee: { - displayName: element.fields.assignee?.displayName, - avatarUrls: element.fields.assignee?.avatarUrls, - }, - rank: element.fields[rankCustomField], - description: element.fields.description, - subtasks: element.fields.subtasks, - created: element.fields.created, - updated: element.fields.updated, - comment: element.fields.comment, - projectId: element.fields.project.id, - sprint: element.fields.sprint, - attachments: element.fields.attachment, - })) - ) - resolve(issues) - } else if (response.status === 400) { - reject(new Error(`Invalid request: ${data}`)) - } else if (response.status === 403) { - reject(new Error(`User does not have a valid licence: ${data}`)) - } else if (response.status === 404) { - reject( - new Error(`The board does not exist or the user does not have permission to view it: ${data}`) - ) - } else { - reject(new Error(`Unknown error: ${data}`)) - } + return new Promise((resolve, _) => { + const issues: Promise = Promise.all( + response.data.issues.map(async (element: JiraIssue) => ({ + issueKey: element.key, + summary: element.fields.summary, + creator: element.fields.creator.displayName, + status: element.fields.status.name, + type: element.fields.issuetype.name, + storyPointsEstimate: await this.getIssueStoryPointsEstimate(element.key), + epic: element.fields.parent?.fields.summary, + labels: element.fields.labels, + assignee: { + displayName: element.fields.assignee?.displayName, + avatarUrls: element.fields.assignee?.avatarUrls, + }, + rank: element.fields[rankCustomField], + description: element.fields.description, + subtasks: element.fields.subtasks, + created: element.fields.created, + updated: element.fields.updated, + comment: element.fields.comment, + projectId: element.fields.project.id, + sprint: element.fields.sprint, + attachments: element.fields.attachment, + })) + ) + resolve(issues) }) } + handleFetchIssuesError(error: AxiosError): Error { + if (!error.response) { + return error; + } + + if (error.response.status === 400) { + return new Error(`Invalid request: ${error.response.data}`) + } else if (error.response.status === 403) { + return new Error(`User does not have a valid licence: ${error.response.data}`) + } else if (error.response.status === 404) { + return new Error( + `The board does not exist or the user does not have permission to view it: ${error.response.data}` + ) + } + + return error; + } + async moveIssueToSprintAndRank( sprint: number, issue: string, @@ -528,24 +507,24 @@ export class JiraCloudProvider implements IProvider { ...(rankBefore ? { rankBeforeIssue: rankBefore } : {}), } ) - .then(async (response) => { - const data = response.data - if (response.status === 204) { - resolve() - } else if (response.status === 400) { - reject(new Error(`Invalid request: ${data}`)) - } else if (response.status === 403) { - reject(new Error(`User does not have a valid licence or permissions to assign issues: ${data}`)) - } else if (response.status === 404) { - reject( - new Error(`The sprint does not exist or the user does not have permission to view it: ${data}`) - ) - } else { - reject(new Error(`Unknown error: ${data}`)) - } - }) + .then(async () => { resolve() }) .catch((error) => { - reject(new Error(`Error in moving this issue to the Sprint with id ${sprint}: ${error}`)) + let specificError = error + if (error.response) { + if (error.response.status === 400) { + specificError = new Error(`Invalid request: ${error.response.data}`) + } else if (error.response.status === 403) { + specificError = new Error( + `User does not have a valid licence or permissions to assign issues: ${error.response.data}` + ) + } else if (error.response.status === 404) { + specificError = new Error( + `The board does not exist or the user does not have permission to view it: ${error.response.data}` + ) + } + } + + reject(new Error(`Error in moving this issue to the Sprint with id ${sprint}: ${specificError}`)) }) }) } @@ -557,24 +536,24 @@ export class JiraCloudProvider implements IProvider { '/backlog/issue', { issues: [issue] } ) - .then(async (response) => { - const data = response.data - if (response.status === 204) { - resolve() - } else if (response.status === 400) { - reject(new Error(`Invalid request: ${data}`)) - } else if (response.status === 403) { - reject(new Error(`User does not have a valid licence or permissions to assign issues: ${data}`)) - } else if (response.status === 404) { - reject( - new Error(`The board does not exist or the user does not have permission to view it: ${data}`) - ) - } else { - reject(new Error(`Unknown error: ${data}`)) - } - }) + .then(async () => { resolve() }) .catch((error) => { - reject(new Error(`Error in moving this issue to the backlog: ${error}`)) + let specificError = error + if (error.response) { + if (error.response.status === 400) { + specificError = new Error(`Invalid request: ${error.response.data}`) + } else if (error.response.status === 403) { + specificError = new Error( + `User does not have a valid licence or permissions to assign issues: ${error.response.data}` + ) + } else if (error.response.status === 404) { + specificError = new Error( + `The board does not exist or the user does not have permission to view it: ${error.response.data}` + ) + } + } + + reject(new Error(`Error in moving this issue to the backlog: ${specificError}`)) }) }) } @@ -605,23 +584,27 @@ export class JiraCloudProvider implements IProvider { this.getAgileRestApiClient('1.0') .put('/issue/rank', body) .then(async (response) => { - const data = response.data if (response.status === 204) { resolve() } else if (response.status === 207) { // Returns the list of issues with status of rank operation. // see documentation: https://developer.atlassian.com/cloud/jira/software/rest/api-group-issue/#api-rest-agile-1-0-issue-rank-put-responses resolve() - } else if (response.status === 400) { - reject(new Error(`Invalid request: ${data}`)) - } else if (response.status === 403) { - reject(new Error(`User does not have a valid licence or permissions to rank issues: ${data}`)) - } else { - reject(new Error(`Unknown error: ${data}`)) } }) .catch((error) => { - reject(new Error(`Error in moving this issue to the backlog: ${error}`)) + let specificError = error + if (error.response) { + if (error.response.status === 400) { + specificError = new Error(`Invalid request: ${error.response.data}`) + } else if (error.response.status === 403) { + specificError = new Error( + `User does not have a valid licence or permissions to rank issues: ${error.response.data}` + ) + } + } + + reject(new Error(`Error in ranking this issue in the backlog: ${specificError}`)) }) }) } @@ -631,23 +614,22 @@ export class JiraCloudProvider implements IProvider { this.getRestApiClient(3) .get(`/issue/${issue}`) .then(async (response) => { - const data = response.data - if (response.status === 200) { - const customField = this.customFields.get("Story point estimate") - const points: number = data.fields[customField!] - - resolve(points) - } else if (response.status === 404) { - reject( - new Error(`The issue was not found or the user does not have permission to view it: ${data}`) - ) - } else { - reject(new Error(`Unknown error: ${data}`)) + const customField = this.customFields.get("Story point estimate") + const points: number = response.data.fields[customField!] + resolve(points) + }) + .catch((error) => { + let specificError = error + if (error.response) { + if (error.response.status === 404) { + specificError = new Error( + `The issue was not found or the user does not have permission to view it: ${error.response.data}` + ) + } } + + reject(new Error(`Error in getting the story points for issue: ${issue}: ${specificError}`)) }) - .catch((error) => - reject(new Error(`Error in getting the story points for issue: ${issue}: ${error}`)) - ) }) } @@ -725,17 +707,20 @@ export class JiraCloudProvider implements IProvider { ) .then(async (response) => { const createdIssue = response.data - if (response.status === 201) { - resolve(JSON.stringify(createdIssue.key)) - this.setTransition(createdIssue.id, status) - } else if (response.status === 400) { - reject(new Error(createdIssue)) - } else if (response.status === 403) { - reject(new Error("The user does not have the necessary permissions")) - } + resolve(JSON.stringify(createdIssue.key)) + this.setTransition(createdIssue.id, status) }) .catch((error) => { - reject(new Error(`Error creating issue: ${error}`)) + let specificError = error + if (error.response) { + if (error.response.status === 400) { + specificError = new Error(error.response.data) + } else if (error.response.status === 404) { + specificError = new Error("The user does not have the necessary permissions") + } + } + + reject(new Error(`Error creating issue: ${specificError}`)) }) }) } @@ -827,22 +812,26 @@ export class JiraCloudProvider implements IProvider { }, } ) - .then(async (response) => { - if (response.status === 204) { - resolve() - } - if (response.status === 400) { - reject(new Error("400 Error: consult the atlassian rest api v3 under Edit issue for information")) - } - if (response.status === 403) { - reject(new Error("The user does not have the necessary permissions")) - } - if (response.status === 404) { - reject(new Error("The issue was not found or the user does not have the necessary permissions")) + .then(async () => { resolve() }) + .catch((error) => { + let specificError = error + if (error.response) { + if (error.response.status === 400) { + specificError = new Error( + "400 Error: consult the atlassian rest api v3 under Edit issue for information" + ) + } else if (error.response.status === 403) { + specificError = new Error( + "The user does not have the necessary permissions" + ) + } else if (error.response.status === 404) { + specificError = new Error( + "The issue was not found or the user does not have the necessary permissions" + ) + } } - }) - .catch(async (error) => { - reject(new Error(`Error creating issue: ${error}`)) + + reject(new Error(`Error creating issue: ${specificError}`)) }) }) } @@ -871,37 +860,35 @@ export class JiraCloudProvider implements IProvider { this.getRestApiClient(3) .get(`search?jql=issuetype = Epic AND project = ${projectIdOrKey}`) .then(async (response) => { - const epicData = response.data - if (response.status === 200) { - const epics: Promise = Promise.all( - epicData.issues.map(async (element: JiraIssue) => ({ - issueKey: element.key, - summary: element.fields.summary, - labels: element.fields.labels, - assignee: { - displayName: element.fields.assignee?.displayName, - avatarUrls: element.fields.assignee?.avatarUrls, - }, - })) - ) - resolve(epics) - } else if (response.status === 400) { - reject(new Error(`Invalid request: ${epicData}`)) - } else if (response.status === 403) { - reject(new Error(`User does not have a valid licence: ${epicData}`)) - } else if (response.status === 404) { - reject( - new Error( - `The board does not exist or the user does not have permission to view it: ${epicData}` + const epics: Promise = Promise.all( + response.data.issues.map(async (element: JiraIssue) => ({ + issueKey: element.key, + summary: element.fields.summary, + labels: element.fields.labels, + assignee: { + displayName: element.fields.assignee?.displayName, + avatarUrls: element.fields.assignee?.avatarUrls, + }, + })) + ) + resolve(epics) + }) + .catch((error) => { + let specificError = error + if (error.response) { + if (error.response.status === 400) { + specificError = new Error(`Invalid request: ${error.response.data}`) + } else if (error.response.status === 403) { + specificError = new Error(`User does not have a valid licence: ${error.response.data}`) + } else if (error.response.status === 404) { + specificError = new Error( + `The board does not exist or the user does not have permission to view it: ${error.response.data}` ) - ) - } else { - reject(new Error(`Unknown error: ${epicData}`)) + } } + + reject(new Error(`Error in fetching the epics for the project ${projectIdOrKey}: ${specificError}`)) }) - .catch((error) => - reject(new Error(`Error in fetching the epics for the project ${projectIdOrKey}: ${error}`)) - ) }) } @@ -910,12 +897,7 @@ export class JiraCloudProvider implements IProvider { this.getRestApiClient(3) .get('/label') .then(async (response) => { - const labelData = response.data - if (response.status === 200) { - resolve(labelData.values) - } else { - reject(new Error(`Unknown error: ${labelData}`)) - } + resolve(response.data.values) }) .catch((error) => reject(new Error(`Error in fetching the labels: ${error}`)) @@ -931,12 +913,7 @@ export class JiraCloudProvider implements IProvider { .get('/priority/search') .then(async (response) => { const priorityData: JiraPriority = response.data - if (response.status === 200) { - const priorities: Priority[] = priorityData.values - resolve(priorities) - } else { - reject(new Error(`Unknown error: ${priorityData}`)) - } + resolve(priorityData.values) }) .catch((error) => reject(new Error(`Error in fetching the labels: ${error}`)) @@ -970,17 +947,18 @@ export class JiraCloudProvider implements IProvider { } } ) - .then(async (response) => { - if (response.status === 201) { - resolve() - } else if (response.status === 400) { - reject(new Error("Invalid api request")) - } else if (response.status === 404) { - reject(new Error("The issue was not found or the user does not have the necessary permissions")) + .then(async () => { resolve() }) + .catch((error) => { + let specificError = error + if (error.response) { + if (error.response.status === 400) { + specificError = new Error("Invalid api request") + } else if (error.response.status === 404) { + specificError = new Error("The issue was not found or the user does not have the necessary permissions") + } } - }) - .catch(async (error) => { - reject(new Error(`Error adding a comment to the issue ${issueIdOrKey}: ${error}`)) + + reject(new Error(`Error adding a comment to the issue ${issueIdOrKey}: ${specificError}`)) }) }) } @@ -1012,19 +990,18 @@ export class JiraCloudProvider implements IProvider { } } ) - .then(async (response) => { - if (response.status === 200) { - resolve() - } else if (response.status === 400) { - reject( - new Error("The user does not have permission to edit the comment or the request is invalid") - ) - } else if (response.status === 404) { - reject(new Error("The issue was not found or the user does not have the necessary permissions")) + .then(async () => { resolve() }) + .catch((error) => { + let specificError = error + if (error.response) { + if (error.response.status === 400) { + specificError = new Error("The user does not have permission to edit the comment or the request is invalid") + } else if (error.response.status === 404) { + specificError = new Error("The issue was not found or the user does not have the necessary permissions") + } } - }) - .catch(async (error) => { - reject(new Error(`Error editing the comment in issue ${issueIdOrKey}: ${error}`)) + + reject(new Error(`Error editing the comment in issue ${issueIdOrKey}: ${specificError}`)) }) }) } @@ -1033,19 +1010,20 @@ export class JiraCloudProvider implements IProvider { return new Promise((resolve, reject) => { this.getRestApiClient(3) .delete(`/issue/${issueIdOrKey}/comment/${commentId}`) - .then(async (response) => { - if (response.status === 204) { - resolve() - } else if (response.status === 400) { - reject(new Error("The user does not have permission to delete the comment")) - } else if (response.status === 404) { - reject(new Error("The issue was not found or the user does not have the necessary permissions")) - } else if (response.status === 405) { - reject(new Error("An anonymous call has been made to the operation")) + .then(async () => { resolve() }) + .catch((error) => { + let specificError = error + if (error.response) { + if (error.response.status === 400) { + specificError = new Error("The user does not have permission to delete the comment") + } else if (error.response.status === 404) { + specificError = new Error("The issue was not found or the user does not have the necessary permissions") + } else if (error.response.status === 405) { + specificError = new Error("An anonymous call has been made to the operation") + } } - }) - .catch(async (error) => { - reject(new Error(`Error editing the comment in issue ${issueIdOrKey}: ${error}`)) + + reject(new Error(`Error deleting the comment in issue ${issueIdOrKey}: ${specificError}`)) }) }) } @@ -1054,21 +1032,22 @@ export class JiraCloudProvider implements IProvider { return new Promise((resolve, reject) => { this.getRestApiClient(2) .delete(`/issue/${issueIdOrKey}`) - .then(async (response) => { - if (response.status === 204) { - resolve() - } else if (response.status === 400) { - reject(new Error("The issue has subtasks and deleteSubtasks is not set to true")) - } else if (response.status === 403) { - reject(new Error("The user does not have permission to delete the issue")) - } else if (response.status === 404) { - reject(new Error("The issue was not found or the user does not have the necessary permissions")) - } else if (response.status === 405) { - reject(new Error("An anonymous call has been made to the operation")) + .then(async () => { resolve() }) + .catch((error) => { + let specificError = error + if (error.response) { + if (error.response.status === 400) { + specificError = new Error("The issue has subtasks and deleteSubtasks is not set to true") + } else if (error.response.status === 403) { + specificError = new Error("The user does not have permission to delete the issue") + } else if (error.response.status === 404) { + specificError = new Error("The issue was not found or the user does not have the necessary permissions") + } else if (error.response.status === 405) { + specificError = new Error("An anonymous call has been made to the operation") + } } - }) - .catch(async (error) => { - reject(new Error(`Error deleting the subtask ${issueIdOrKey}: ${error}`)) + + reject(new Error(`Error deleting the subtask ${issueIdOrKey}: ${specificError}`)) }) }) } @@ -1100,18 +1079,19 @@ export class JiraCloudProvider implements IProvider { ) .then(async (response) => { const createdSubtask: { id: string; key: string } = response.data - if (response.status === 201) { - resolve(createdSubtask) - } else if (response.status === 400) { - reject(new Error(`Invalid request: ${createdSubtask}`)) - } else if (response.status === 403) { - reject(new Error(`User does not have a valid licence: ${createdSubtask}`)) - } else { - reject(new Error(`Unknown error: ${createdSubtask}`)) - } + resolve(createdSubtask) }) .catch((error) => { - reject(new Error(`Error creating subtask: ${error}`)) + let specificError = error + if (error.response) { + if (error.response.status === 400) { + specificError = new Error(`Invalid request: ${error.response.data}`) + } else if (error.response.status === 403) { + specificError = new Error(`User does not have a valid licence: ${error.response.data}`) + } + } + + reject(new Error(`Error creating subtask: ${specificError}`)) }) }) } @@ -1156,23 +1136,20 @@ export class JiraCloudProvider implements IProvider { ...(goal && { goal }), } ) - .then(async (response) => { - if (response.status === 201) { - resolve() - } else if (response.status === 400) { - reject(new Error("Invalid request")) - } else if (response.status === 403) { - reject(new Error("The user does not have the necessary permissions")) - } else if (response.status === 404) { - reject( - new Error( - "The Board does not exists or the user does not have the necessary permissions to view it" - ) - ) - } - }) + .then(async () => { resolve() }) .catch((error) => { - reject(new Error(`Error creating sprint: ${error}`)) + let specificError = error + if (error.response) { + if (error.response.status === 400) { + specificError = new Error("Invalid request") + } else if (error.response.status === 403) { + specificError = new Error("The user does not have the necessary permissions") + } else if (error.response.status === 404) { + specificError = new Error("The Board does not exist or the user does not have the necessary permissions to view it") + } + } + + reject(new Error(`Error creating sprint: ${specificError}`)) }) }) } From e30fa9e8224384d07a0ad5bccf7ffff084cf39b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ru=CC=88sch?= Date: Sun, 12 Nov 2023 16:45:51 +0100 Subject: [PATCH 08/19] Universally handle 400 errors --- .../jira-cloud-provider/JiraCloudProvider.ts | 76 +++++++------------ 1 file changed, 28 insertions(+), 48 deletions(-) diff --git a/electron/providers/jira-cloud-provider/JiraCloudProvider.ts b/electron/providers/jira-cloud-provider/JiraCloudProvider.ts index 18fc70b8..aefb0740 100644 --- a/electron/providers/jira-cloud-provider/JiraCloudProvider.ts +++ b/electron/providers/jira-cloud-provider/JiraCloudProvider.ts @@ -19,7 +19,7 @@ import { } from "../../../types/jira" import { IProvider } from "../base-provider" import { getAccessToken, refreshTokens } from "./getAccessToken" -import { AxiosError, AxiosResponse } from "axios"; +import { AxiosError, AxiosResponse, isAxiosError } from "axios"; import axios from "axios" export class JiraCloudProvider implements IProvider { @@ -43,13 +43,23 @@ export class JiraCloudProvider implements IProvider { }, }) + const recreateAxiosError = (originalError: AxiosError, message: string) => new AxiosError( + message, + originalError.code, + originalError.config, + originalError.request, + originalError.response + ) + instance.interceptors.response.use( (response) => response, (error) => { - if (error.response) { + if (isAxiosError(error) && error.response) { const statusCode = error.response.status - if (statusCode === 401) { - return Promise.reject(new Error(`User not authenticated: ${JSON.stringify(error.response.data)}`)) + if (statusCode === 400) { + return Promise.reject(recreateAxiosError(error, `Invalid request: ${JSON.stringify(error.response.data)}`)) + } else if (statusCode === 401) { + return Promise.reject(recreateAxiosError(error, `User not authenticated: ${JSON.stringify(error.response.data)}`)) } } @@ -185,9 +195,7 @@ export class JiraCloudProvider implements IProvider { .catch((error) => { let specificError = error if (error.response) { - if (error.response.status === 400) { - specificError = new Error(`Invalid request: ${error.response.data}`) - } else if (error.response.status === 404) { + if (error.response.status === 404) { specificError = new Error(`No projects matching the search criteria were found: ${error.response.data}`) } } @@ -276,9 +284,7 @@ export class JiraCloudProvider implements IProvider { .catch((error) => { let specificError = error if (error.response) { - if (error.response.status === 400) { - specificError = new Error(`Some infos are missing: ${error.response.data}`) - } else if (error.response.status === 404) { + if (error.response.status === 404) { specificError = new Error(`Project, issue, or transition were not found: ${error.response.data}`) } else if (error.response.status === 429) { specificError = new Error(`Rate limit exceeded: ${error.response.data}`) @@ -338,9 +344,7 @@ export class JiraCloudProvider implements IProvider { .catch((error) => { let specificError = error if (error.response) { - if (error.response.status === 400) { - specificError = new Error(`Invalid request: ${error.response.data}`) - } else if (error.response.status === 403) { + if (error.response.status === 403) { specificError = new Error(`User does not have a valid licence: ${error.response.data}`) } } @@ -381,9 +385,7 @@ export class JiraCloudProvider implements IProvider { .catch((error) => { let specificError = error if (error.response) { - if (error.response.status === 400) { - specificError = new Error(`Invalid request: ${error.response.data}`) - } else if (error.response.status === 403) { + if (error.response.status === 403) { specificError = new Error(`User does not have a valid licence: ${error.response.data}`) } else if (error.response.status === 404) { specificError = new Error( @@ -476,9 +478,7 @@ export class JiraCloudProvider implements IProvider { return error; } - if (error.response.status === 400) { - return new Error(`Invalid request: ${error.response.data}`) - } else if (error.response.status === 403) { + if (error.response.status === 403) { return new Error(`User does not have a valid licence: ${error.response.data}`) } else if (error.response.status === 404) { return new Error( @@ -511,9 +511,7 @@ export class JiraCloudProvider implements IProvider { .catch((error) => { let specificError = error if (error.response) { - if (error.response.status === 400) { - specificError = new Error(`Invalid request: ${error.response.data}`) - } else if (error.response.status === 403) { + if (error.response.status === 403) { specificError = new Error( `User does not have a valid licence or permissions to assign issues: ${error.response.data}` ) @@ -540,9 +538,7 @@ export class JiraCloudProvider implements IProvider { .catch((error) => { let specificError = error if (error.response) { - if (error.response.status === 400) { - specificError = new Error(`Invalid request: ${error.response.data}`) - } else if (error.response.status === 403) { + if (error.response.status === 403) { specificError = new Error( `User does not have a valid licence or permissions to assign issues: ${error.response.data}` ) @@ -595,9 +591,7 @@ export class JiraCloudProvider implements IProvider { .catch((error) => { let specificError = error if (error.response) { - if (error.response.status === 400) { - specificError = new Error(`Invalid request: ${error.response.data}`) - } else if (error.response.status === 403) { + if (error.response.status === 403) { specificError = new Error( `User does not have a valid licence or permissions to rank issues: ${error.response.data}` ) @@ -713,9 +707,7 @@ export class JiraCloudProvider implements IProvider { .catch((error) => { let specificError = error if (error.response) { - if (error.response.status === 400) { - specificError = new Error(error.response.data) - } else if (error.response.status === 404) { + if (error.response.status === 404) { specificError = new Error("The user does not have the necessary permissions") } } @@ -816,11 +808,7 @@ export class JiraCloudProvider implements IProvider { .catch((error) => { let specificError = error if (error.response) { - if (error.response.status === 400) { - specificError = new Error( - "400 Error: consult the atlassian rest api v3 under Edit issue for information" - ) - } else if (error.response.status === 403) { + if (error.response.status === 403) { specificError = new Error( "The user does not have the necessary permissions" ) @@ -876,9 +864,7 @@ export class JiraCloudProvider implements IProvider { .catch((error) => { let specificError = error if (error.response) { - if (error.response.status === 400) { - specificError = new Error(`Invalid request: ${error.response.data}`) - } else if (error.response.status === 403) { + if (error.response.status === 403) { specificError = new Error(`User does not have a valid licence: ${error.response.data}`) } else if (error.response.status === 404) { specificError = new Error( @@ -951,9 +937,7 @@ export class JiraCloudProvider implements IProvider { .catch((error) => { let specificError = error if (error.response) { - if (error.response.status === 400) { - specificError = new Error("Invalid api request") - } else if (error.response.status === 404) { + if (error.response.status === 404) { specificError = new Error("The issue was not found or the user does not have the necessary permissions") } } @@ -1084,9 +1068,7 @@ export class JiraCloudProvider implements IProvider { .catch((error) => { let specificError = error if (error.response) { - if (error.response.status === 400) { - specificError = new Error(`Invalid request: ${error.response.data}`) - } else if (error.response.status === 403) { + if (error.response.status === 403) { specificError = new Error(`User does not have a valid licence: ${error.response.data}`) } } @@ -1140,9 +1122,7 @@ export class JiraCloudProvider implements IProvider { .catch((error) => { let specificError = error if (error.response) { - if (error.response.status === 400) { - specificError = new Error("Invalid request") - } else if (error.response.status === 403) { + if (error.response.status === 403) { specificError = new Error("The user does not have the necessary permissions") } else if (error.response.status === 404) { specificError = new Error("The Board does not exist or the user does not have the necessary permissions to view it") From ead343836634ae893fb5086a84b811d8faa09893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ru=CC=88sch?= Date: Sun, 12 Nov 2023 16:47:29 +0100 Subject: [PATCH 09/19] Universally handle 429 errors --- electron/providers/jira-cloud-provider/JiraCloudProvider.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electron/providers/jira-cloud-provider/JiraCloudProvider.ts b/electron/providers/jira-cloud-provider/JiraCloudProvider.ts index aefb0740..b48f9b3e 100644 --- a/electron/providers/jira-cloud-provider/JiraCloudProvider.ts +++ b/electron/providers/jira-cloud-provider/JiraCloudProvider.ts @@ -60,6 +60,8 @@ export class JiraCloudProvider implements IProvider { return Promise.reject(recreateAxiosError(error, `Invalid request: ${JSON.stringify(error.response.data)}`)) } else if (statusCode === 401) { return Promise.reject(recreateAxiosError(error, `User not authenticated: ${JSON.stringify(error.response.data)}`)) + } else if (error.response.status === 429) { + return Promise.reject(recreateAxiosError(error, `Rate limit exceeded: ${JSON.stringify(error.response.data)}`)) } } @@ -286,8 +288,6 @@ export class JiraCloudProvider implements IProvider { if (error.response) { if (error.response.status === 404) { specificError = new Error(`Project, issue, or transition were not found: ${error.response.data}`) - } else if (error.response.status === 429) { - specificError = new Error(`Rate limit exceeded: ${error.response.data}`) } } From cf84f140f3d741373ebfcb08eec4a0a73baee5fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ru=CC=88sch?= Date: Sun, 12 Nov 2023 16:51:34 +0100 Subject: [PATCH 10/19] Handle exposed resource through API client --- electron/providers/jira-cloud-provider/JiraCloudProvider.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/electron/providers/jira-cloud-provider/JiraCloudProvider.ts b/electron/providers/jira-cloud-provider/JiraCloudProvider.ts index b48f9b3e..6b40b74e 100644 --- a/electron/providers/jira-cloud-provider/JiraCloudProvider.ts +++ b/electron/providers/jira-cloud-provider/JiraCloudProvider.ts @@ -1081,9 +1081,11 @@ export class JiraCloudProvider implements IProvider { getResource(): Promise { return new Promise((resolve, reject) => { if (this.accessToken !== undefined) { + // IMPROVE expose API client instead of resource + const defaults = this.getRestApiClient(3).defaults const result: Resource = { - baseUrl: `https://api.atlassian.com/ex/jira/${this.cloudID}/rest/api/3/`, - authorization: `Bearer ${this.accessToken}`, + baseUrl: defaults.baseURL ?? '', + authorization: defaults.headers.Authorization as string, } resolve(result) } else { From 053ef385452d093aaa10b01e6ec8a2edd05b8f9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ru=CC=88sch?= Date: Sun, 12 Nov 2023 16:54:44 +0100 Subject: [PATCH 11/19] Universally handle generic 403 errors --- .../jira-cloud-provider/JiraCloudProvider.ts | 38 ++++--------------- 1 file changed, 8 insertions(+), 30 deletions(-) diff --git a/electron/providers/jira-cloud-provider/JiraCloudProvider.ts b/electron/providers/jira-cloud-provider/JiraCloudProvider.ts index 6b40b74e..618100d6 100644 --- a/electron/providers/jira-cloud-provider/JiraCloudProvider.ts +++ b/electron/providers/jira-cloud-provider/JiraCloudProvider.ts @@ -60,6 +60,8 @@ export class JiraCloudProvider implements IProvider { return Promise.reject(recreateAxiosError(error, `Invalid request: ${JSON.stringify(error.response.data)}`)) } else if (statusCode === 401) { return Promise.reject(recreateAxiosError(error, `User not authenticated: ${JSON.stringify(error.response.data)}`)) + } else if (error.response.status === 403) { + return Promise.reject(recreateAxiosError(error, `User does not have a valid licence: ${JSON.stringify(error.response.data)}`)) } else if (error.response.status === 429) { return Promise.reject(recreateAxiosError(error, `Rate limit exceeded: ${JSON.stringify(error.response.data)}`)) } @@ -342,14 +344,7 @@ export class JiraCloudProvider implements IProvider { resolve(boardIds) }) .catch((error) => { - let specificError = error - if (error.response) { - if (error.response.status === 403) { - specificError = new Error(`User does not have a valid licence: ${error.response.data}`) - } - } - - reject(new Error(`Error getting projects: ${specificError}`)) + reject(new Error(`Error getting projects: ${error}`)) }) }) } @@ -385,9 +380,7 @@ export class JiraCloudProvider implements IProvider { .catch((error) => { let specificError = error if (error.response) { - if (error.response.status === 403) { - specificError = new Error(`User does not have a valid licence: ${error.response.data}`) - } else if (error.response.status === 404) { + if (error.response.status === 404) { specificError = new Error( `The board does not exist or the user does not have permission to view it: ${error.response.data}` ) @@ -478,9 +471,7 @@ export class JiraCloudProvider implements IProvider { return error; } - if (error.response.status === 403) { - return new Error(`User does not have a valid licence: ${error.response.data}`) - } else if (error.response.status === 404) { + if (error.response.status === 404) { return new Error( `The board does not exist or the user does not have permission to view it: ${error.response.data}` ) @@ -808,11 +799,7 @@ export class JiraCloudProvider implements IProvider { .catch((error) => { let specificError = error if (error.response) { - if (error.response.status === 403) { - specificError = new Error( - "The user does not have the necessary permissions" - ) - } else if (error.response.status === 404) { + if (error.response.status === 404) { specificError = new Error( "The issue was not found or the user does not have the necessary permissions" ) @@ -864,9 +851,7 @@ export class JiraCloudProvider implements IProvider { .catch((error) => { let specificError = error if (error.response) { - if (error.response.status === 403) { - specificError = new Error(`User does not have a valid licence: ${error.response.data}`) - } else if (error.response.status === 404) { + if (error.response.status === 404) { specificError = new Error( `The board does not exist or the user does not have permission to view it: ${error.response.data}` ) @@ -1066,14 +1051,7 @@ export class JiraCloudProvider implements IProvider { resolve(createdSubtask) }) .catch((error) => { - let specificError = error - if (error.response) { - if (error.response.status === 403) { - specificError = new Error(`User does not have a valid licence: ${error.response.data}`) - } - } - - reject(new Error(`Error creating subtask: ${specificError}`)) + reject(new Error(`Error creating subtask: ${error}`)) }) }) } From 65775f062ce5abe5bb729de15b4c18bb24540cf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Sun, 12 Nov 2023 19:58:58 +0100 Subject: [PATCH 12/19] Add new Jira server API clients --- .../JiraServerProvider.ts | 56 ++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/electron/providers/jira-server-provider/JiraServerProvider.ts b/electron/providers/jira-server-provider/JiraServerProvider.ts index a45a3a14..6dc7daeb 100644 --- a/electron/providers/jira-server-provider/JiraServerProvider.ts +++ b/electron/providers/jira-server-provider/JiraServerProvider.ts @@ -18,6 +18,7 @@ import { JiraSprint, } from "../../../types/jira" import { IProvider } from "../base-provider" +import axios, { AxiosError, isAxiosError } from "axios"; export class JiraServerProvider implements IProvider { private loginOptions = { @@ -28,12 +29,65 @@ export class JiraServerProvider implements IProvider { private customFields = new Map() - getAuthHeader() { + private getAuthHeader() { return `Basic ${Buffer.from( `${this.loginOptions.username}:${this.loginOptions.password}` ).toString("base64")}` } + private constructRestBasedClient(apiName: string, version: string) { + const instance = axios.create({ + baseURL: `${this.loginOptions.url}/rest/${apiName}/${version}`, + headers: { + Accept: "application/json", + Authorization: this.getAuthHeader(), + "Content-Type": "application/json", + }, + }) + + const recreateAxiosError = (originalError: AxiosError, message: string) => new AxiosError( + message, + originalError.code, + originalError.config, + originalError.request, + originalError.response + ) + + instance.interceptors.response.use( + (response) => response, + (error) => { + if (isAxiosError(error) && error.response) { + const statusCode = error.response.status + if (statusCode === 400) { + return Promise.reject(recreateAxiosError(error, `Invalid request: ${JSON.stringify(error.response.data)}`)) + } else if (statusCode === 401) { + return Promise.reject(recreateAxiosError(error, `User not authenticated: ${JSON.stringify(error.response.data)}`)) + } else if (error.response.status === 403) { + return Promise.reject(recreateAxiosError(error, `User does not have a valid licence: ${JSON.stringify(error.response.data)}`)) + } else if (error.response.status === 429) { + return Promise.reject(recreateAxiosError(error, `Rate limit exceeded: ${JSON.stringify(error.response.data)}`)) + } + } + + return Promise.reject(error) + } + ) + + return instance + } + + private getRestApiClient(version: number) { + return this.constructRestBasedClient('api', version.toString()); + } + + private getAuthRestApiClient(version: number) { + return this.constructRestBasedClient('auth', version.toString()); + } + + private getAgileRestApiClient(version: string) { + return this.constructRestBasedClient('agile', version); + } + async login({ basicLoginOptions, }: { From de7083f83727798304a0f823931be6e9125f7175 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Sun, 12 Nov 2023 19:59:16 +0100 Subject: [PATCH 13/19] Migrate trivial Jira server calls to new API clients --- .../JiraServerProvider.ts | 303 +++++++----------- 1 file changed, 111 insertions(+), 192 deletions(-) diff --git a/electron/providers/jira-server-provider/JiraServerProvider.ts b/electron/providers/jira-server-provider/JiraServerProvider.ts index 6dc7daeb..fcd21954 100644 --- a/electron/providers/jira-server-provider/JiraServerProvider.ts +++ b/electron/providers/jira-server-provider/JiraServerProvider.ts @@ -107,160 +107,122 @@ export class JiraServerProvider implements IProvider { async isLoggedIn(): Promise { return new Promise((resolve, reject) => { - fetch(`${this.loginOptions.url}/rest/auth/1/session`, { - method: "GET", - headers: { - Accept: "application/json", - Authorization: this.getAuthHeader(), - }, - }) - .then((response) => { - if (response.status === 200) resolve() - if (response.status === 401) { - reject(new Error("Wrong Username or Password")) - } - if (response.status === 404) { - reject(new Error("Wrong URL")) + this.getAuthRestApiClient(1) + .get('/session') + .then(() => { resolve() }) + .catch((error) => { + if (isAxiosError(error) && error.response) { + if (error.response.status === 401) { + return Promise.reject(new Error("Wrong Username or Password")) + } else if (error.response.status === 404) { + return Promise.reject(new Error("Wrong URL")) + } } }) - .catch((err) => { - if (err.name === "FetchError") reject(new Error("Wrong URL")) + .catch((error) => { + reject(new Error(`Error in checking login status: ${error}`)) }) }) } async logout(): Promise { return new Promise((resolve, reject) => { - fetch(`${this.loginOptions.url}/rest/auth/1/session`, { - method: "DELETE", - headers: { - Authorization: this.getAuthHeader(), - }, - }).then((res) => { - if (res.status === 204) { - resolve() - } - if (res.status === 401) { - reject(new Error("user not authenticated")) - } - }) + this.getAuthRestApiClient(1) + .delete('/session') + .then(() => { resolve() }) + .catch((error) => { + reject(new Error(`Error in logging out: ${error}`)) + }) }) } async mapCustomFields(): Promise { - const response = await fetch(`${this.loginOptions.url}/rest/api/2/field`, { - method: "GET", - headers: { - Accept: "application/json", - Authorization: this.getAuthHeader(), - }, - }) - const data = await response.json() - data.forEach((field: { name: string; id: string }) => { - this.customFields.set(field.name, field.id) + return new Promise((resolve, reject) => { + this.getRestApiClient(2) + .delete('/field') + .then((response) => { + response.data.forEach((field: { name: string; id: string }) => { + this.customFields.set(field.name, field.id) + }) + resolve() + }) + .catch((error) => { + reject(new Error(`Error in mapping custom fields: ${error}`)) + }) }) } async getProjects(): Promise { - const response = await fetch( - `${this.loginOptions.url}/rest/api/2/project?expand=lead,description`, - { - method: "GET", - headers: { - Accept: "application/json", - Authorization: this.getAuthHeader(), - }, - } - ) - if (response.ok) { - const data = await response.json() - const projects = data.map((project: JiraProject) => ({ - key: project.key, - name: project.name, - id: project.id, - lead: project.lead.displayName, - type: project.projectTypeKey, - })) - return projects - } - return Promise.reject(new Error(response.statusText)) + return new Promise((resolve, _) => { + this.getRestApiClient(2) + .get('/project?expand=lead,description') + .then((response) => { + const projects = response.data.map((project: JiraProject) => ({ + key: project.key, + name: project.name, + id: project.id, + lead: project.lead.displayName, + type: project.projectTypeKey, + })) + resolve(projects) + }) + }) } async getIssueTypesByProject(projectIdOrKey: string): Promise { return new Promise((resolve, reject) => { - fetch( - `${this.loginOptions.url}/rest/api/2/project/${projectIdOrKey}/statuses`, - { - headers: { - Accept: "application/json", - Authorization: this.getAuthHeader(), - }, - } - ) + this.getRestApiClient(2) + .get(`/project/${projectIdOrKey}/statuses`) .then(async (response) => { - const issueTypes: JiraIssueType[] = await response.json() + const issueTypes: JiraIssueType[] = response.data resolve(issueTypes as IssueType[]) }) - .catch((error) => - reject(new Error(`Error in fetching the issue types: ${error}`)) - ) + .catch((error) => reject(new Error(`Error in fetching the issue types: ${error}`))) }) } async getBoardIds(project: string): Promise { - const response = await fetch( - `${this.loginOptions.url}/rest/agile/1.0/board?projectKeyOrId=${project}`, - { - method: "GET", - headers: { - Accept: "application/json", - Authorization: this.getAuthHeader(), - }, - } - ) - - const data = await response.json() - - const boardIds: number[] = data.values.map( - (element: { id: number; name: string }) => element.id - ) - return boardIds + return new Promise((resolve, reject) => { + this.getAgileRestApiClient('1.0') + .get(`/board?projectKeyOrId=${project}`) + .then(async (response) => { + const boardIds: number[] = response.data.values.map( + (element: { id: number; name: string }) => element.id + ) + resolve(boardIds) + }) + .catch((error) => reject(new Error(`Error in fetching the boards: ${error}`))) + }) } async getSprints(boardId: number): Promise { - const response = await fetch( - `${this.loginOptions.url}/rest/agile/1.0/board/${boardId}/sprint`, - { - method: "GET", - headers: { - Accept: "application/json", - Authorization: this.getAuthHeader(), - }, - } - ) - - const data = await response.json() - - const sprints: Sprint[] = data.values - .filter((element: { state: string }) => element.state !== "closed") - .map((element: JiraSprint) => { - const sDate = new Date(element.startDate) - const startDate = Number.isNaN(sDate.getTime()) - ? "Invalid Date" - : dateTimeFormat.format(sDate) - const eDate = new Date(element.endDate) - const endDate = Number.isNaN(eDate.getTime()) - ? "Invalid Date" - : dateTimeFormat.format(eDate) - return { - id: element.id, - name: element.name, - state: element.state, - startDate, - endDate, - } - }) - return sprints + return new Promise((resolve, reject) => { + this.getAgileRestApiClient('1.0') + .get(`/board/${boardId}/sprint`) + .then(async (response) => { + const sprints: Sprint[] = response.data.values + .filter((element: { state: string }) => element.state !== "closed") + .map((element: JiraSprint) => { + const sDate = new Date(element.startDate) + const startDate = Number.isNaN(sDate.getTime()) + ? "Invalid Date" + : dateTimeFormat.format(sDate) + const eDate = new Date(element.endDate) + const endDate = Number.isNaN(eDate.getTime()) + ? "Invalid Date" + : dateTimeFormat.format(eDate) + return { + id: element.id, + name: element.name, + state: element.state, + startDate, + endDate, + } + }) + resolve(sprints) + }) + .catch((error) => reject(new Error(`Error in fetching the boards: ${error}`))) + }) } async getIssuesByProject(project: string, boardId: number): Promise { @@ -330,53 +292,33 @@ export class JiraServerProvider implements IProvider { ): Promise { return new Promise((resolve, reject) => { const rankCustomField = this.customFields.get("Rank") - const body = { - rankCustomFieldId: rankCustomField!.match(/_(\d+)/)![1], - issues: [issue], - ...(rankAfter && { rankAfterIssue: rankAfter }), - ...(rankBefore && { rankBeforeIssue: rankBefore }), - } - fetch(`${this.loginOptions.url}/rest/agile/1.0/sprint/${sprint}/issue`, { - method: "POST", - headers: { - Accept: "application/json", - Authorization: this.getAuthHeader(), - "Content-Type": "application/json", - }, - body: JSON.stringify(body), - }) + this.getAgileRestApiClient('1.0') + .post( + `/sprint/${sprint}/issue`, + { + rankCustomFieldId: rankCustomField!.match(/_(\d+)/)![1], + issues: [issue], + ...(rankAfter && { rankAfterIssue: rankAfter }), + ...(rankBefore && { rankBeforeIssue: rankBefore }), + } + ) .then(() => resolve()) - .catch((error) => { - reject( - new Error( - `Error in moving this issue to the Sprint with id ${sprint}: ${error}` - ) - ) + reject(new Error(`Error in moving this issue to the Sprint with id ${sprint}: ${error}`)) }) }) } async moveIssueToBacklog(issue: string): Promise { return new Promise((resolve, reject) => { - fetch(`${this.loginOptions.url}/rest/agile/1.0/backlog/issue`, { - method: "POST", - headers: { - Accept: "application/json", - Authorization: this.getAuthHeader(), - "Content-Type": "application/json", - }, - body: `{ - "issues": [ - "${issue}" - ] - }`, - }) + this.getAgileRestApiClient('1.0') + .post( + '/backlog/issue', + { issues: [issue] } + ) .then(() => resolve()) .catch((error) => - reject( - new Error(`Error in moving this issue to the Backlog: ${error}`) - ) + reject(new Error(`Error in moving this issue to the Backlog: ${error}`)) ) }) } @@ -402,50 +344,27 @@ export class JiraServerProvider implements IProvider { } else if (rankAfter) { body.rankAfterIssue = rankAfter } - fetch(`http://${this.loginOptions.url}/rest/agile/1.0/issue/rank`, { - method: "PUT", - headers: { - Accept: "application/json", - Authorization: this.getAuthHeader(), - "Content-Type": "application/json", - }, - body: JSON.stringify(body), - }) - .then(() => { - resolve() - }) - + this.getAgileRestApiClient('1.0') + .put('/issue/rank', body) + .then(() => resolve()) .catch((error) => - reject( - new Error(`Error in moving this issue to the Backlog: ${error}`) - ) + reject(new Error(`Error in moving this issue to the Backlog: ${error}`)) ) }) } async getIssueStoryPointsEstimate(issue: string): Promise { return new Promise((resolve, reject) => { - fetch(`${this.loginOptions.url}/rest/api/2/issue/${issue}`, { - method: "GET", - headers: { - Accept: "application/json", - Authorization: this.getAuthHeader(), - }, - }) + this.getRestApiClient(2) + .get(`/issue/${issue}`) .then(async (response) => { - const data = await response.json() const customField = this.customFields.get("Story Points") - const points: number = data.fields[customField!] + const points: number = response.data.fields[customField!] resolve(points) - return points }) .catch((error) => - reject( - new Error( - `Error in getting the story points for issue: ${issue}: ${error}` - ) - ) + reject(new Error(`Error in getting the story points for issue: ${issue}: ${error}`)) ) }) } From f54a3e9d5aeee8a323f7b413e30fe522ac503561 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Sun, 12 Nov 2023 21:35:42 +0100 Subject: [PATCH 14/19] Migrate non-trivial issue fetching API requests --- .../JiraServerProvider.ts | 56 ++++++++----------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/electron/providers/jira-server-provider/JiraServerProvider.ts b/electron/providers/jira-server-provider/JiraServerProvider.ts index fcd21954..eb476504 100644 --- a/electron/providers/jira-server-provider/JiraServerProvider.ts +++ b/electron/providers/jira-server-provider/JiraServerProvider.ts @@ -1,5 +1,3 @@ -/* eslint-disable class-methods-use-this */ -import fetch from "cross-fetch" import { dateTimeFormat, Issue, @@ -11,14 +9,9 @@ import { SprintCreate, User, } from "../../../types" -import { - JiraIssue, - JiraIssueType, - JiraProject, - JiraSprint, -} from "../../../types/jira" -import { IProvider } from "../base-provider" -import axios, { AxiosError, isAxiosError } from "axios"; +import {JiraIssue, JiraIssueType, JiraProject, JiraSprint,} from "../../../types/jira" +import {IProvider} from "../base-provider" +import axios, {AxiosError, AxiosResponse, isAxiosError} from "axios"; export class JiraServerProvider implements IProvider { private loginOptions = { @@ -226,9 +219,12 @@ export class JiraServerProvider implements IProvider { } async getIssuesByProject(project: string, boardId: number): Promise { - return this.fetchIssues( - `${this.loginOptions.url}/rest/agile/1.0/board/${boardId}/issue?jql=project=${project}&maxResults=10000` - ) + return new Promise((resolve, reject) => { + this.getAgileRestApiClient('1.0') + .get(`/board/${boardId}/issue?jql=project=${project}&maxResults=10000`) + .then((response) => resolve(this.fetchIssues(response))) + .catch((error) => reject(new Error(`Error in fetching issues: ${error}`))) + }) } async getIssuesBySprintAndProject( @@ -236,34 +232,31 @@ export class JiraServerProvider implements IProvider { project: string, boardId: number ): Promise { - return this.fetchIssues( - `${this.loginOptions.url}/rest/agile/1.0/board/${boardId}/sprint/${sprintId}/issue?jql=project=${project}` - ) + return new Promise((resolve, reject) => { + this.getAgileRestApiClient('1.0') + .get(`/board/${boardId}/sprint/${sprintId}/issue?jql=project=${project}`) + .then((response) => resolve(this.fetchIssues(response))) + .catch((error) => reject(new Error(`Error in fetching issues: ${error}`))) + }) } async getBacklogIssuesByProjectAndBoard( project: string, boardId: number ): Promise { - return this.fetchIssues( - `${this.loginOptions.url}/rest/agile/1.0/board/${boardId}/backlog?jql=sprint is EMPTY AND project=${project}` - ) + return new Promise((resolve, reject) => { + this.getAgileRestApiClient('1.0') + .get(`/board/${boardId}/backlog?jql=sprint is EMPTY AND project=${project}`) + .then((response) => resolve(this.fetchIssues(response))) + .catch((error) => reject(new Error(`Error in fetching issues: ${error}`))) + }) } - async fetchIssues(url: string): Promise { + async fetchIssues(response: AxiosResponse): Promise { const rankCustomField = this.customFields.get("Rank") - const response = await fetch(url, { - method: "GET", - headers: { - Accept: "application/json", - Authorization: this.getAuthHeader(), - }, - }) - - const data = await response.json() - const issues: Promise = Promise.all( - data.issues.map(async (element: JiraIssue) => ({ + return Promise.all( + response.data.issues.map(async (element: JiraIssue) => ({ issueKey: element.key, summary: element.fields.summary, creator: element.fields.creator.name, @@ -281,7 +274,6 @@ export class JiraServerProvider implements IProvider { rank: element.fields[rankCustomField!], })) ) - return issues } async moveIssueToSprintAndRank( From 53369ef36202a0354b9eaa74379b8968c73888e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Sun, 12 Nov 2023 21:50:03 +0100 Subject: [PATCH 15/19] Fix eslint --- .../jira-server-provider/JiraServerProvider.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/electron/providers/jira-server-provider/JiraServerProvider.ts b/electron/providers/jira-server-provider/JiraServerProvider.ts index eb476504..883fabb8 100644 --- a/electron/providers/jira-server-provider/JiraServerProvider.ts +++ b/electron/providers/jira-server-provider/JiraServerProvider.ts @@ -1,3 +1,5 @@ +/* eslint-disable class-methods-use-this */ +import axios, {AxiosError, AxiosResponse, isAxiosError} from "axios"; import { dateTimeFormat, Issue, @@ -11,7 +13,6 @@ import { } from "../../../types" import {JiraIssue, JiraIssueType, JiraProject, JiraSprint,} from "../../../types/jira" import {IProvider} from "../base-provider" -import axios, {AxiosError, AxiosResponse, isAxiosError} from "axios"; export class JiraServerProvider implements IProvider { private loginOptions = { @@ -53,11 +54,11 @@ export class JiraServerProvider implements IProvider { const statusCode = error.response.status if (statusCode === 400) { return Promise.reject(recreateAxiosError(error, `Invalid request: ${JSON.stringify(error.response.data)}`)) - } else if (statusCode === 401) { + } if (statusCode === 401) { return Promise.reject(recreateAxiosError(error, `User not authenticated: ${JSON.stringify(error.response.data)}`)) - } else if (error.response.status === 403) { + } if (error.response.status === 403) { return Promise.reject(recreateAxiosError(error, `User does not have a valid licence: ${JSON.stringify(error.response.data)}`)) - } else if (error.response.status === 429) { + } if (error.response.status === 429) { return Promise.reject(recreateAxiosError(error, `Rate limit exceeded: ${JSON.stringify(error.response.data)}`)) } } @@ -107,10 +108,12 @@ export class JiraServerProvider implements IProvider { if (isAxiosError(error) && error.response) { if (error.response.status === 401) { return Promise.reject(new Error("Wrong Username or Password")) - } else if (error.response.status === 404) { + } if (error.response.status === 404) { return Promise.reject(new Error("Wrong URL")) } } + + return Promise.reject(error) }) .catch((error) => { reject(new Error(`Error in checking login status: ${error}`)) @@ -146,7 +149,7 @@ export class JiraServerProvider implements IProvider { } async getProjects(): Promise { - return new Promise((resolve, _) => { + return new Promise((resolve) => { this.getRestApiClient(2) .get('/project?expand=lead,description') .then((response) => { From caa02310999c5fbd91196fdf359cabfa726b42d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Sun, 12 Nov 2023 21:50:56 +0100 Subject: [PATCH 16/19] Fix eslint --- .../jira-cloud-provider/JiraCloudProvider.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/electron/providers/jira-cloud-provider/JiraCloudProvider.ts b/electron/providers/jira-cloud-provider/JiraCloudProvider.ts index 618100d6..7bf62a0a 100644 --- a/electron/providers/jira-cloud-provider/JiraCloudProvider.ts +++ b/electron/providers/jira-cloud-provider/JiraCloudProvider.ts @@ -1,4 +1,5 @@ /* eslint-disable class-methods-use-this */ +import axios, { AxiosError, AxiosResponse, isAxiosError } from "axios"; import { dateTimeFormat, Issue, @@ -19,8 +20,6 @@ import { } from "../../../types/jira" import { IProvider } from "../base-provider" import { getAccessToken, refreshTokens } from "./getAccessToken" -import { AxiosError, AxiosResponse, isAxiosError } from "axios"; -import axios from "axios" export class JiraCloudProvider implements IProvider { public accessToken: string | undefined @@ -58,11 +57,11 @@ export class JiraCloudProvider implements IProvider { const statusCode = error.response.status if (statusCode === 400) { return Promise.reject(recreateAxiosError(error, `Invalid request: ${JSON.stringify(error.response.data)}`)) - } else if (statusCode === 401) { + } if (statusCode === 401) { return Promise.reject(recreateAxiosError(error, `User not authenticated: ${JSON.stringify(error.response.data)}`)) - } else if (error.response.status === 403) { + } if (error.response.status === 403) { return Promise.reject(recreateAxiosError(error, `User does not have a valid licence: ${JSON.stringify(error.response.data)}`)) - } else if (error.response.status === 429) { + } if (error.response.status === 429) { return Promise.reject(recreateAxiosError(error, `Rate limit exceeded: ${JSON.stringify(error.response.data)}`)) } } @@ -436,7 +435,7 @@ export class JiraCloudProvider implements IProvider { async fetchIssues(response: AxiosResponse): Promise { const rankCustomField = this.customFields.get("Rank") || "" - return new Promise((resolve, _) => { + return new Promise((resolve) => { const issues: Promise = Promise.all( response.data.issues.map(async (element: JiraIssue) => ({ issueKey: element.key, @@ -817,7 +816,7 @@ export class JiraCloudProvider implements IProvider { `/issue/${issueKey}/transitions`, ) - const data = transitionResponse.data + const {data} = transitionResponse data.transitions.forEach((field: { name: string; id: string }) => { transitions.set(field.name, field.id) @@ -1060,7 +1059,7 @@ export class JiraCloudProvider implements IProvider { return new Promise((resolve, reject) => { if (this.accessToken !== undefined) { // IMPROVE expose API client instead of resource - const defaults = this.getRestApiClient(3).defaults + const {defaults} = this.getRestApiClient(3) const result: Resource = { baseUrl: defaults.baseURL ?? '', authorization: defaults.headers.Authorization as string, From 50c152db251b670d43d3b2f655fabd0ea433231d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Sun, 12 Nov 2023 22:03:46 +0100 Subject: [PATCH 17/19] Fix custom fields mapping --- electron/providers/jira-server-provider/JiraServerProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electron/providers/jira-server-provider/JiraServerProvider.ts b/electron/providers/jira-server-provider/JiraServerProvider.ts index 883fabb8..6cd77268 100644 --- a/electron/providers/jira-server-provider/JiraServerProvider.ts +++ b/electron/providers/jira-server-provider/JiraServerProvider.ts @@ -135,7 +135,7 @@ export class JiraServerProvider implements IProvider { async mapCustomFields(): Promise { return new Promise((resolve, reject) => { this.getRestApiClient(2) - .delete('/field') + .get('/field') .then((response) => { response.data.forEach((field: { name: string; id: string }) => { this.customFields.set(field.name, field.id) From daae882a183bdb265a17542db7a341bd8a8d9b97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Sat, 18 Nov 2023 18:13:06 +0100 Subject: [PATCH 18/19] Add ADR for axios --- doc/architecture-decisions/ADR-011.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 doc/architecture-decisions/ADR-011.md diff --git a/doc/architecture-decisions/ADR-011.md b/doc/architecture-decisions/ADR-011.md new file mode 100644 index 00000000..96b5fd05 --- /dev/null +++ b/doc/architecture-decisions/ADR-011.md @@ -0,0 +1,19 @@ +# ADR 11: Axios for calls to the Jira API + +## Status + +accepted + +## Context + +We want to be able to configure the headers, base URL and other parameters once for Jira API calls, instead of each time we make a call as previous with the `fetch` and `cross-fetch` packages. + +## Decision + +We are now using [Axios](https://axios-http.com/docs/intro), a package that enables creation of REST clients and enables us to configure defaults for each call. +Additionally, we are able to define error handlers for specific HTTP error calls, enabling default error handling for e.g. 401 responses. + +## Consequences + +Every call to the Jira Server API (except for authorization calls to e.g. fetch a token) should be made via an Axios instance. +This also means rewriting the error handling in each call, finding common errors and enabling default handling for them. From 77b179a147697f0af9ad906bd3ddf0187f553426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Sun, 19 Nov 2023 18:22:02 +0100 Subject: [PATCH 19/19] Fix get projects --- electron/providers/jira-cloud-provider/JiraCloudProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electron/providers/jira-cloud-provider/JiraCloudProvider.ts b/electron/providers/jira-cloud-provider/JiraCloudProvider.ts index 7bf62a0a..c9d4cb21 100644 --- a/electron/providers/jira-cloud-provider/JiraCloudProvider.ts +++ b/electron/providers/jira-cloud-provider/JiraCloudProvider.ts @@ -184,7 +184,7 @@ export class JiraCloudProvider implements IProvider { async getProjects(): Promise { return new Promise((resolve, reject) => { this.getRestApiClient(3) - .get('/project/searchs?expand=description,lead,issueTypes,url,projectKeys,permissions,insight') + .get('/project/search?expand=description,lead,issueTypes,url,projectKeys,permissions,insight') .then(async (response) => { const projects = response.data.values.map((project: JiraProject) => ({ key: project.key,