Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 163 additions & 12 deletions issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand All @@ -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.
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
12 changes: 10 additions & 2 deletions jira.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading