From 03f6dc55565dda5bf67108f8a478959ff55700d5 Mon Sep 17 00:00:00 2001 From: Fedor Rychkov Date: Sun, 12 Jan 2025 20:49:08 +0300 Subject: [PATCH] fix(*): add paginated sprints request to valid get last 4, and refetch worklogs if its necessary --- package.json | 2 +- src/guards/chat.telegraf.guard.ts | 6 +- src/helpers/index.ts | 1 + src/helpers/jsonSafe.ts | 19 +++++ src/modules/jira/jira.service.ts | 24 ++++++- src/scenes/main/main.jira.scene.service.ts | 84 ++++++++++++++++++++-- 6 files changed, 123 insertions(+), 13 deletions(-) create mode 100644 src/helpers/jsonSafe.ts diff --git a/package.json b/package.json index a2c4f29..6da55e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nestjs-tg-jira-bot", - "version": "0.1.1", + "version": "0.1.2", "description": "", "author": "", "private": true, diff --git a/src/guards/chat.telegraf.guard.ts b/src/guards/chat.telegraf.guard.ts index 9df65da..8a7fdbe 100644 --- a/src/guards/chat.telegraf.guard.ts +++ b/src/guards/chat.telegraf.guard.ts @@ -102,7 +102,7 @@ ${botWelcomeCommandsText}`, } } - request.chatContext = { + const chatContext: ChatTelegrafContextType = { type: chat?.type, isChatWithTopics: chat?.is_forum || replyMessage?.is_topic_message, threadMessageId: replyMessage?.message_thread_id, @@ -111,7 +111,9 @@ ${botWelcomeCommandsText}`, from: from, chat: update?.message?.chat, isEditableAvailable, - } as ChatTelegrafContextType + } + + request.chatContext = chatContext return true } diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 550b516..67ffc01 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -1,3 +1,4 @@ export * from './getImageData' export * from './id' +export * from './jsonSafe' export * from './time' diff --git a/src/helpers/jsonSafe.ts b/src/helpers/jsonSafe.ts new file mode 100644 index 0000000..096c5af --- /dev/null +++ b/src/helpers/jsonSafe.ts @@ -0,0 +1,19 @@ +export function jsonParse(value?: string | null): T | null | undefined { + if (!value) { + return undefined + } + + try { + return JSON.parse(value) + } catch { + return undefined + } +} + +export function jsonStringify(value: T): string { + if (!value) { + return null + } + + return JSON.stringify(value) +} diff --git a/src/modules/jira/jira.service.ts b/src/modules/jira/jira.service.ts index bac2b85..30e255f 100644 --- a/src/modules/jira/jira.service.ts +++ b/src/modules/jira/jira.service.ts @@ -1,5 +1,6 @@ import { Inject, Injectable } from '@nestjs/common' import { ConfigService } from '@nestjs/config' +import { Axios } from 'axios' import { exec } from 'child_process' import { AgileClient } from 'jira.js' import NodeJsVersion3Client, { Version3Client } from 'jira-rest-sdk' @@ -7,6 +8,7 @@ import NodeJsVersion3Client, { Version3Client } from 'jira-rest-sdk' @Injectable() export class JiraService { private jira: NodeJsVersion3Client + private api: Axios public baseURL: string private apiToken: string private email: string @@ -20,6 +22,14 @@ export class JiraService { this.apiToken = this.configService.get('API_KEY') this.baseURL = this.configService.get('JIRA_HOST') + this.api = new Axios({ + baseURL: this.baseURL, + auth: { + username: this.email, + password: this.apiToken, + }, + }) + this.jira = new Version3Client({ baseURL: this.baseURL, authentication: { @@ -41,7 +51,7 @@ export class JiraService { }) } - public async createTask(payload: { summary: string; description: string; key: string; issueType: 'Task' | 'Bug' }) { + public async createTask(payload: { summary: string; description: string; key: string; issueType: string }) { const { summary, description, key, issueType } = payload const createdIssue = await this.jira.createIssue({ @@ -80,23 +90,31 @@ export class JiraService { return boards } - public async getBoardSprints(boardId: number) { + public async getBoardSprints(boardId: number, startAt = 0) { const boards = await this.agileClient.board.getAllSprints({ boardId, + startAt: startAt > 0 ? startAt : 0, }) return boards } - public async getIssuesBySprint(sprintId: number) { + public async getIssuesBySprint(sprintId: number, startAt = 0) { const issues = await this.agileClient.sprint.getIssuesForSprint({ sprintId, maxResults: 500, + startAt: startAt > 0 ? startAt : 0, }) return issues } + public async getIssueWorklogs(issueId: string) { + const response = await this.api.get(`/rest/api/2/issue/${issueId}/worklog`) + + return response.data + } + public async getSprint(sprintId: number) { const issues = await this.agileClient.sprint.getSprint({ sprintId, diff --git a/src/scenes/main/main.jira.scene.service.ts b/src/scenes/main/main.jira.scene.service.ts index bb8fd56..1fb614e 100644 --- a/src/scenes/main/main.jira.scene.service.ts +++ b/src/scenes/main/main.jira.scene.service.ts @@ -2,6 +2,7 @@ import { Injectable, Logger } from '@nestjs/common' import * as dayjs from 'dayjs' import * as isBetween from 'dayjs/plugin/isBetween' import * as fs from 'fs' +import { Fields } from 'jira.js/out/version2/models' import { Action, Command, Ctx, Hears, On, Update } from 'nestjs-telegraf' import { MAIN_CALLBACK_DATA } from 'src/constants' import { getJiraProjectKeyboards, getJiraProjectSprintsKeyboards } from 'src/constants/keyboard' @@ -13,7 +14,7 @@ import { UserTelegrafGuard, UseSafeGuards, } from 'src/guards' -import { getImageFile, time } from 'src/helpers' +import { getImageFile, jsonParse, time } from 'src/helpers' import { CustomConfigService } from 'src/modules' import { JiraService } from 'src/modules/jira' import { ChatTelegrafContextType, JiraConfigType, TgInitUser } from 'src/types' @@ -132,7 +133,17 @@ export class MainJiraSceneService { const boards = await this.jiraService.getProjectBoards(key) const boardIds = boards.values.map((board) => board.id) - const sprints = await Promise.all(boardIds.map((boardId) => this.jiraService.getBoardSprints(boardId))) + const sprints = await Promise.all( + boardIds.map(async (boardId) => { + const defaultResponse = await this.jiraService.getBoardSprints(boardId) + + if (defaultResponse.isLast) { + return defaultResponse + } + + return this.jiraService.getBoardSprints(boardId, defaultResponse.total - 4) + }), + ) const filteredSprints = sprints.flatMap((sprint) => sprint.values.filter((sprint, index, values) => { @@ -167,7 +178,23 @@ export class MainJiraSceneService { const sprint = await this.jiraService.getSprint(key) const definedIssues = await this.jiraService.getIssuesBySprint(key) - this.logger.log(sprint) + const currentIssueLenght = definedIssues.issues.length + + if (definedIssues.total > currentIssueLenght) { + /** + * Генерим длину массива startAt свойств + */ + const size = Math.floor(definedIssues.total / currentIssueLenght) + + const startAtArray = Array.from({ length: size }, (_, index) => definedIssues.total * (index + 1)) + + for await (const startAt of startAtArray) { + const response = await this.jiraService.getIssuesBySprint(key, startAt) + definedIssues.issues.push(...response.issues) + } + + return + } const srtartDate = sprint.startDate const endDate = sprint.completeDate || sprint.endDate @@ -190,8 +217,32 @@ export class MainJiraSceneService { return } - const issues = parsedIssues - .filter((issue) => { + const preparedIssuesWithFullWorklogs = await Promise.all( + parsedIssues.map(async (issue) => { + if (issue.fields.worklog.total > issue.fields.worklog.worklogs.length) { + const response = await this.jiraService.getIssueWorklogs(issue.id) + const parsed = jsonParse(response) + + if (parsed && typeof parsed === 'object' && 'worklogs' in parsed) { + return { + ...issue, + fields: { + ...issue.fields, + worklog: { + ...issue.fields.worklog, + ...parsed, + }, + }, + } + } + } + + return issue + }), + ) + + const issues = preparedIssuesWithFullWorklogs + .filter(async (issue) => { if (jiraConfig.isSuperAdmin) { return true } @@ -201,6 +252,7 @@ export class MainJiraSceneService { } const worklogs = issue.fields.worklog.worklogs + const parsedWorkLogs = worklogs.filter((log) => { if (jiraConfig.isSuperAdmin) { return true @@ -451,7 +503,25 @@ export class MainJiraSceneService { @UserContext() userContext: TgInitUser, @ChatTelegrafContext() chatContext: ChatTelegrafContextType, ) { - const projectKey = chatContext?.topic?.name?.split('=')?.[1] + const paramsString = chatContext?.topic?.name?.split(':')[1] + const params = paramsString?.split('&') + + const paramsObject = params?.reduce((acc: Record, param) => { + const [key, value] = param.split('=') + acc[key] = value + + return acc + }, {}) + + const { key: projectKey, type: issueType = 'Bug' } = paramsObject + + if (!projectKey) { + return + } + + if (!['Bug', 'Task', 'Story'].includes(issueType)) { + return + } // В качестве тайтла вытаскиваем первый абзац и первые 100 символов const summary = ctx?.text?.split('\n')?.[0]?.slice(0, 100) @@ -461,7 +531,7 @@ export class MainJiraSceneService { const { createdLink } = await this.jiraService.createTask({ key: projectKey, summary: `[JiraBot] ${summary}`, - issueType: 'Bug', + issueType, description: `${description}\nCreated by bot from: https://t.me/c/${chatContext?.chat?.id?.toString()?.replace('-100', '')}/${chatContext?.threadMessageId}/${ctx?.message?.message_id}\nCaller user: @${userContext.username} (${userContext.firstName} ${userContext.lastName})`, })