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. diff --git a/electron/providers/jira-cloud-provider/JiraCloudProvider.ts b/electron/providers/jira-cloud-provider/JiraCloudProvider.ts index 3d65ba79..c9d4cb21 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, @@ -31,6 +32,55 @@ export class JiraCloudProvider implements IProvider { private reversedCustomFields = new Map() + private constructRestBasedClient(basePath: string, version: string) { + 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", + }, + }) + + 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)}`)) + } if (statusCode === 401) { + return Promise.reject(recreateAxiosError(error, `User not authenticated: ${JSON.stringify(error.response.data)}`)) + } if (error.response.status === 403) { + return Promise.reject(recreateAxiosError(error, `User does not have a valid licence: ${JSON.stringify(error.response.data)}`)) + } 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 getAgileRestApiClient(version: string) { + return this.constructRestBasedClient('agile', version); + } + offsetDate(date: Date) { if (!date) { return date @@ -116,28 +166,14 @@ 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) { - 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) { - reject(new Error(`User not authenticated: ${fetchedFields}`)) - } else { - reject(new Error(`Unknown error: ${fetchedFields}`)) - } + this.getRestApiClient(3) + .get('/field') + .then(async (response) => { + 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}`)) @@ -147,118 +183,77 @@ 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() - 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 === 401) { - reject(new Error(`User not authenticated: ${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 === 404) { + specificError = new Error(`No projects matching the search criteria were found: ${error.response.data}`) + } + } + + reject(new Error(`Error getting projects: ${specificError}`)) }) }) } 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) => { - if (response.status === 200) { - const issueTypes: JiraIssueType[] = await response.json() - resolve(issueTypes as IssueType[]) - } else if (response.status === 401) { - reject( - new Error(`User not authenticated: ${await response.json()}`) - ) - } 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()}` + 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}` ) - ) - } else { - reject(new Error(`Unknown error: ${await response.json()}`)) + } } + + reject(new Error(`Error in fetching the issue types: ${specificError}`)) }) - .catch((error) => - reject(new Error(`Error in fetching the issue types: ${error}`)) - ) }) } 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() - 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 if (response.status === 401) { - reject(new Error(`User not authenticated: ${metadata}`)) - } 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}`)) @@ -268,27 +263,13 @@ 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() - if (response.status === 200) { - const fieldKeys = Object.keys(metadata.fields).map( - (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}`)) - } + 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}`)) @@ -298,71 +279,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) => { - if (response.status === 200) { - const users: User[] = await response.json() - resolve(users as User[]) - } else if (response.status === 400) { - reject( - new Error(`Some infos are missing: ${await response.json()}`) - ) - } else if (response.status === 401) { - reject( - new Error(`User not authenticated: ${await response.json()}`) - ) - } else if (response.status === 404) { - reject( - new Error( - `Project, issue, or transition were not found: ${await response.json()}` - ) - ) - } else if (response.status === 429) { - reject(new Error(`Rate limit exceeded: ${await response.json()}`)) - } else { - reject(new Error(`Unknown error: ${await response.json()}`)) + resolve(response.data as User[]) + }) + .catch((error) => { + let specificError = error + if (error.response) { + if (error.response.status === 404) { + specificError = new Error(`Project, issue, or transition were not found: ${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}` - ) - ) - ) }) } 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) => { - if (response.status === 200) { - const user: User = await response.json() - resolve(user as User) - } else if (response.status === 401) { - reject( - new Error(`User not authenticated: ${await response.json()}`) - ) - } else { - reject(new Error(`Unknown error: ${await response.json()}`)) - } + resolve(response.data as User) }) .catch((error) => reject(new Error(`Error in fetching the current user: ${error}`)) @@ -372,64 +312,35 @@ 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() - 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}` + 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}` ) - ) - } else { - reject(new Error(`Unknown error: ${user}`)) + } } + + reject(new Error(`Error in fetching the issue reporter: ${specificError}`)) }) - .catch((error) => - reject(new Error(`Error in fetching the current user: ${error}`)) - ) }) } 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() - 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 === 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 { - 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}`)) @@ -439,86 +350,69 @@ 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() - if (response.status === 200) { - 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, - } - }) - resolve(sprints) - } 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}` - ) + const sprints: Sprint[] = response.data.values + .filter( + (element: { state: string }) => element.state !== "closed" ) - } else { - reject(new Error(`Unknown error: ${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 === 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}`)) }) }) } 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}`)) + reject(new Error(`Error fetching issues by project: ${this.handleFetchIssuesError(error)}`)) }) }) } 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}`)) + reject(new Error(`Error fetching issues by sprint: ${this.handleFetchIssuesError(error)}`)) }) }) } @@ -528,80 +422,63 @@ 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}`)) + reject(new Error(`Error fetching issues by project: ${this.handleFetchIssuesError(error)}`)) }) }) } - 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}`)) - }) + 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 === 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, @@ -610,106 +487,59 @@ 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), - } - ) - .then(async (response) => { - if (response.status === 204) { - resolve() - } else if (response.status === 400) { - reject(new Error(`Invalid request: ${await response.json()}`)) - } else if (response.status === 401) { - reject( - new Error(`User not authenticated: ${await response.json()}`) - ) - } else if (response.status === 403) { - reject( - new Error( - `User does not have a valid licence or permissions to assign issues: ${await response.json()}` + this.getAgileRestApiClient('1.0') + .post( + `/sprint/${sprint}/issue`, + { + rankCustomFieldId: rankCustomField!.match(/_(\d+)/)![1], + issues: [issue], + ...(rankAfter ? { rankAfterIssue: rankAfter } : {}), + ...(rankBefore ? { rankBeforeIssue: rankBefore } : {}), + } + ) + .then(async () => { resolve() }) + .catch((error) => { + let specificError = error + if (error.response) { + 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 (response.status === 404) { - reject( - new Error( - `The sprint does not exist or the user does not have permission to view it: ${await response.json()}` + } 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: ${await response.json()}`)) + } } - }) - .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}: ${specificError}`)) }) }) } 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}" - ] - }`, - } - ) - .then(async (response) => { - if (response.status === 204) { - resolve() - } else if (response.status === 400) { - reject(new Error(`Invalid request: ${await response.json()}`)) - } else if (response.status === 401) { - reject( - new Error(`User not authenticated: ${await response.json()}`) - ) - } else if (response.status === 403) { - reject( - new Error( - `User does not have a valid licence or permissions to assign issues: ${await response.json()}` + this.getAgileRestApiClient('1.0') + .post( + '/backlog/issue', + { issues: [issue] } + ) + .then(async () => { resolve() }) + .catch((error) => { + let specificError = error + if (error.response) { + 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 (response.status === 404) { - reject( - new Error( - `The board does not exist or the user does not have permission to view it: ${await response.json()}` + } 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: ${await response.json()}`)) + } } - }) - .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: ${specificError}`)) }) }) } @@ -737,18 +567,8 @@ 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) => { if (response.status === 204) { resolve() @@ -756,67 +576,44 @@ export class JiraCloudProvider implements IProvider { // 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: ${await response.json()}`)) - } else if (response.status === 401) { - reject( - new Error(`User not authenticated: ${await response.json()}`) - ) - } else if (response.status === 403) { - reject( - new Error( - `User does not have a valid licence or permissions to rank issues: ${await response.json()}` - ) - ) - } else { - reject(new Error(`Unknown error: ${await response.json()}`)) } }) .catch((error) => { - reject( - new Error(`Error in moving this issue to the backlog: ${error}`) - ) + let specificError = error + if (error.response) { + 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}`)) }) }) } 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() - if (response.status === 200) { - const customField = this.customFields.get("Story point estimate") - 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}` + 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}` ) - ) - } else { - reject(new Error(`Unknown error: ${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}` - ) - ) - ) }) } @@ -840,16 +637,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 }, @@ -890,35 +681,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) { - resolve(JSON.stringify(createdIssue.key)) - this.setTransition(createdIssue.id, status) - } - if (data.status === 400) { - reject(new Error(createdIssue)) - } - 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") - ) } + ) + .then(async (response) => { + const createdIssue = response.data + 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 === 404) { + specificError = new Error("The user does not have the necessary permissions") + } + } + + reject(new Error(`Error creating issue: ${specificError}`)) }) }) } @@ -945,16 +729,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, @@ -1011,152 +789,85 @@ 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" - ) - ) - } - 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") - ) - } - if (data.status === 404) { - reject( - new Error( + ) + .then(async () => { resolve() }) + .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 the necessary permissions" ) - ) + } } - }) - .catch(async (error) => { - reject(new Error(`Error creating issue: ${error}`)) + + reject(new Error(`Error creating issue: ${specificError}`)) }) }) } 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} = transitionResponse 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() - 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 === 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) { - 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 === 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}` - ) - ) - ) }) } 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() - if (response.status === 200) { - const labels: Promise = labelData.values - - resolve(labels) - } else if (response.status === 401) { - reject(new Error(`User not authenticated: ${labelData}`)) - } else { - reject(new Error(`Unknown error: ${labelData}`)) - } + resolve(response.data.values) }) .catch((error) => reject(new Error(`Error in fetching the labels: ${error}`)) @@ -1168,27 +879,11 @@ 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) => { - 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()}`) - ) - } else { - reject(new Error(`Unknown error: ${await response.json()}`)) - } + const priorityData: JiraPriority = response.data + resolve(priorityData.values) }) .catch((error) => reject(new Error(`Error in fetching the labels: ${error}`)) @@ -1200,60 +895,38 @@ export class JiraCloudProvider implements IProvider { issueIdOrKey: string, commentText: string ): Promise { - const bodyData = `{ - "body": { - "content": [ - { - "content": [ - { - "text": "${commentText.replace(/\n/g, " ")}", - "type": "text" - } - ], - "type": "paragraph" - } - ], - "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() - } - if (data.status === 400) { - reject(new Error("Invalid api request")) - } - if (data.status === 401) { - reject(new Error("User not authenticated")) + this.getRestApiClient(3) + .post( + `/issue/${issueIdOrKey}/comment`, + { + body: { + content: [ + { + content: [ + { + text: commentText.replace(/\n/g, " "), + type: "text" + } + ], + type: "paragraph" + } + ], + type: "doc", + version: 1 + } } - if (data.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 === 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}`)) }) }) } @@ -1263,165 +936,86 @@ export class JiraCloudProvider implements IProvider { commentId: string, commentText: string ): Promise { - const bodyData = `{ - "body": { - "content": [ - { - "content": [ - { - "text": "${commentText.replace(/\n/g, " ")}", - "type": "text" - } - ], - "type": "paragraph" - } - ], - "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" - ) - ) - } - if (data.status === 401) { - reject(new Error("User not authenticated")) + this.getRestApiClient(3) + .put( + `/issue/${issueIdOrKey}/comment/${commentId}`, + { + body: { + content: [ + { + content: [ + { + text: commentText.replace(/\n/g, " "), + type: "text" + } + ], + type: "paragraph" + } + ], + type: "doc", + version: 1 + } } - if (data.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}`)) }) }) } 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}`, - }, - } - ) - .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" - ) - ) - } - 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" - ) - ) - } - if (data.status === 405) { - reject( - new Error("An anonymous call has been made to the operation") - ) + this.getRestApiClient(3) + .delete(`/issue/${issueIdOrKey}/comment/${commentId}`) + .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}`)) }) }) } 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}`, - }, - } - ) - .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" - ) - ) - } - 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") - ) - } - if (data.status === 404) { - 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") - ) + this.getRestApiClient(2) + .delete(`/issue/${issueIdOrKey}`) + .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}`)) }) }) } @@ -1433,16 +1027,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: { @@ -1455,27 +1043,11 @@ export class JiraCloudProvider implements IProvider { id: projectId, }, }, - }), - } - ) - .then(async (data) => { - if (data.status === 201) { - const createdSubtask: { id: string; key: string } = - await data.json() - 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 { - reject(new Error(`Unknown error: ${await data.json()}`)) } + ) + .then(async (response) => { + const createdSubtask: { id: string; key: string } = response.data + resolve(createdSubtask) }) .catch((error) => { reject(new Error(`Error creating subtask: ${error}`)) @@ -1486,9 +1058,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) 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 { @@ -1508,16 +1082,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 && { @@ -1527,34 +1095,20 @@ export class JiraCloudProvider implements IProvider { endDate: offsetEndDate, }), ...(goal && { goal }), - }), - } - ) - .then(async (data) => { - if (data.status === 201) { - resolve() } - if (data.status === 400) { - reject(new Error("Invalid request")) - } - 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") - ) - } - if (data.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 === 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}`)) }) }) } 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"