diff --git a/issue.go b/issue.go index 518b73b1..3a778f5a 100644 --- a/issue.go +++ b/issue.go @@ -35,8 +35,8 @@ type IssueService struct { // UpdateQueryOptions specifies the optional parameters to the Edit issue type UpdateQueryOptions struct { NotifyUsers *bool `url:"notifyUsers,omitempty"` - OverrideScreenSecurity *bool `url:"overrideScreenSecurity,omitempty"` - OverrideEditableFlag *bool `url:"overrideEditableFlag,omitempty"` + OverrideScreenSecurity *bool `url:"overrideScreenSecurity,omitempty"` + OverrideEditableFlag *bool `url:"overrideEditableFlag,omitempty"` } // Issue represents a Jira issue. @@ -527,6 +527,63 @@ type SearchOptions struct { ValidateQuery string `url:"validateQuery,omitempty"` } +// SearchOptionsV2 specifies the parameters for the Jira Cloud-specific +// paramaters to List methods that support pagination +// +// Docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-issue-search/#api-rest-api-2-search-jql-get +type SearchOptionsV2 struct { + // NextPageToken: The token for a page to fetch that is not the first page. + // The first page has a nextPageToken of null. + // Use the nextPageToken to fetch the next page of issues. + // Note: The nextPageToken field is not included in the response for the last page, + // indicating there is no next page. + NextPageToken string `url:"nextPageToken,omitempty"` + + // MaxResults: The maximum number of items to return per page. + // To manage page size, API may return fewer items per page where a large number of fields or properties are requested. + // The greatest number of items returned per page is achieved when requesting id or key only. + // It returns max 5000 issues. + // Default: 50 + MaxResults int `url:"maxResults,omitempty"` + + // Fields: A list of fields to return for each issue + + // Fields: A list of fields to return for each issue, use it to retrieve a subset of fields. + // This parameter accepts a comma-separated list. Expand options include: + // + // `*all` Returns all fields. + // `*navigable` Returns navigable fields. + // `id` Returns only issue IDs. + // Any issue field, prefixed with a minus to exclude. + // + // The default is id. + // + // Examples: + // + // `summary,comment` Returns only the summary and comments fields only. + // `-description` Returns all navigable (default) fields except description. + // `*all,-comment` Returns all fields except comments. + // + // Multiple `fields` parameters can be included in a request. + // + // Note: By default, this resource returns IDs only. This differs from GET issue where the default is all fields. + Fields []string + + // Expand: Use expand to include additional information about issues in the response. + // TODO add proper docs, see https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-issue-search/#api-rest-api-2-search-jql-get + Expand string `url:"expand,omitempty"` + // A list of up to 5 issue properties to include in the results + Properties []string `url:"properties,omitempty"` + // FieldsByKeys: Reference fields by their key (rather than ID). + // The default is false. + FieldsByKeys bool `url:"fieldsByKeys,omitempty"` + // FailFast: Fail this request early if we can't retrieve all field data. + // Default false. + FailFast bool `url:"failFast,omitempty"` + // ReconcileIssues: Strong consistency issue ids to be reconciled with search results. Accepts max 50 ids + ReconcileIssues []int `url:"reconcileIssues,omitempty"` +} + // searchResult is only a small wrapper around the Search (with JQL) method // to be able to parse the results type searchResult struct { @@ -536,6 +593,24 @@ type searchResult struct { Total int `json:"total" structs:"total"` } +// searchResultV2 is only a small wrapper around the Jira Cloud-specific SearchV2 (with JQL) method +// to be able to parse the results +type searchResultV2 struct { + // IsLast: Indicates whether this is the last page of the paginated response. + IsLast bool `json:"isLast" structs:"isLast"` + // Issues: The list of issues found by the search or reconsiliation. + Issues []Issue `json:"issues" structs:"issues"` + + // TODO Missing + // Field names object + // Field schema object + + // NextPageToken: Continuation token to fetch the next page. + // If this result represents the last or the only page this token will be null. + // This token will expire in 7 days. + NextPageToken string `json:"nextPageToken" structs:"nextPageToken"` +} + // GetQueryOptions specifies the optional parameters for the Get Issue methods type GetQueryOptions struct { // Fields is the list of fields to return for the issue. By default, all fields are returned. @@ -613,7 +688,7 @@ type RemoteLinkStatus struct { // This can be an issue id, or an issue key. // If the issue cannot be found via an exact match, Jira will also look for the issue in a case-insensitive way, or by looking to see if the issue was moved. // -// The given options will be appended to the query string +// # # # The given options will be appended to the query string // // Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-getIssue func (s *IssueService) GetWithContext(ctx context.Context, issueID string, options *GetQueryOptions) (*Issue, *Response, error) { @@ -1152,8 +1227,78 @@ func (s *IssueService) Search(jql string, options *SearchOptions) ([]Issue, *Res return s.SearchWithContext(context.Background(), jql, options) } -// SearchPagesWithContext will get issues from all pages in a search +// SearchV2JQL will search for tickets according to the jql for Jira Cloud // +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-issue-search/#api-rest-api-2-search-jql-get +func (s *IssueService) SearchV2JQL(jql string, options *SearchOptionsV2) ([]Issue, *Response, error) { + return s.SearchV2JQLWithContext(context.Background(), jql, options) +} + +func (s *IssueService) SearchV2JQLWithContext(ctx context.Context, jql string, options *SearchOptionsV2) ([]Issue, *Response, error) { + u := url.URL{ + Path: "rest/api/2/search/jql", + } + uv := url.Values{} + if jql != "" { + uv.Add("jql", jql) + } + + // TODO Check this out if this works with addOptions as well + if options != nil { + if options.NextPageToken != "" { + uv.Add("nextPageToken", options.NextPageToken) + } + if options.MaxResults != 0 { + uv.Add("maxResults", strconv.Itoa(options.MaxResults)) + } + if strings.Join(options.Fields, ",") != "" { + uv.Add("fields", strings.Join(options.Fields, ",")) + } + if options.Expand != "" { + uv.Add("expand", options.Expand) + } + if len(options.Properties) > 5 { + return nil, nil, fmt.Errorf("Search option Properties accepts maximum five entries") + } + if strings.Join(options.Properties, ",") != "" { + uv.Add("properties", strings.Join(options.Properties, ",")) + } + if options.FieldsByKeys { + uv.Add("fieldsByKeys", "true") + } + if options.FailFast { + uv.Add("failFast", "true") + } + if len(options.ReconcileIssues) > 50 { + return nil, nil, fmt.Errorf("Search option ReconcileIssue accepts maximum 50 entries") + } + if len(options.ReconcileIssues) > 0 { + // TODO Extract this + // Convert []int to []string for strings.Join + reconcileIssuesStr := make([]string, len(options.ReconcileIssues)) + for i, v := range options.ReconcileIssues { + reconcileIssuesStr[i] = strconv.Itoa(v) + } + uv.Add("reconcileIssues", strings.Join(reconcileIssuesStr, ",")) + } + } + + u.RawQuery = uv.Encode() + + req, err := s.client.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) + if err != nil { + return []Issue{}, nil, err + } + + v := new(searchResultV2) + resp, err := s.client.Do(req, v) + if err != nil { + err = NewJiraError(resp, err) + } + + return v.Issues, resp, err +} + // Jira API docs: https://developer.atlassian.com/jiradev/jira-apis/jira-rest-apis/jira-rest-api-tutorials/jira-rest-api-example-query-issues func (s *IssueService) SearchPagesWithContext(ctx context.Context, jql string, options *SearchOptions, f func(Issue) error) error { if options == nil { @@ -1313,15 +1458,21 @@ func (s *IssueService) DoTransitionWithPayload(ticketID, payload interface{}) (* } // InitIssueWithMetaAndFields returns Issue with with values from fieldsConfig properly set. -// * metaProject should contain metaInformation about the project where the issue should be created. -// * metaIssuetype is the MetaInformation about the Issuetype that needs to be created. -// * fieldsConfig is a key->value pair where key represents the name of the field as seen in the UI -// And value is the string value for that particular key. +// - metaProject should contain metaInformation about the project where the issue should be created. +// -- metaIssuetype is the MetaInformation about the Issuetype that needs to be created. +// -- fieldsConfig is a key->value pair where key represents the name of the field as seen in the UI +// -- And value is the string value for that particular key. +// - +// +// // Note: This method doesn't verify that the fieldsConfig is complete with mandatory fields. The fieldsConfig is -// supposed to be already verified with MetaIssueType.CheckCompleteAndAvailable. It will however return -// error if the key is not found. -// All values will be packed into Unknowns. This is much convenient. If the struct fields needs to be -// configured as well, marshalling and unmarshalling will set the proper fields. +// +// +// +//posed to be already verified with MetaIssueType.CheckCompleteAndAvailable. It will however return +//rr if the key is not found. +//Al values will be packed into Unknowns. This is much convenient. If the struct fields needs to be +// configured as well, marshalling and unmarshalling will set the proper fields. func InitIssueWithMetaAndFields(metaProject *MetaProject, metaIssuetype *MetaIssueType, fieldsConfig map[string]string) (*Issue, error) { issue := new(Issue) issueFields := new(IssueFields) diff --git a/jira.go b/jira.go index 3ffbf62b..528f8fb2 100644 --- a/jira.go +++ b/jira.go @@ -354,6 +354,10 @@ type Response struct { StartAt int MaxResults int Total int + + // *searchResultV2 + IsLast bool + NextPageToken string } func newResponse(r *http.Response, v interface{}) *Response { @@ -370,6 +374,9 @@ func (r *Response) populatePageValues(v interface{}) { r.StartAt = value.StartAt r.MaxResults = value.MaxResults r.Total = value.Total + case *searchResultV2: + r.IsLast = value.IsLast + r.NextPageToken = value.NextPageToken case *groupMembersResult: r.StartAt = value.StartAt r.MaxResults = value.MaxResults @@ -729,8 +736,9 @@ func (t *CookieAuthTransport) transport() http.RoundTripper { // // Jira docs: https://developer.atlassian.com/cloud/jira/platform/understanding-jwt // Examples in other languages: -// https://bitbucket.org/atlassian/atlassian-jwt-ruby/src/d44a8e7a4649e4f23edaa784402655fda7c816ea/lib/atlassian/jwt.rb -// https://bitbucket.org/atlassian/atlassian-jwt-py/src/master/atlassian_jwt/url_utils.py +// +// https://bitbucket.org/atlassian/atlassian-jwt-ruby/src/d44a8e7a4649e4f23edaa784402655fda7c816ea/lib/atlassian/jwt.rb +// https://bitbucket.org/atlassian/atlassian-jwt-py/src/master/atlassian_jwt/url_utils.py type JWTAuthTransport struct { Secret []byte Issuer string