From dc01e51e6b76c310ddad1946ceaa0b0c72f7df1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Miguel=20Tabosa=20Vaz=20Marques=20Silva?= Date: Thu, 16 Mar 2023 12:28:28 +0000 Subject: [PATCH] Added support for paging using cursor in search results. --- .../JsonTestFiles/search.json | 1 + src/Dapplo.Confluence/ContentExtensions.cs | 18 ++++++++-- .../Entities/CursorBasedResult.cs | 35 +++++++++++++++++++ .../Entities/SearchDetails.cs | 5 +++ 4 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 src/Dapplo.Confluence/Entities/CursorBasedResult.cs diff --git a/src/Dapplo.Confluence.Tests/JsonTestFiles/search.json b/src/Dapplo.Confluence.Tests/JsonTestFiles/search.json index 3350be0..6b40709 100644 --- a/src/Dapplo.Confluence.Tests/JsonTestFiles/search.json +++ b/src/Dapplo.Confluence.Tests/JsonTestFiles/search.json @@ -29,6 +29,7 @@ "size": 1, "_links": { "self": "https://greenshot.atlassian.net/wiki/rest/api/content/search?cql=text%20~%20%22greenshot%22", + "next": "/rest/api/search?cql=text%20~%20%22greenshot%22&limit=25&cursor=raNDoMsTRiNg", "base": "https://greenshot.atlassian.net/wiki", "context": "/wiki" } diff --git a/src/Dapplo.Confluence/ContentExtensions.cs b/src/Dapplo.Confluence/ContentExtensions.cs index aed1411..491c59a 100644 --- a/src/Dapplo.Confluence/ContentExtensions.cs +++ b/src/Dapplo.Confluence/ContentExtensions.cs @@ -262,11 +262,12 @@ public static async Task GetHistoryAsync(this IContentDomain confluence /// the execution context for CQL functions, provides current space key and content id. If this is /// not provided some CQL functions will not be available. /// + /// Cursor identifier to get the next pages of the results /// PagingInformation /// The expand value for the search, when null the value from the ConfluenceClientConfig.ExpandSearch is taken /// CancellationToken /// Result with content items - public static Task> SearchAsync(this IContentDomain confluenceClient, IFinalClause cqlClause, string cqlContext = null, PagingInformation pagingInformation = null, IEnumerable expandSearch = null, + public static Task> SearchAsync(this IContentDomain confluenceClient, IFinalClause cqlClause, string cqlContext = null, string cursor = null, PagingInformation pagingInformation = null, IEnumerable expandSearch = null, CancellationToken cancellationToken = default) { var searchDetails = new SearchDetails(cqlClause) @@ -283,6 +284,11 @@ public static Task> SearchAsync(this IContentDomain confluenceCl { searchDetails.ExpandSearch = expandSearch; } + if (cursor != null) + { + searchDetails.Cursor = cursor; + } + return confluenceClient.SearchAsync(searchDetails, cancellationToken); } @@ -295,7 +301,7 @@ public static Task> SearchAsync(this IContentDomain confluenceCl /// All the details needed for a search /// CancellationToken /// Result with content items - public static async Task> SearchAsync(this IContentDomain confluenceClient, SearchDetails searchDetails, CancellationToken cancellationToken = default) + public static async Task> SearchAsync(this IContentDomain confluenceClient, SearchDetails searchDetails, CancellationToken cancellationToken = default) { if (searchDetails == null) throw new ArgumentNullException(nameof(searchDetails)); @@ -311,6 +317,12 @@ public static async Task> SearchAsync(this IContentDomain conflu { searchUri = searchUri.ExtendQuery("start", searchDetails.Start); } + if (!string.IsNullOrEmpty(searchDetails.Cursor)) + { + searchUri = searchUri.ExtendQuery("cursor", searchDetails.Cursor); + searchUri = searchUri.ExtendQuery("next", "true"); + } + var expand = string.Join(",", searchDetails.ExpandSearch ?? ConfluenceClientConfig.ExpandSearch ?? Enumerable.Empty()); if (!string.IsNullOrEmpty(expand)) @@ -323,7 +335,7 @@ public static async Task> SearchAsync(this IContentDomain conflu searchUri = searchUri.ExtendQuery("cqlcontext", searchDetails.CqlContext); } - var response = await searchUri.GetAsAsync, Error>>(cancellationToken).ConfigureAwait(false); + var response = await searchUri.GetAsAsync, Error>>(cancellationToken).ConfigureAwait(false); return response.HandleErrors(); } diff --git a/src/Dapplo.Confluence/Entities/CursorBasedResult.cs b/src/Dapplo.Confluence/Entities/CursorBasedResult.cs new file mode 100644 index 0000000..0226fcc --- /dev/null +++ b/src/Dapplo.Confluence/Entities/CursorBasedResult.cs @@ -0,0 +1,35 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Dapplo.Confluence.Entities +{ + /// + /// A container to store pageable results that need a cursor to be paged. + /// See: https://developer.atlassian.com/cloud/confluence/rest/v1/api-group-search/ + /// + /// + public class CursorBasedResult : Result + { + private string cursor = null; + + /// + /// Cursor needed to page trought the results. + /// + [JsonIgnore] + public string Cursor + { + get + { + if (!HasNext) return null; + if (cursor == null) + { + var querystring = Links.Next.OriginalString.Substring(Links.Next.OriginalString.IndexOf('?')); + cursor = UriParseExtensions.QueryStringToDictionary(querystring)?["cursor"]; + } + return cursor; + } + } + } +} diff --git a/src/Dapplo.Confluence/Entities/SearchDetails.cs b/src/Dapplo.Confluence/Entities/SearchDetails.cs index 608acac..19ed291 100644 --- a/src/Dapplo.Confluence/Entities/SearchDetails.cs +++ b/src/Dapplo.Confluence/Entities/SearchDetails.cs @@ -31,4 +31,9 @@ public SearchDetails(IFinalClause cql) /// Specify the search expand values, default is what is specified in the ConfluenceClientConfig.ExpandSearch /// public IEnumerable ExpandSearch { get; set; } = ConfluenceClientConfig.ExpandSearch; + + /// + /// Cursor used to page trought search results. + /// + public string Cursor { get; set; } } \ No newline at end of file