Skip to content

Commit 07ba174

Browse files
authored
Fix(jira): reading multiple issues and write
fixed the read and write tools in jira
1 parent ced6412 commit 07ba174

File tree

7 files changed

+263
-222
lines changed

7 files changed

+263
-222
lines changed

apps/docs/content/docs/tools/jira.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ Retrieve detailed information about a specific Jira issue
5858
| Parameter | Type | Required | Description |
5959
| --------- | ---- | -------- | ----------- |
6060
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
61-
| `projectId` | string | No | Jira project ID to retrieve issues from. If not provided, all issues will be retrieved. |
61+
| `projectId` | string | No | Jira project ID \(optional; not required to retrieve a single issue\). |
6262
| `issueKey` | string | Yes | Jira issue key to retrieve \(e.g., PROJ-123\) |
6363
| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. |
6464

apps/sim/app/api/tools/jira/issues/route.ts

Lines changed: 95 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,40 @@ export const dynamic = 'force-dynamic'
66

77
const logger = createLogger('JiraIssuesAPI')
88

9+
// Helper functions
10+
const createErrorResponse = async (response: Response, defaultMessage: string) => {
11+
try {
12+
const errorData = await response.json()
13+
return errorData.message || errorData.errorMessages?.[0] || defaultMessage
14+
} catch {
15+
return defaultMessage
16+
}
17+
}
18+
19+
const validateRequiredParams = (domain: string | null, accessToken: string | null) => {
20+
if (!domain) {
21+
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
22+
}
23+
if (!accessToken) {
24+
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
25+
}
26+
return null
27+
}
28+
929
export async function POST(request: Request) {
1030
try {
1131
const { domain, accessToken, issueKeys = [], cloudId: providedCloudId } = await request.json()
1232

13-
if (!domain) {
14-
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
15-
}
16-
17-
if (!accessToken) {
18-
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
19-
}
33+
const validationError = validateRequiredParams(domain || null, accessToken || null)
34+
if (validationError) return validationError
2035

2136
if (issueKeys.length === 0) {
2237
logger.info('No issue keys provided, returning empty result')
2338
return NextResponse.json({ issues: [] })
2439
}
2540

2641
// Use provided cloudId or fetch it if not provided
27-
const cloudId = providedCloudId || (await getJiraCloudId(domain, accessToken))
42+
const cloudId = providedCloudId || (await getJiraCloudId(domain!, accessToken!))
2843

2944
// Build the URL using cloudId for Jira API
3045
const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/bulkfetch`
@@ -53,47 +68,24 @@ export async function POST(request: Request) {
5368

5469
if (!response.ok) {
5570
logger.error(`Jira API error: ${response.status} ${response.statusText}`)
56-
let errorMessage
57-
58-
try {
59-
const errorData = await response.json()
60-
logger.error('Error details:', JSON.stringify(errorData, null, 2))
61-
errorMessage = errorData.message || `Failed to fetch Jira issues (${response.status})`
62-
} catch (e) {
63-
logger.error('Could not parse error response as JSON:', e)
64-
65-
try {
66-
const _text = await response.text()
67-
errorMessage = `Failed to fetch Jira issues: ${response.status} ${response.statusText}`
68-
} catch (_textError) {
69-
errorMessage = `Failed to fetch Jira issues: ${response.status} ${response.statusText}`
70-
}
71-
}
72-
71+
const errorMessage = await createErrorResponse(
72+
response,
73+
`Failed to fetch Jira issues (${response.status})`
74+
)
7375
return NextResponse.json({ error: errorMessage }, { status: response.status })
7476
}
7577

7678
const data = await response.json()
77-
78-
if (data.issues && data.issues.length > 0) {
79-
data.issues.slice(0, 3).forEach((issue: any) => {
80-
logger.info(`- ${issue.key}: ${issue.fields.summary}`)
81-
})
82-
}
83-
84-
return NextResponse.json({
85-
issues: data.issues
86-
? data.issues.map((issue: any) => ({
87-
id: issue.key,
88-
name: issue.fields.summary,
89-
mimeType: 'jira/issue',
90-
url: `https://${domain}/browse/${issue.key}`,
91-
modifiedTime: issue.fields.updated,
92-
webViewLink: `https://${domain}/browse/${issue.key}`,
93-
}))
94-
: [],
95-
cloudId, // Return the cloudId so it can be cached
96-
})
79+
const issues = (data.issues || []).map((issue: any) => ({
80+
id: issue.key,
81+
name: issue.fields.summary,
82+
mimeType: 'jira/issue',
83+
url: `https://${domain}/browse/${issue.key}`,
84+
modifiedTime: issue.fields.updated,
85+
webViewLink: `https://${domain}/browse/${issue.key}`,
86+
}))
87+
88+
return NextResponse.json({ issues, cloudId })
9789
} catch (error) {
9890
logger.error('Error fetching Jira issues:', error)
9991
return NextResponse.json(
@@ -111,83 +103,79 @@ export async function GET(request: Request) {
111103
const providedCloudId = url.searchParams.get('cloudId')
112104
const query = url.searchParams.get('query') || ''
113105
const projectId = url.searchParams.get('projectId') || ''
106+
const manualProjectId = url.searchParams.get('manualProjectId') || ''
107+
const all = url.searchParams.get('all')?.toLowerCase() === 'true'
108+
const limitParam = Number.parseInt(url.searchParams.get('limit') || '', 10)
109+
const limit = Number.isFinite(limitParam) && limitParam > 0 ? limitParam : 0
114110

115-
if (!domain) {
116-
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
117-
}
118-
119-
if (!accessToken) {
120-
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
121-
}
122-
123-
// Use provided cloudId or fetch it if not provided
124-
const cloudId = providedCloudId || (await getJiraCloudId(domain, accessToken))
125-
logger.info('Using cloud ID:', cloudId)
126-
127-
// Build query parameters
128-
const params = new URLSearchParams()
129-
130-
// Only add query if it exists
131-
if (query) {
132-
params.append('query', query)
133-
}
111+
const validationError = validateRequiredParams(domain || null, accessToken || null)
112+
if (validationError) return validationError
134113

114+
const cloudId = providedCloudId || (await getJiraCloudId(domain!, accessToken!))
135115
let data: any
136116

137117
if (query) {
138-
const apiUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/picker?${params.toString()}`
139-
logger.info(`Fetching Jira issue suggestions from: ${apiUrl}`)
118+
const params = new URLSearchParams({ query })
119+
const apiUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/picker?${params}`
140120
const response = await fetch(apiUrl, {
141-
method: 'GET',
142121
headers: {
143122
Authorization: `Bearer ${accessToken}`,
144123
Accept: 'application/json',
145124
},
146125
})
147-
logger.info('Response status:', response.status, response.statusText)
126+
148127
if (!response.ok) {
149-
logger.error(`Jira API error: ${response.status} ${response.statusText}`)
150-
let errorMessage
151-
try {
152-
const errorData = await response.json()
153-
logger.error('Error details:', errorData)
154-
errorMessage =
155-
errorData.message || `Failed to fetch issue suggestions (${response.status})`
156-
} catch (_e) {
157-
errorMessage = `Failed to fetch issue suggestions: ${response.status} ${response.statusText}`
158-
}
128+
const errorMessage = await createErrorResponse(
129+
response,
130+
`Failed to fetch issue suggestions (${response.status})`
131+
)
159132
return NextResponse.json({ error: errorMessage }, { status: response.status })
160133
}
161134
data = await response.json()
162-
} else if (projectId) {
163-
// When no query, list latest issues for the selected project using Search API
164-
const searchParams = new URLSearchParams()
165-
searchParams.append('jql', `project=${projectId} ORDER BY updated DESC`)
166-
searchParams.append('maxResults', '25')
167-
searchParams.append('fields', 'summary,key')
168-
const searchUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/search?${searchParams.toString()}`
169-
logger.info(`Fetching Jira issues via search from: ${searchUrl}`)
170-
const response = await fetch(searchUrl, {
171-
method: 'GET',
172-
headers: {
173-
Authorization: `Bearer ${accessToken}`,
174-
Accept: 'application/json',
175-
},
176-
})
177-
if (!response.ok) {
178-
let errorMessage
179-
try {
180-
const errorData = await response.json()
181-
logger.error('Jira Search API error details:', errorData)
182-
errorMessage =
183-
errorData.errorMessages?.[0] || `Failed to fetch issues (${response.status})`
184-
} catch (_e) {
185-
errorMessage = `Failed to fetch issues: ${response.status} ${response.statusText}`
186-
}
187-
return NextResponse.json({ error: errorMessage }, { status: response.status })
135+
} else if (projectId || manualProjectId) {
136+
const SAFETY_CAP = 1000
137+
const PAGE_SIZE = 100
138+
const target = Math.min(all ? limit || SAFETY_CAP : 25, SAFETY_CAP)
139+
const projectKey = (projectId || manualProjectId).trim()
140+
141+
const buildSearchUrl = (startAt: number) => {
142+
const params = new URLSearchParams({
143+
jql: `project=${projectKey} ORDER BY updated DESC`,
144+
maxResults: String(Math.min(PAGE_SIZE, target)),
145+
startAt: String(startAt),
146+
fields: 'summary,key,updated',
147+
})
148+
return `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/search?${params}`
188149
}
189-
const searchData = await response.json()
190-
const issues = (searchData.issues || []).map((it: any) => ({
150+
151+
let startAt = 0
152+
let collected: any[] = []
153+
let total = 0
154+
155+
do {
156+
const response = await fetch(buildSearchUrl(startAt), {
157+
headers: {
158+
Authorization: `Bearer ${accessToken}`,
159+
Accept: 'application/json',
160+
},
161+
})
162+
163+
if (!response.ok) {
164+
const errorMessage = await createErrorResponse(
165+
response,
166+
`Failed to fetch issues (${response.status})`
167+
)
168+
return NextResponse.json({ error: errorMessage }, { status: response.status })
169+
}
170+
171+
const page = await response.json()
172+
const issues = page.issues || []
173+
total = page.total || issues.length
174+
collected = collected.concat(issues)
175+
startAt += PAGE_SIZE
176+
} while (all && collected.length < Math.min(total, target))
177+
178+
const issues = collected.slice(0, target).map((it: any) => ({
191179
key: it.key,
192180
summary: it.fields?.summary || it.key,
193181
}))
@@ -196,10 +184,7 @@ export async function GET(request: Request) {
196184
data = { sections: [], cloudId }
197185
}
198186

199-
return NextResponse.json({
200-
...data,
201-
cloudId, // Return the cloudId so it can be cached
202-
})
187+
return NextResponse.json({ ...data, cloudId })
203188
} catch (error) {
204189
logger.error('Error fetching Jira issue suggestions:', error)
205190
return NextResponse.json(

apps/sim/app/api/tools/jira/write/route.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,7 @@ export async function POST(request: Request) {
4242
return NextResponse.json({ error: 'Summary is required' }, { status: 400 })
4343
}
4444

45-
if (!issueType) {
46-
logger.error('Missing issue type in request')
47-
return NextResponse.json({ error: 'Issue type is required' }, { status: 400 })
48-
}
45+
const normalizedIssueType = issueType || 'Task'
4946

5047
// Use provided cloudId or fetch it if not provided
5148
const cloudId = providedCloudId || (await getJiraCloudId(domain, accessToken))
@@ -62,7 +59,7 @@ export async function POST(request: Request) {
6259
id: projectId,
6360
},
6461
issuetype: {
65-
name: issueType,
62+
name: normalizedIssueType,
6663
},
6764
summary: summary,
6865
}

apps/sim/blocks/blocks/jira.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
2020
layout: 'full',
2121
options: [
2222
{ label: 'Read Issue', id: 'read' },
23-
{ label: 'Read Issues', id: 'read-bulk' },
2423
{ label: 'Update Issue', id: 'update' },
2524
{ label: 'Write Issue', id: 'write' },
2625
],
@@ -99,7 +98,7 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
9998
layout: 'full',
10099
canonicalParamId: 'issueKey',
101100
placeholder: 'Enter Jira issue key',
102-
dependsOn: ['credential', 'domain', 'projectId'],
101+
dependsOn: ['credential', 'domain', 'projectId', 'manualProjectId'],
103102
condition: { field: 'operation', value: ['read', 'update'] },
104103
mode: 'advanced',
105104
},
@@ -127,8 +126,15 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
127126
access: ['jira_retrieve', 'jira_update', 'jira_write', 'jira_bulk_read'],
128127
config: {
129128
tool: (params) => {
129+
const effectiveProjectId = (params.projectId || params.manualProjectId || '').trim()
130+
const effectiveIssueKey = (params.issueKey || params.manualIssueKey || '').trim()
131+
130132
switch (params.operation) {
131133
case 'read':
134+
// If a project is selected but no issue is chosen, route to bulk read
135+
if (effectiveProjectId && !effectiveIssueKey) {
136+
return 'jira_bulk_read'
137+
}
132138
return 'jira_retrieve'
133139
case 'update':
134140
return 'jira_update'
@@ -194,25 +200,34 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
194200
}
195201
}
196202
case 'read': {
197-
if (!effectiveIssueKey) {
203+
// Check for project ID from either source
204+
const projectForRead = (params.projectId || params.manualProjectId || '').trim()
205+
const issueForRead = (params.issueKey || params.manualIssueKey || '').trim()
206+
207+
if (!issueForRead) {
198208
throw new Error(
199-
'Issue Key is required. Please select an issue or enter an issue key manually.'
209+
'Select a project to read issues, or provide an issue key to read a single issue.'
200210
)
201211
}
202212
return {
203213
...baseParams,
204-
issueKey: effectiveIssueKey,
214+
issueKey: issueForRead,
215+
// Include projectId if available for context
216+
...(projectForRead && { projectId: projectForRead }),
205217
}
206218
}
207219
case 'read-bulk': {
208-
if (!effectiveProjectId) {
220+
// Check both projectId and manualProjectId directly from params
221+
const finalProjectId = params.projectId || params.manualProjectId || ''
222+
223+
if (!finalProjectId) {
209224
throw new Error(
210225
'Project ID is required. Please select a project or enter a project ID manually.'
211226
)
212227
}
213228
return {
214229
...baseParams,
215-
projectId: effectiveProjectId,
230+
projectId: finalProjectId.trim(),
216231
}
217232
}
218233
default:

0 commit comments

Comments
 (0)