@@ -82,45 +82,47 @@ func CollectEpics(taskCtx plugin.SubTaskContext) errors.Error {
8282 jql = buildJQL (* apiCollector .GetSince (), loc )
8383 }
8484
85- // Choose API endpoint based on JIRA version
86- var urlTemplate string
87- var shouldAddFieldsParam bool
88-
89- // Use api/2 for JIRA Server <= v8, api/3 for newer versions
90- if data .JiraServerInfo .DeploymentType == models .DeploymentServer &&
91- len (data .JiraServerInfo .VersionNumbers ) == 3 &&
92- data .JiraServerInfo .VersionNumbers [0 ] <= 8 {
93- // JIRA Server <= v8
94- urlTemplate = "api/2/search"
95- shouldAddFieldsParam = false
96- logger .Info ("Using api/2/search for JIRA Server version <= 8" )
85+ // Choose API endpoint based on JIRA deployment type
86+ if data .JiraServerInfo .DeploymentType == models .DeploymentServer {
87+ logger .Info ("Using api/2/search for JIRA Server" )
88+ err = setupApiV2Collector (apiCollector , data , epicIterator , jql )
9789 } else {
98- // JIRA Cloud and Server > v8 (api/2 deprecated in Cloud)
99- urlTemplate = "api/3/search/jql"
100- shouldAddFieldsParam = true
101- logger .Info ("Using api/3/search/jql for JIRA Cloud or JIRA Server version > 8" )
90+ logger .Info ("Using api/3/search/jql for JIRA Cloud" )
91+ err = setupApiV3Collector (apiCollector , data , epicIterator , jql )
10292 }
93+ if err != nil {
94+ return err
95+ }
96+ return apiCollector .Execute ()
97+ }
10398
104- err = apiCollector .InitCollector (api.ApiCollectorArgs {
99+ // JIRA Server API v2 collector
100+ func setupApiV2Collector (apiCollector * api.StatefulApiCollector , data * JiraTaskData , epicIterator api.Iterator , jql string ) errors.Error {
101+ return apiCollector .InitCollector (api.ApiCollectorArgs {
105102 ApiClient : data .ApiClient ,
106103 PageSize : 100 ,
107104 Incremental : false ,
108- UrlTemplate : urlTemplate ,
105+ UrlTemplate : "api/2/search" ,
109106 Query : func (reqData * api.RequestData ) (url.Values , errors.Error ) {
110107 query := url.Values {}
111108 epicKeys := []string {}
112- for _ , e := range reqData .Input .([]interface {}) {
113- epicKeys = append (epicKeys , * e .(* string ))
109+
110+ input , ok := reqData .Input .([]interface {})
111+ if ! ok {
112+ return nil , errors .Default .New ("invalid input type, expected []interface{}" )
114113 }
114+
115+ for _ , e := range input {
116+ if epicKey , ok := e .(* string ); ok && epicKey != nil {
117+ epicKeys = append (epicKeys , * epicKey )
118+ }
119+ }
120+
115121 localJQL := fmt .Sprintf ("issue in (%s) and %s" , strings .Join (epicKeys , "," ), jql )
116122 query .Set ("jql" , localJQL )
117123 query .Set ("startAt" , fmt .Sprintf ("%v" , reqData .Pager .Skip ))
118124 query .Set ("maxResults" , fmt .Sprintf ("%v" , reqData .Pager .Size ))
119125 query .Set ("expand" , "changelog" )
120- // api/3 requires fields parameter, api/2 does not
121- if shouldAddFieldsParam {
122- query .Set ("fields" , "*all" )
123- }
124126 return query , nil
125127 },
126128 Input : epicIterator ,
@@ -140,13 +142,78 @@ func CollectEpics(taskCtx plugin.SubTaskContext) errors.Error {
140142 }
141143 return data .Issues , nil
142144 },
143- // Jira Server returns 400 if the epic is not found
144145 AfterResponse : ignoreHTTPStatus400 ,
145146 })
147+ }
148+
149+ // JIRA Cloud API v3 collector
150+ func setupApiV3Collector (apiCollector * api.StatefulApiCollector , data * JiraTaskData , epicIterator api.Iterator , jql string ) errors.Error {
151+ return apiCollector .InitCollector (api.ApiCollectorArgs {
152+ ApiClient : data .ApiClient ,
153+ PageSize : 100 ,
154+ Incremental : false ,
155+ UrlTemplate : "api/3/search/jql" ,
156+ GetNextPageCustomData : getNextPageCustomDataForV3 ,
157+ Query : func (reqData * api.RequestData ) (url.Values , errors.Error ) {
158+ query := url.Values {}
159+ epicKeys := []string {}
160+ for _ , e := range reqData .Input .([]interface {}) {
161+ epicKeys = append (epicKeys , * e .(* string ))
162+ }
163+ localJQL := fmt .Sprintf ("issue in (%s) and %s" , strings .Join (epicKeys , "," ), jql )
164+ query .Set ("jql" , localJQL )
165+ query .Set ("maxResults" , fmt .Sprintf ("%v" , reqData .Pager .Size ))
166+ query .Set ("expand" , "changelog" )
167+ query .Set ("fields" , "*all" )
168+
169+ if reqData .CustomData != nil {
170+ query .Set ("nextPageToken" , reqData .CustomData .(string ))
171+ }
172+
173+ return query , nil
174+ },
175+ Input : epicIterator ,
176+ ResponseParser : func (res * http.Response ) ([]json.RawMessage , errors.Error ) {
177+ var data struct {
178+ Issues []json.RawMessage `json:"issues"`
179+ }
180+ blob , err := io .ReadAll (res .Body )
181+ if err != nil {
182+ return nil , errors .Convert (err )
183+ }
184+ err = json .Unmarshal (blob , & data )
185+ if err != nil {
186+ return nil , errors .Convert (err )
187+ }
188+ return data .Issues , nil
189+ },
190+ AfterResponse : ignoreHTTPStatus400 ,
191+ })
192+ }
193+
194+ // Get next page token for API v3
195+ func getNextPageCustomDataForV3 (_ * api.RequestData , prevPageResponse * http.Response ) (interface {}, errors.Error ) {
196+ var response struct {
197+ NextPageToken string `json:"nextPageToken"`
198+ }
199+
200+ blob , err := io .ReadAll (prevPageResponse .Body )
146201 if err != nil {
147- return err
202+ return nil , errors . Convert ( err )
148203 }
149- return apiCollector .Execute ()
204+
205+ prevPageResponse .Body = io .NopCloser (strings .NewReader (string (blob )))
206+
207+ err = json .Unmarshal (blob , & response )
208+ if err != nil {
209+ return nil , errors .Convert (err )
210+ }
211+
212+ if response .NextPageToken == "" {
213+ return nil , api .ErrFinishCollect
214+ }
215+
216+ return response .NextPageToken , nil
150217}
151218
152219func GetEpicKeysIterator (db dal.Dal , data * JiraTaskData , batchSize int ) (api.Iterator , errors.Error ) {
0 commit comments