issue-handlers.ts•7.24 kB
import { JiraApiClient } from '../utils/api-client.js';
import { JiraFormatters } from '../utils/formatters.js';
import { UserHandlers } from './user-handlers.js';
export class IssueHandlers {
  private fieldMetadataCache: Map<string, string> | null = null;
  constructor(private apiClient: JiraApiClient, private userHandlers: UserHandlers) {}
  /**
   * Fetch and cache field metadata (field ID -> display name mapping)
   */
  private async getFieldMetadata(): Promise<Map<string, string>> {
    if (this.fieldMetadataCache) {
      return this.fieldMetadataCache;
    }
    try {
      const fields = await this.apiClient.get('/field');
      const metadata = new Map<string, string>();
      fields.forEach((field: any) => {
        if (field.id && field.name) {
          metadata.set(field.id, field.name);
        }
      });
      this.fieldMetadataCache = metadata;
      return metadata;
    } catch (error) {
      // If field metadata fetch fails, return empty map
      return new Map<string, string>();
    }
  }
  async handleGetIssue(args: any) {
    try {
      const { issueKey } = args;
      if (!issueKey) {
        throw new Error('issueKey is required');
      }
      const issue = await this.apiClient.get(`/issue/${issueKey}`);
      const fieldMetadata = await this.getFieldMetadata();
      return {
        content: [
          {
            type: 'text',
            text: JiraFormatters.formatIssue(issue, fieldMetadata),
          },
        ],
      };
    } catch (error: any) {
      return {
        content: [
          {
            type: 'text',
            text: JiraFormatters.formatError(error),
          },
        ],
        isError: true,
      };
    }
  }
  async handleCreateIssue(args: any) {
    try {
      const { projectKey, summary, issueType, description, priority, assignee, labels, customFields } = args;
      if (!projectKey || !summary || !issueType) {
        throw new Error('projectKey, summary, and issueType are required');
      }
      const issueData: any = {
        fields: {
          project: { key: projectKey },
          summary,
          issuetype: { name: issueType },
        },
      };
      // Handle description - convert to ADF format if it's plain text
      if (description) {
        if (typeof description === 'string') {
          // Convert plain text to Atlassian Document Format
          issueData.fields.description = {
            type: 'doc',
            version: 1,
            content: [
              {
                type: 'paragraph',
                content: [
                  {
                    type: 'text',
                    text: description,
                  },
                ],
              },
            ],
          };
        } else {
          // Already in ADF format
          issueData.fields.description = description;
        }
      }
      if (priority) {
        issueData.fields.priority = { name: priority };
      }
      if (assignee) {
        // Auto-resolve email to account ID if needed
        const accountId = await this.userHandlers.resolveUserToAccountId(assignee);
        issueData.fields.assignee = { id: accountId };
      }
      if (labels && Array.isArray(labels)) {
        issueData.fields.labels = labels;
      }
      // Merge custom fields
      if (customFields && typeof customFields === 'object') {
        Object.assign(issueData.fields, customFields);
      }
      const result = await this.apiClient.post('/issue', issueData);
      return {
        content: [
          {
            type: 'text',
            text: `✅ Issue created successfully!\n\n**Key**: ${result.key}\n**ID**: ${result.id}\n**URL**: ${this.apiClient.getBaseUrl()}/browse/${result.key}`,
          },
        ],
      };
    } catch (error: any) {
      return {
        content: [
          {
            type: 'text',
            text: JiraFormatters.formatError(error),
          },
        ],
        isError: true,
      };
    }
  }
  async handleUpdateIssue(args: any) {
    try {
      const { issueKey, summary, description, priority, assignee, labels, customFields } = args;
      if (!issueKey) {
        throw new Error('issueKey is required');
      }
      const updateData: any = { fields: {} };
      if (summary) updateData.fields.summary = summary;
      // Handle description - convert to ADF format if it's plain text
      if (description) {
        if (typeof description === 'string') {
          // Convert plain text to Atlassian Document Format
          updateData.fields.description = {
            type: 'doc',
            version: 1,
            content: [
              {
                type: 'paragraph',
                content: [
                  {
                    type: 'text',
                    text: description,
                  },
                ],
              },
            ],
          };
        } else {
          // Already in ADF format
          updateData.fields.description = description;
        }
      }
      if (priority) updateData.fields.priority = { name: priority };
      if (assignee) {
        // Auto-resolve email to account ID if needed
        const accountId = await this.userHandlers.resolveUserToAccountId(assignee);
        updateData.fields.assignee = { id: accountId };
      }
      if (labels) updateData.fields.labels = labels;
      // Merge custom fields
      if (customFields && typeof customFields === 'object') {
        Object.assign(updateData.fields, customFields);
      }
      if (Object.keys(updateData.fields).length === 0) {
        throw new Error('At least one field to update must be provided');
      }
      await this.apiClient.put(`/issue/${issueKey}`, updateData);
      return {
        content: [
          {
            type: 'text',
            text: `✅ Issue ${issueKey} updated successfully!`,
          },
        ],
      };
    } catch (error: any) {
      return {
        content: [
          {
            type: 'text',
            text: JiraFormatters.formatError(error),
          },
        ],
        isError: true,
      };
    }
  }
  async handleAssignIssue(args: any) {
    try {
      const { issueKey, assignee } = args;
      if (!issueKey || !assignee) {
        throw new Error('issueKey and assignee are required');
      }
      let assigneeData;
      let assigneeText;
      if (assignee === '-1') {
        assigneeData = { accountId: null };
        assigneeText = 'unassigned';
      } else {
        // Auto-resolve email to account ID if needed
        const accountId = await this.userHandlers.resolveUserToAccountId(assignee);
        assigneeData = { accountId: accountId };
        assigneeText = `assigned to ${assignee}`;
      }
      await this.apiClient.put(`/issue/${issueKey}/assignee`, assigneeData);
      return {
        content: [
          {
            type: 'text',
            text: `✅ Issue ${issueKey} ${assigneeText} successfully!`,
          },
        ],
      };
    } catch (error: any) {
      return {
        content: [
          {
            type: 'text',
            text: JiraFormatters.formatError(error),
          },
        ],
        isError: true,
      };
    }
  }
}