@@ -6,25 +6,40 @@ export const dynamic = 'force-dynamic'
66
77const 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+
929export 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 (
0 commit comments