# Jira Client

Create issues, search projects, and manage work in Jira.

## Methods

| Method                                   | Description                                      |
| ---------------------------------------- | ------------------------------------------------ |
| `apiRequest(options, schema, metadata?)` | Make any Jira API request with schema validation |

## Usage

### Create an Issue

```typescript
import { api, z, jira } from "@superblocksteam/sdk-api";

// Integration ID from the integrations panel
const PROD_JIRA = "a1b2c3d4-5678-90ab-cdef-jira00000001";

const IssueSchema = z.object({
  id: z.string(),
  key: z.string(),
  self: z.string(),
  fields: z
    .object({
      summary: z.string(),
      status: z.object({ name: z.string() }),
      issuetype: z.object({ name: z.string() }),
    })
    .passthrough(),
});

const CreateIssueResponseSchema = z.object({
  id: z.string(),
  key: z.string(),
  self: z.string(),
});

export default api({
  integrations: {
    jira: jira(PROD_JIRA),
  },
  name: "JiraExample",
  input: z.object({
    projectKey: z.string(),
    summary: z.string(),
    description: z.string(),
  }),
  output: z.object({
    issueKey: z.string(),
  }),
  async run(ctx, { projectKey, summary, description }) {
    const result = await ctx.integrations.jira.apiRequest(
      {
        method: "POST",
        path: "/rest/api/3/issue",
        body: {
          fields: {
            project: { key: projectKey },
            summary: summary,
            description: {
              type: "doc",
              version: 1,
              content: [
                {
                  type: "paragraph",
                  content: [{ type: "text", text: description }],
                },
              ],
            },
            issuetype: { name: "Task" },
          },
        },
      },
      { response: CreateIssueResponseSchema },
    );

    return { issueKey: result.key };
  },
});
```

### Search Issues (JQL)

```typescript
const SearchResponseSchema = z.object({
  issues: z.array(IssueSchema),
  total: z.number(),
  maxResults: z.number(),
  startAt: z.number(),
});

const result = await ctx.integrations.jira.apiRequest(
  {
    method: "POST",
    path: "/rest/api/3/search",
    body: {
      jql: 'project = "PROJ" AND status = "In Progress" ORDER BY created DESC',
      fields: ["summary", "status", "assignee", "priority"],
      maxResults: 50,
    },
  },
  { response: SearchResponseSchema },
);

result.issues.forEach((issue) => {
  console.log(`${issue.key}: ${issue.fields.summary}`);
});
```

### Get an Issue

```typescript
const result = await ctx.integrations.jira.apiRequest(
  {
    method: "GET",
    path: `/rest/api/3/issue/${issueKey}`,
    params: {
      fields: "summary,status,assignee,description,priority,labels",
    },
  },
  { response: IssueSchema },
);
```

### Update an Issue

```typescript
await ctx.integrations.jira.apiRequest(
  {
    method: "PUT",
    path: `/rest/api/3/issue/${issueKey}`,
    body: {
      fields: {
        summary: "Updated summary",
        labels: ["urgent", "bug"],
        priority: { name: "High" },
      },
    },
  },
  { response: z.object({}).optional() }, // Returns empty on success
);
```

### Transition an Issue

```typescript
// First, get available transitions
const TransitionsResponseSchema = z.object({
  transitions: z.array(
    z.object({
      id: z.string(),
      name: z.string(),
      to: z.object({ name: z.string() }),
    }),
  ),
});

const transitions = await ctx.integrations.jira.apiRequest(
  {
    method: "GET",
    path: `/rest/api/3/issue/${issueKey}/transitions`,
  },
  { response: TransitionsResponseSchema },
);

// Find the transition you want
const doneTransition = transitions.transitions.find((t) => t.name === "Done");

// Execute the transition
if (doneTransition) {
  await ctx.integrations.jira.apiRequest(
    {
      method: "POST",
      path: `/rest/api/3/issue/${issueKey}/transitions`,
      body: {
        transition: { id: doneTransition.id },
      },
    },
    { response: z.object({}).optional() },
  );
}
```

### Add a Comment

```typescript
const CommentResponseSchema = z.object({
  id: z.string(),
  body: z.unknown(),
  created: z.string(),
  author: z.object({
    displayName: z.string(),
  }),
});

const result = await ctx.integrations.jira.apiRequest(
  {
    method: "POST",
    path: `/rest/api/3/issue/${issueKey}/comment`,
    body: {
      body: {
        type: "doc",
        version: 1,
        content: [
          {
            type: "paragraph",
            content: [{ type: "text", text: "This is a comment." }],
          },
        ],
      },
    },
  },
  { response: CommentResponseSchema },
);
```

### Assign an Issue

```typescript
await ctx.integrations.jira.apiRequest(
  {
    method: "PUT",
    path: `/rest/api/3/issue/${issueKey}/assignee`,
    body: {
      accountId: "5b10a2844c20165700ede21g", // User account ID
    },
  },
  { response: z.object({}).optional() },
);
```

### List Projects

```typescript
const ListProjectsResponseSchema = z.object({
  values: z.array(
    z.object({
      id: z.string(),
      key: z.string(),
      name: z.string(),
      projectTypeKey: z.string(),
    }),
  ),
  total: z.number(),
});

const result = await ctx.integrations.jira.apiRequest(
  {
    method: "GET",
    path: "/rest/api/3/project/search",
    params: {
      maxResults: 50,
      orderBy: "name",
    },
  },
  { response: ListProjectsResponseSchema },
);
```

### Add Attachment

```typescript
// Note: Attachments require multipart/form-data
// The body should be the file content
const AttachmentResponseSchema = z.array(
  z.object({
    id: z.string(),
    filename: z.string(),
    size: z.number(),
  }),
);

const result = await ctx.integrations.jira.apiRequest(
  {
    method: "POST",
    path: `/rest/api/3/issue/${issueKey}/attachments`,
    headers: {
      "X-Atlassian-Token": "no-check",
    },
    body: fileContent, // Multipart form data
  },
  { response: AttachmentResponseSchema },
);
```

## Trace Metadata

All methods accept an optional `metadata` parameter as the last argument for diagnostics labeling. See the [root SDK README](../../../README.md#trace-metadata) for details.

## Common Pitfalls

### No Specialized Methods

```typescript
// WRONG - These methods do not exist
await jira.createIssue({ ... });
await jira.searchIssues({ ... });

// CORRECT - Use apiRequest
await ctx.integrations.jira.apiRequest(
  { method: "POST", path: "/rest/api/3/issue", body: { fields: { ... } } },
  { response: CreateIssueResponseSchema }
);
```

### API Version in Path

Jira uses `/rest/api/3/` for the latest API:

```typescript
// WRONG - Missing API version
const path = "/issue";

// CORRECT - Include full path
const path = "/rest/api/3/issue";
```

### Atlassian Document Format (ADF)

Jira uses ADF for rich text fields:

```typescript
// WRONG - Plain text (won't work for description)
const body = {
  fields: {
    description: "Plain text description",
  },
};

// CORRECT - ADF format
const body = {
  fields: {
    description: {
      type: "doc",
      version: 1,
      content: [
        {
          type: "paragraph",
          content: [{ type: "text", text: "Description text" }],
        },
      ],
    },
  },
};

// ADF with formatting
const adf = {
  type: "doc",
  version: 1,
  content: [
    {
      type: "paragraph",
      content: [
        { type: "text", text: "Bold text", marks: [{ type: "strong" }] },
        { type: "text", text: " and normal text" },
      ],
    },
    {
      type: "bulletList",
      content: [
        {
          type: "listItem",
          content: [
            { type: "paragraph", content: [{ type: "text", text: "Item 1" }] },
          ],
        },
      ],
    },
  ],
};
```

### JQL Syntax

```typescript
// Basic query
const jql = 'project = "PROJ"';

// Multiple conditions
const jql = 'project = "PROJ" AND status = "In Progress"';

// Date queries
const jql = "created >= -7d"; // Last 7 days
const jql = "due <= 2024-12-31";

// Text search
const jql = 'text ~ "search term"';

// Assignee
const jql = "assignee = currentUser()";
const jql = 'assignee = "john@example.com"';

// Labels
const jql = 'labels = "urgent"';

// Order by
const jql = 'project = "PROJ" ORDER BY priority DESC, created ASC';
```

### Account ID vs Username

Jira Cloud uses Account IDs, not usernames:

```typescript
// WRONG - Username
const body = { assignee: { name: "john.doe" } };

// CORRECT - Account ID
const body = { assignee: { accountId: "5b10a2844c20165700ede21g" } };

// Or use accountId directly
const body = { accountId: "5b10a2844c20165700ede21g" };
```

### Custom Fields

Custom fields use IDs like `customfield_10001`:

```typescript
const body = {
  fields: {
    summary: "Issue summary",
    customfield_10001: "Custom value", // Text field
    customfield_10002: { value: "Option A" }, // Select field
    customfield_10003: [{ value: "A" }, { value: "B" }], // Multi-select
  },
};
```

### Pagination

Large searches require pagination:

```typescript
async function getAllIssues(jira: JiraClient, jql: string) {
  const allIssues: Issue[] = [];
  let startAt = 0;
  const maxResults = 100;

  while (true) {
    const result = await ctx.integrations.jira.apiRequest(
      {
        method: "POST",
        path: "/rest/api/3/search",
        body: { jql, startAt, maxResults },
      },
      { response: SearchResponseSchema },
    );

    allIssues.push(...result.issues);

    if (result.issues.length < maxResults) break;
    startAt += maxResults;
  }

  return allIssues;
}
```

## Error Handling

```typescript
import { RestApiValidationError } from "@superblocksteam/sdk-api";

try {
  const result = await ctx.integrations.jira.apiRequest(
    { method: "POST", path: "/rest/api/3/issue", body: { ... } },
    { response: CreateIssueResponseSchema }
  );
} catch (error) {
  if (error instanceof RestApiValidationError) {
    console.error("Validation failed:", error.details.zodError);
  }
}
```

## API Reference

- [Jira REST API Documentation](https://developer.atlassian.com/cloud/jira/platform/rest/v3/intro/)
- [Create Issue](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-post)
- [Search Issues (JQL)](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-search/#api-rest-api-3-search-post)
- [JQL Reference](https://support.atlassian.com/jira-software-cloud/docs/advanced-search-reference-jql-fields/)
- [ADF Reference](https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/)
