PageRenderTime 63ms CodeModel.GetById 1ms app.highlight 55ms RepoModel.GetById 1ms app.codeStats 0ms

/Atlassian.Jira/Remote/IssueService.cs

https://bitbucket.org/johnhilberts/atlassian.net-sdk
C# | 535 lines | 443 code | 90 blank | 2 comment | 33 complexity | 0d4e054ecf54d917d35f1c13969631c9 MD5 | raw file
  1using System;
  2using System.Collections.Generic;
  3using System.Diagnostics;
  4using System.Linq;
  5using System.Threading;
  6using System.Threading.Tasks;
  7using Atlassian.Jira.Linq;
  8using Newtonsoft.Json;
  9using Newtonsoft.Json.Linq;
 10using RestSharp;
 11
 12namespace Atlassian.Jira.Remote
 13{
 14    internal class IssueService : IIssueService
 15    {
 16        private const int DEFAULT_MAX_ISSUES_PER_REQUEST = 20;
 17
 18        private readonly Jira _jira;
 19        private readonly JiraRestClientSettings _restSettings;
 20
 21        private JsonSerializerSettings _serializerSettings;
 22
 23        private class JqlOptions
 24        {
 25            public JqlOptions(string jql, CancellationToken token)
 26            {
 27                this.Jql = jql;
 28                this.Token = token;
 29            }
 30
 31            public string Jql { get; private set; }
 32            public CancellationToken Token { get; private set; }
 33            public int MaxIssuesPerRequest { get; set; } = DEFAULT_MAX_ISSUES_PER_REQUEST;
 34            public int StartAt { get; set; } = 0;
 35            public bool ValidateQuery { get; set; } = true;
 36        }
 37
 38        public IssueService(Jira jira, JiraRestClientSettings restSettings)
 39        {
 40            _jira = jira;
 41            _restSettings = restSettings;
 42        }
 43
 44        public JiraQueryable<Issue> Queryable
 45        {
 46            get
 47            {
 48                var translator = _jira.Services.Get<IJqlExpressionVisitor>();
 49                var provider = new JiraQueryProvider(translator, this);
 50                return new JiraQueryable<Issue>(provider);
 51            }
 52        }
 53
 54        public bool ValidateQuery { get; set; } = true;
 55
 56        public int MaxIssuesPerRequest { get; set; } = DEFAULT_MAX_ISSUES_PER_REQUEST;
 57
 58        private async Task<JsonSerializerSettings> GetIssueSerializerSettingsAsync(CancellationToken token)
 59        {
 60            if (this._serializerSettings == null)
 61            {
 62                var fieldService = _jira.Services.Get<IIssueFieldService>();
 63                var customFields = await fieldService.GetCustomFieldsAsync(token).ConfigureAwait(false);
 64                var remoteFields = customFields.Select(f => f.RemoteField);
 65
 66                var serializers = new Dictionary<string, ICustomFieldValueSerializer>(this._restSettings.CustomFieldSerializers, StringComparer.InvariantCultureIgnoreCase);
 67
 68                this._serializerSettings = new JsonSerializerSettings();
 69                this._serializerSettings.NullValueHandling = NullValueHandling.Ignore;
 70                this._serializerSettings.Converters.Add(new RemoteIssueJsonConverter(remoteFields, serializers));
 71            }
 72
 73            return this._serializerSettings;
 74        }
 75
 76        public async Task<Issue> GetIssueAsync(string issueKey, CancellationToken token = default(CancellationToken))
 77        {
 78            var resource = String.Format("rest/api/2/issue/{0}", issueKey);
 79            var response = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resource, null, token).ConfigureAwait(false);
 80            var serializerSettings = await GetIssueSerializerSettingsAsync(token).ConfigureAwait(false);
 81            var issue = JsonConvert.DeserializeObject<RemoteIssueWrapper>(response.ToString(), serializerSettings);
 82
 83            return new Issue(_jira, issue.RemoteIssue);
 84        }
 85
 86        public Task<IPagedQueryResult<Issue>> GetIssuesFromJqlAsync(string jql, int? maxIssues = default(int?), int startAt = 0, CancellationToken token = default(CancellationToken))
 87        {
 88            return GetIssuesFromJqlAsync(new JqlOptions(jql, token)
 89            {
 90                MaxIssuesPerRequest = maxIssues ?? this.MaxIssuesPerRequest,
 91                StartAt = startAt,
 92                ValidateQuery = this.ValidateQuery
 93            });
 94        }
 95
 96        private async Task<IPagedQueryResult<Issue>> GetIssuesFromJqlAsync(JqlOptions options)
 97        {
 98            if (_jira.Debug)
 99            {
100                Trace.WriteLine("[GetFromJqlAsync] JQL: " + options.Jql);
101            }
102
103            var parameters = new
104            {
105                jql = options.Jql,
106                startAt = options.StartAt,
107                maxResults = options.MaxIssuesPerRequest,
108                validateQuery = options.ValidateQuery,
109            };
110
111            var result = await _jira.RestClient.ExecuteRequestAsync(Method.POST, "rest/api/2/search", parameters, options.Token).ConfigureAwait(false);
112            var serializerSettings = await this.GetIssueSerializerSettingsAsync(options.Token).ConfigureAwait(false);
113            var issues = result["issues"]
114                .Cast<JObject>()
115                .Select(issueJson =>
116                {
117                    var remoteIssue = JsonConvert.DeserializeObject<RemoteIssueWrapper>(issueJson.ToString(), serializerSettings).RemoteIssue;
118                    return new Issue(_jira, remoteIssue);
119                });
120
121            return PagedQueryResult<Issue>.FromJson((JObject)result, issues);
122        }
123
124        public async Task UpdateIssueAsync(Issue issue, IssueUpdateOptions options, CancellationToken token = default(CancellationToken))
125        {
126            var resource = String.Format("rest/api/2/issue/{0}", issue.Key.Value);
127            if (options.SuppressEmailNotification)
128            {
129                resource += "?notifyUsers=false";
130            }
131            var fieldProvider = issue as IRemoteIssueFieldProvider;
132            var remoteFields = await fieldProvider.GetRemoteFieldValuesAsync(token).ConfigureAwait(false);
133            var remoteIssue = await issue.ToRemoteAsync(token).ConfigureAwait(false);
134            var fields = await this.BuildFieldsObjectFromIssueAsync(remoteIssue, remoteFields, token).ConfigureAwait(false);
135
136            await _jira.RestClient.ExecuteRequestAsync(Method.PUT, resource, new { fields = fields }, token).ConfigureAwait(false);
137        }
138
139        public Task UpdateIssueAsync(Issue issue, CancellationToken token = default(CancellationToken))
140        {
141            var options = new IssueUpdateOptions();
142            return UpdateIssueAsync(issue, options, token);
143        }
144
145        public async Task<string> CreateIssueAsync(Issue issue, CancellationToken token = default(CancellationToken))
146        {
147            var remoteIssue = await issue.ToRemoteAsync(token).ConfigureAwait(false);
148            var remoteIssueWrapper = new RemoteIssueWrapper(remoteIssue, issue.ParentIssueKey);
149            var serializerSettings = await this.GetIssueSerializerSettingsAsync(token).ConfigureAwait(false);
150            var requestBody = JsonConvert.SerializeObject(remoteIssueWrapper, serializerSettings);
151
152            var result = await _jira.RestClient.ExecuteRequestAsync(Method.POST, "rest/api/2/issue", requestBody, token).ConfigureAwait(false);
153            return (string)result["key"];
154        }
155
156        private async Task<JObject> BuildFieldsObjectFromIssueAsync(RemoteIssue remoteIssue, RemoteFieldValue[] remoteFields, CancellationToken token)
157        {
158            var issueWrapper = new RemoteIssueWrapper(remoteIssue);
159            var serializerSettings = await this.GetIssueSerializerSettingsAsync(token).ConfigureAwait(false);
160            var issueJson = JsonConvert.SerializeObject(issueWrapper, serializerSettings);
161
162            var fieldsJsonSerializerSettings = new JsonSerializerSettings()
163            {
164                DateParseHandling = DateParseHandling.None
165            };
166
167            var issueFields = JsonConvert.DeserializeObject<JObject>(issueJson, fieldsJsonSerializerSettings)["fields"] as JObject;
168            var updateFields = new JObject();
169
170            foreach (var field in remoteFields)
171            {
172                var issueFieldName = field.id;
173                var issueFieldValue = issueFields[issueFieldName];
174
175                if (issueFieldValue == null && issueFieldName.Equals("components", StringComparison.OrdinalIgnoreCase))
176                {
177                    // JIRA does not accept 'null' as a valid value for the 'components' field.
178                    //   So if the components field has been cleared it must be set to empty array instead.
179                    issueFieldValue = new JArray();
180                }
181
182                updateFields.Add(issueFieldName, issueFieldValue);
183            }
184
185            return updateFields;
186        }
187
188        public async Task ExecuteWorkflowActionAsync(Issue issue, string actionName, WorkflowTransitionUpdates updates, CancellationToken token = default(CancellationToken))
189        {
190            var actions = await this.GetActionsAsync(issue.Key.Value, token).ConfigureAwait(false);
191            var action = actions.FirstOrDefault(a => a.Name.Equals(actionName, StringComparison.OrdinalIgnoreCase));
192
193            if (action == null)
194            {
195                throw new InvalidOperationException(String.Format("Workflow action with name '{0}' not found.", actionName));
196            }
197
198            updates = updates ?? new WorkflowTransitionUpdates();
199
200            var resource = String.Format("rest/api/2/issue/{0}/transitions", issue.Key.Value);
201            var fieldProvider = issue as IRemoteIssueFieldProvider;
202            var remoteFields = await fieldProvider.GetRemoteFieldValuesAsync(token).ConfigureAwait(false);
203            var remoteIssue = await issue.ToRemoteAsync(token).ConfigureAwait(false);
204            var fields = await BuildFieldsObjectFromIssueAsync(remoteIssue, remoteFields, token).ConfigureAwait(false);
205            var updatesObject = new JObject();
206
207            if (!String.IsNullOrEmpty(updates.Comment))
208            {
209                updatesObject.Add("comment", new JArray(new JObject[]
210                {
211                    new JObject(new JProperty("add",
212                        new JObject(new JProperty("body", updates.Comment))))
213                }));
214            }
215
216            var requestBody = new
217            {
218                transition = new
219                {
220                    id = action.Id
221                },
222                update = updatesObject,
223                fields = fields
224            };
225
226            await _jira.RestClient.ExecuteRequestAsync(Method.POST, resource, requestBody, token).ConfigureAwait(false);
227        }
228
229        public async Task<IssueTimeTrackingData> GetTimeTrackingDataAsync(string issueKey, CancellationToken token = default(CancellationToken))
230        {
231            if (String.IsNullOrEmpty(issueKey))
232            {
233                throw new InvalidOperationException("Unable to retrieve time tracking data, make sure the issue has been created.");
234            }
235
236            var resource = String.Format("rest/api/2/issue/{0}?fields=timetracking", issueKey);
237            var response = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resource, null, token).ConfigureAwait(false);
238
239            var serializerSettings = _jira.RestClient.Settings.JsonSerializerSettings;
240            var timeTrackingJson = response["fields"]?["timetracking"];
241
242            if (timeTrackingJson != null)
243            {
244                return JsonConvert.DeserializeObject<IssueTimeTrackingData>(timeTrackingJson.ToString(), serializerSettings);
245            }
246            else
247            {
248                return null;
249            }
250        }
251
252        public async Task<IDictionary<string, IssueFieldEditMetadata>> GetFieldsEditMetadataAsync(string issueKey, CancellationToken token = default(CancellationToken))
253        {
254            var dict = new Dictionary<string, IssueFieldEditMetadata>();
255            var resource = String.Format("rest/api/2/issue/{0}/editmeta", issueKey);
256            var serializer = JsonSerializer.Create(_jira.RestClient.Settings.JsonSerializerSettings);
257            var result = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resource, null, token).ConfigureAwait(false);
258            JObject fields = result["fields"].Value<JObject>();
259
260            foreach (var prop in fields.Properties())
261            {
262                var fieldName = (prop.Value["name"] ?? prop.Name).ToString();
263                dict.Add(fieldName, prop.Value.ToObject<IssueFieldEditMetadata>(serializer));
264            }
265
266            return dict;
267        }
268
269        public async Task<Comment> AddCommentAsync(string issueKey, Comment comment, CancellationToken token = default(CancellationToken))
270        {
271            if (String.IsNullOrEmpty(comment.Author))
272            {
273                throw new InvalidOperationException("Unable to add comment due to missing author field.");
274            }
275
276            var resource = String.Format("rest/api/2/issue/{0}/comment", issueKey);
277            var remoteComment = await _jira.RestClient.ExecuteRequestAsync<RemoteComment>(Method.POST, resource, comment.toRemote(), token).ConfigureAwait(false);
278            return new Comment(remoteComment);
279        }
280
281        public async Task<IPagedQueryResult<Comment>> GetPagedCommentsAsync(string issueKey, int? maxComments = default(int?), int startAt = 0, CancellationToken token = default(CancellationToken))
282        {
283            var resource = $"rest/api/2/issue/{issueKey}/comment?startAt={startAt}";
284
285            if (maxComments.HasValue)
286            {
287                resource += $"&maxResults={maxComments.Value}";
288            }
289
290            var result = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resource).ConfigureAwait(false);
291            var serializerSettings = _jira.RestClient.Settings.JsonSerializerSettings;
292            var comments = result["comments"]
293                .Cast<JObject>()
294                .Select(commentJson =>
295                {
296                    var remoteComment = JsonConvert.DeserializeObject<RemoteComment>(commentJson.ToString(), serializerSettings);
297                    return new Comment(remoteComment);
298                });
299
300            return PagedQueryResult<Comment>.FromJson((JObject)result, comments);
301        }
302
303        public async Task<IEnumerable<JiraNamedEntity>> GetActionsAsync(string issueKey, CancellationToken token = default(CancellationToken))
304        {
305            var resource = String.Format("rest/api/2/issue/{0}/transitions", issueKey);
306            var serializerSettings = _jira.RestClient.Settings.JsonSerializerSettings;
307            var result = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resource, null, token).ConfigureAwait(false);
308            var remoteTransitions = JsonConvert.DeserializeObject<RemoteNamedObject[]>(result["transitions"].ToString(), serializerSettings);
309
310            return remoteTransitions.Select(transition => new JiraNamedEntity(transition));
311        }
312
313        public async Task<IEnumerable<Attachment>> GetAttachmentsAsync(string issueKey, CancellationToken token = default(CancellationToken))
314        {
315            var resource = String.Format("rest/api/2/issue/{0}?fields=attachment", issueKey);
316            var serializerSettings = _jira.RestClient.Settings.JsonSerializerSettings;
317            var result = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resource, null, token).ConfigureAwait(false);
318            var attachmentsJson = result["fields"]["attachment"];
319            var attachments = JsonConvert.DeserializeObject<RemoteAttachment[]>(attachmentsJson.ToString(), serializerSettings);
320
321            return attachments.Select(remoteAttachment => new Attachment(_jira, new WebClientWrapper(_jira), remoteAttachment));
322        }
323
324        public async Task<string[]> GetLabelsAsync(string issueKey, CancellationToken token = default(CancellationToken))
325        {
326            var resource = String.Format("rest/api/2/issue/{0}?fields=labels", issueKey);
327            var serializerSettings = await this.GetIssueSerializerSettingsAsync(token).ConfigureAwait(false);
328            var response = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resource).ConfigureAwait(false);
329            var issue = JsonConvert.DeserializeObject<RemoteIssueWrapper>(response.ToString(), serializerSettings);
330            return issue.RemoteIssue.labels ?? new string[0];
331        }
332
333        public Task SetLabelsAsync(string issueKey, string[] labels, CancellationToken token = default(CancellationToken))
334        {
335            var resource = String.Format("rest/api/2/issue/{0}", issueKey);
336            return _jira.RestClient.ExecuteRequestAsync(Method.PUT, resource, new
337            {
338                fields = new
339                {
340                    labels = labels
341                }
342
343            }, token);
344        }
345
346        public async Task<IEnumerable<JiraUser>> GetWatchersAsync(string issueKey, CancellationToken token = default(CancellationToken))
347        {
348            if (string.IsNullOrEmpty(issueKey))
349            {
350                throw new InvalidOperationException("Unable to interact with the watchers resource, make sure the issue has been created.");
351            }
352
353            var resourceUrl = String.Format("rest/api/2/issue/{0}/watchers", issueKey);
354            var serializerSettings = _jira.RestClient.Settings.JsonSerializerSettings;
355            var result = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resourceUrl, null, token).ConfigureAwait(false);
356            var watchersJson = result["watchers"];
357            return watchersJson.Select(watcherJson => JsonConvert.DeserializeObject<JiraUser>(watcherJson.ToString(), serializerSettings));
358        }
359
360        public async Task<IEnumerable<IssueChangeLog>> GetChangeLogsAsync(string issueKey, CancellationToken token = default(CancellationToken))
361        {
362            var resourceUrl = String.Format("rest/api/2/issue/{0}?fields=created&expand=changelog", issueKey);
363            var serializerSettings = _jira.RestClient.Settings.JsonSerializerSettings;
364            var response = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resourceUrl, null, token).ConfigureAwait(false);
365            var result = Enumerable.Empty<IssueChangeLog>();
366            var changeLogs = response["changelog"];
367            if (changeLogs != null)
368            {
369                var histories = changeLogs["histories"];
370                if (histories != null)
371                {
372                    result = histories.Select(history => JsonConvert.DeserializeObject<IssueChangeLog>(history.ToString(), serializerSettings));
373                }
374            }
375
376            return result;
377        }
378
379        public Task DeleteWatcherAsync(string issueKey, string username, CancellationToken token = default(CancellationToken))
380        {
381            if (string.IsNullOrEmpty(issueKey))
382            {
383                throw new InvalidOperationException("Unable to interact with the watchers resource, make sure the issue has been created.");
384            }
385
386            var resourceUrl = String.Format("rest/api/2/issue/{0}/watchers?username={1}", issueKey, System.Uri.EscapeDataString(username));
387            return _jira.RestClient.ExecuteRequestAsync(Method.DELETE, resourceUrl, null, token);
388        }
389
390        public Task AddWatcherAsync(string issueKey, string username, CancellationToken token = default(CancellationToken))
391        {
392            if (string.IsNullOrEmpty(issueKey))
393            {
394                throw new InvalidOperationException("Unable to interact with the watchers resource, make sure the issue has been created.");
395            }
396
397            var requestBody = String.Format("\"{0}\"", username);
398            var resourceUrl = String.Format("rest/api/2/issue/{0}/watchers", issueKey);
399            return _jira.RestClient.ExecuteRequestAsync(Method.POST, resourceUrl, requestBody, token);
400        }
401
402        public Task<IPagedQueryResult<Issue>> GetSubTasksAsync(string issueKey, int? maxIssues = default(int?), int startAt = 0, CancellationToken token = default(CancellationToken))
403        {
404            var jql = String.Format("parent = {0}", issueKey);
405            return GetIssuesFromJqlAsync(jql, maxIssues, startAt, token);
406        }
407
408        public Task AddAttachmentsAsync(string issueKey, UploadAttachmentInfo[] attachments, CancellationToken token = default(CancellationToken))
409        {
410            var resource = String.Format("rest/api/2/issue/{0}/attachments", issueKey);
411            var request = new RestRequest();
412            request.Method = Method.POST;
413            request.Resource = resource;
414            request.AddHeader("X-Atlassian-Token", "nocheck");
415            request.AlwaysMultipartFormData = true;
416
417            foreach (var attachment in attachments)
418            {
419                request.AddFile("file", attachment.Data, attachment.Name);
420            }
421
422            return _jira.RestClient.ExecuteRequestAsync(request, token);
423        }
424
425        public Task DeleteAttachmentAsync(string issueKey, string attachmentId, CancellationToken token = default(CancellationToken))
426        {
427            var resource = String.Format("rest/api/2/attachment/{0}", attachmentId);
428
429            return _jira.RestClient.ExecuteRequestAsync(Method.DELETE, resource, null, token);
430        }
431
432        public async Task<IDictionary<string, Issue>> GetIssuesAsync(IEnumerable<string> issueKeys, CancellationToken token = default(CancellationToken))
433        {
434            if (issueKeys.Any())
435            {
436                var distinctKeys = issueKeys.Distinct();
437                var jql = String.Format("key in ({0})", String.Join(",", distinctKeys));
438                var result = await this.GetIssuesFromJqlAsync(new JqlOptions(jql, token)
439                {
440                    MaxIssuesPerRequest = distinctKeys.Count(),
441                    ValidateQuery = false
442                }).ConfigureAwait(false);
443                return result.ToDictionary<Issue, string>(i => i.Key.Value);
444            }
445            else
446            {
447                return new Dictionary<string, Issue>();
448            }
449        }
450
451        public Task<IDictionary<string, Issue>> GetIssuesAsync(params string[] issueKeys)
452        {
453            return this.GetIssuesAsync(issueKeys, default(CancellationToken));
454        }
455
456        public async Task<IEnumerable<Comment>> GetCommentsAsync(string issueKey, CancellationToken token = default(CancellationToken))
457        {
458            var resource = String.Format("rest/api/2/issue/{0}/comment?expand=properties", issueKey);
459            var serializerSettings = _jira.RestClient.Settings.JsonSerializerSettings;
460            var issueJson = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resource, null, token).ConfigureAwait(false);
461            var commentJson = issueJson["comments"];
462
463            var remoteComments = JsonConvert.DeserializeObject<RemoteComment[]>(commentJson.ToString(), serializerSettings);
464
465            return remoteComments.Select(c => new Comment(c));
466        }
467
468        public Task DeleteCommentAsync(string issueKey, string commentId, CancellationToken token = default(CancellationToken))
469        {
470            var resource = String.Format("rest/api/2/issue/{0}/comment/{1}", issueKey, commentId);
471
472            return _jira.RestClient.ExecuteRequestAsync(Method.DELETE, resource, null, token);
473        }
474
475        public async Task<Worklog> AddWorklogAsync(string issueKey, Worklog worklog, WorklogStrategy worklogStrategy = WorklogStrategy.AutoAdjustRemainingEstimate, string newEstimate = null, CancellationToken token = default(CancellationToken))
476        {
477            var remoteWorklog = worklog.ToRemote();
478            string queryString = null;
479
480            if (worklogStrategy == WorklogStrategy.RetainRemainingEstimate)
481            {
482                queryString = "adjustEstimate=leave";
483            }
484            else if (worklogStrategy == WorklogStrategy.NewRemainingEstimate)
485            {
486                queryString = "adjustEstimate=new&newEstimate=" + Uri.EscapeDataString(newEstimate);
487            }
488
489            var resource = String.Format("rest/api/2/issue/{0}/worklog?{1}", issueKey, queryString);
490            var serverWorklog = await _jira.RestClient.ExecuteRequestAsync<RemoteWorklog>(Method.POST, resource, remoteWorklog, token).ConfigureAwait(false);
491            return new Worklog(serverWorklog);
492        }
493
494        public Task DeleteWorklogAsync(string issueKey, string worklogId, WorklogStrategy worklogStrategy = WorklogStrategy.AutoAdjustRemainingEstimate, string newEstimate = null, CancellationToken token = default(CancellationToken))
495        {
496            string queryString = null;
497
498            if (worklogStrategy == WorklogStrategy.RetainRemainingEstimate)
499            {
500                queryString = "adjustEstimate=leave";
501            }
502            else if (worklogStrategy == WorklogStrategy.NewRemainingEstimate)
503            {
504                queryString = "adjustEstimate=new&newEstimate=" + Uri.EscapeDataString(newEstimate);
505            }
506
507            var resource = String.Format("rest/api/2/issue/{0}/worklog/{1}?{2}", issueKey, worklogId, queryString);
508            return _jira.RestClient.ExecuteRequestAsync(Method.DELETE, resource, null, token);
509        }
510
511        public async Task<IEnumerable<Worklog>> GetWorklogsAsync(string issueKey, CancellationToken token = default(CancellationToken))
512        {
513            var resource = String.Format("rest/api/2/issue/{0}/worklog", issueKey);
514            var serializerSettings = _jira.RestClient.Settings.JsonSerializerSettings;
515            var response = await _jira.RestClient.ExecuteRequestAsync(Method.GET, resource, null, token).ConfigureAwait(false);
516            var worklogsJson = response["worklogs"];
517            var remoteWorklogs = JsonConvert.DeserializeObject<RemoteWorklog[]>(worklogsJson.ToString(), serializerSettings);
518
519            return remoteWorklogs.Select(w => new Worklog(w));
520        }
521
522        public async Task<Worklog> GetWorklogAsync(string issueKey, string worklogId, CancellationToken token = default(CancellationToken))
523        {
524            var resource = String.Format("rest/api/2/issue/{0}/worklog/{1}", issueKey, worklogId);
525            var remoteWorklog = await _jira.RestClient.ExecuteRequestAsync<RemoteWorklog>(Method.GET, resource, null, token).ConfigureAwait(false);
526            return new Worklog(remoteWorklog);
527        }
528
529        public Task DeleteIssueAsync(string issueKey, CancellationToken token = default(CancellationToken))
530        {
531            var resource = String.Format("rest/api/2/issue/{0}", issueKey);
532            return _jira.RestClient.ExecuteRequestAsync(Method.DELETE, resource, null, token);
533        }
534    }
535}