diff --git a/Conduit/Conduit.Tests/Integration/Articles/Services/ArticlesDataService/GetArticlesTests.cs b/Conduit/Conduit.Tests/Integration/Articles/Services/ArticlesDataService/GetArticlesTests.cs index fb91bb03c7..8ce85f286f 100644 --- a/Conduit/Conduit.Tests/Integration/Articles/Services/ArticlesDataService/GetArticlesTests.cs +++ b/Conduit/Conduit.Tests/Integration/Articles/Services/ArticlesDataService/GetArticlesTests.cs @@ -68,7 +68,7 @@ public async Task Results_with_no_filters_no_authenticated_user() // assert Assert.That(result.Status, Is.EqualTo(DataResultStatus.Ok)); - Assert.That(result.DataResult.Count, Is.EqualTo(20)); + Assert.That(result.DataResult.Articles.Count, Is.EqualTo(20)); } [Test] @@ -90,7 +90,7 @@ public async Task Results_with_authenticated_user_favorited() // assert Assert.That(results.Status, Is.EqualTo(DataResultStatus.Ok)); - foreach (var result in results.DataResult) + foreach (var result in results.DataResult.Articles) { Assert.That(result.Favorited, Is.True); } @@ -115,7 +115,7 @@ public async Task Results_with_authenticated_user_following_authors() // assert Assert.That(results.Status, Is.EqualTo(DataResultStatus.Ok)); - foreach (var result in results.DataResult) + foreach (var result in results.DataResult.Articles) { Assert.That(result.Author.Following, Is.True); } @@ -144,8 +144,8 @@ public async Task Results_with_tag_specified() // assert Assert.That(results.Status, Is.EqualTo(DataResultStatus.Ok)); - Assert.That(results.DataResult.All(x => x.TagList.Contains("baseball")), Is.True); - Assert.That(results.DataResult.All(x => !x.TagList.Contains("cruising")), Is.True); + Assert.That(results.DataResult.Articles.All(x => x.TagList.Contains("baseball")), Is.True); + Assert.That(results.DataResult.Articles.All(x => !x.TagList.Contains("cruising")), Is.True); } [Test] @@ -170,8 +170,8 @@ public async Task Results_with_author_specified() // assert Assert.That(results.Status, Is.EqualTo(DataResultStatus.Ok)); - Assert.That(results.DataResult.All(x => x.Author.Username == authorTarget.Username), Is.True); - Assert.That(results.DataResult.All(x => x.Author.Username != authorOther.Username), Is.True); + Assert.That(results.DataResult.Articles.All(x => x.Author.Username == authorTarget.Username), Is.True); + Assert.That(results.DataResult.Articles.All(x => x.Author.Username != authorOther.Username), Is.True); } [Test] @@ -196,7 +196,7 @@ public async Task Results_with_favoritedBy_specified() // assert Assert.That(results.Status, Is.EqualTo(DataResultStatus.Ok)); - Assert.That(results.DataResult.All(x => x.Favorited), Is.True); + Assert.That(results.DataResult.Articles.All(x => x.Favorited), Is.True); } [TestCase(5)] @@ -221,7 +221,7 @@ public async Task Results_with_limit_specified(int limit) // assert Assert.That(results.Status, Is.EqualTo(DataResultStatus.Ok)); - Assert.That(results.DataResult.Count, Is.EqualTo(limit)); + Assert.That(results.DataResult.Articles.Count, Is.EqualTo(limit)); } [Test] @@ -255,8 +255,8 @@ public async Task Results_with_offset_specified() // assert Assert.That(results.Status, Is.EqualTo(DataResultStatus.Ok)); - Assert.That(results.DataResult.Count, Is.EqualTo(5)); - foreach (var result in results.DataResult) + Assert.That(results.DataResult.Articles.Count, Is.EqualTo(5)); + foreach (var result in results.DataResult.Articles) Assert.That(expectedSlugs.Any(e => e == result.Slug), Is.True); } @@ -290,8 +290,8 @@ public async Task Get_articles_for_the_feed() // assert Assert.That(results.Status, Is.EqualTo(DataResultStatus.Ok)); - Assert.That(results.DataResult.Count, Is.EqualTo(expectedSlugs.Count)); + Assert.That(results.DataResult.Articles.Count, Is.EqualTo(expectedSlugs.Count)); foreach(var expectedSlug in expectedSlugs) - Assert.That(results.DataResult.Any(r => r.Slug == expectedSlug), Is.True); + Assert.That(results.DataResult.Articles.Any(r => r.Slug == expectedSlug), Is.True); } } \ No newline at end of file diff --git a/Conduit/Conduit.Tests/Unit/Articles/Handlers/GetArticlesHandlerTests.cs b/Conduit/Conduit.Tests/Unit/Articles/Handlers/GetArticlesHandlerTests.cs index 5108b15c65..c9a6d596d6 100644 --- a/Conduit/Conduit.Tests/Unit/Articles/Handlers/GetArticlesHandlerTests.cs +++ b/Conduit/Conduit.Tests/Unit/Articles/Handlers/GetArticlesHandlerTests.cs @@ -18,7 +18,7 @@ public async Task Setup() { _mockArticleDataService = new Mock(); _mockArticleDataService.Setup(m => m.GetArticles(It.IsAny())) - .ReturnsAsync(new DataServiceResult>(new List(), DataResultStatus.Ok)); + .ReturnsAsync(new DataServiceResult(new ArticlesViewModel(), DataResultStatus.Ok)); var validator = new GetArticlesRequestValidator(); _handler = new GetArticlesHandler(validator, _mockArticleDataService.Object); @@ -94,7 +94,7 @@ public async Task Handler_returns_failure_if_getting_articles_data_is_not_OK() var filter = new ArticleFilterOptionsModel(); var request = new GetArticlesRequest("doesnt-matter", filter); _mockArticleDataService.Setup(m => m.GetArticles(It.IsAny())) - .ReturnsAsync(new DataServiceResult>(null, DataResultStatus.Error)); + .ReturnsAsync(new DataServiceResult(null, DataResultStatus.Error)); // act var result = await _handler.Handle(request, CancellationToken.None); diff --git a/Conduit/Conduit.Web/Articles/Controllers/ArticlesController.cs b/Conduit/Conduit.Web/Articles/Controllers/ArticlesController.cs index a7e4c480b7..6b1f9e7e49 100644 --- a/Conduit/Conduit.Web/Articles/Controllers/ArticlesController.cs +++ b/Conduit/Conduit.Web/Articles/Controllers/ArticlesController.cs @@ -269,7 +269,7 @@ public async Task GetArticles([FromQuery] ArticleFilterOptionsMod if (getArticlesResponse.ValidationErrors?.Any() ?? false) return UnprocessableEntity(getArticlesResponse.ValidationErrors.ToCsv()); - return Ok(new { articles = getArticlesResponse.ArticlesView }); + return Ok(new { articles = getArticlesResponse.ArticlesView.Articles, articlesCount = getArticlesResponse.ArticlesView.ArticlesCount }); //, articlesCount = getArticlesResponse.NumTotalArticles }); } /// @@ -298,6 +298,6 @@ public async Task GetFeed([FromQuery] ArticleFeedOptionsModel opt if (getArticlesResponse.ValidationErrors?.Any() ?? false) return UnprocessableEntity(getArticlesResponse.ValidationErrors.ToCsv()); - return Ok(new { articles = getArticlesResponse.ArticlesView }); + return Ok(new { articles = getArticlesResponse.ArticlesView.Articles, articlesCount = getArticlesResponse.ArticlesView.ArticlesCount }); } } diff --git a/Conduit/Conduit.Web/Articles/Handlers/GetArticlesResponse.cs b/Conduit/Conduit.Web/Articles/Handlers/GetArticlesResponse.cs index 9f411cc9ef..7c71583985 100644 --- a/Conduit/Conduit.Web/Articles/Handlers/GetArticlesResponse.cs +++ b/Conduit/Conduit.Web/Articles/Handlers/GetArticlesResponse.cs @@ -6,6 +6,6 @@ namespace Conduit.Web.Articles.Handlers; public class GetArticlesResponse { public List ValidationErrors { get; set; } - public List ArticlesView { get; set; } + public ArticlesViewModel ArticlesView { get; set; } public bool IsFailure { get; set; } } \ No newline at end of file diff --git a/Conduit/Conduit.Web/Articles/Handlers/GetFeedHandler.cs b/Conduit/Conduit.Web/Articles/Handlers/GetFeedHandler.cs index 1da617ffd2..fc3727e78e 100644 --- a/Conduit/Conduit.Web/Articles/Handlers/GetFeedHandler.cs +++ b/Conduit/Conduit.Web/Articles/Handlers/GetFeedHandler.cs @@ -44,7 +44,7 @@ public async Task Handle(GetFeedRequest request, CancellationTo return new GetFeedResponse { - ArticlesView = result.DataResult + ArticlesView = result.DataResult, }; } } \ No newline at end of file diff --git a/Conduit/Conduit.Web/Articles/Handlers/GetFeedResponse.cs b/Conduit/Conduit.Web/Articles/Handlers/GetFeedResponse.cs index 525a7d512e..8592a520a3 100644 --- a/Conduit/Conduit.Web/Articles/Handlers/GetFeedResponse.cs +++ b/Conduit/Conduit.Web/Articles/Handlers/GetFeedResponse.cs @@ -6,6 +6,6 @@ namespace Conduit.Web.Articles.Handlers; public class GetFeedResponse { public List ValidationErrors { get; set; } - public List ArticlesView { get; set; } + public ArticlesViewModel ArticlesView { get; set; } public bool IsFailure { get; set; } } \ No newline at end of file diff --git a/Conduit/Conduit.Web/Articles/Services/ArticlesDataService.cs b/Conduit/Conduit.Web/Articles/Services/ArticlesDataService.cs index a792405ebc..235400bead 100644 --- a/Conduit/Conduit.Web/Articles/Services/ArticlesDataService.cs +++ b/Conduit/Conduit.Web/Articles/Services/ArticlesDataService.cs @@ -24,7 +24,7 @@ public interface IArticlesDataService Task UpdateArticle(Article newArticle); Task> DeleteArticle(string slug); Task IsArticleAuthor(string slug, string username); - Task>> GetArticles(GetArticlesSpec request); + Task> GetArticles(GetArticlesSpec request); } public class ArticlesDataService : IArticlesDataService @@ -294,7 +294,7 @@ private async Task EnsureFavoritesDocumentExists(string username) } } - public async Task>> GetArticles(GetArticlesSpec spec) + public async Task> GetArticles(GetArticlesSpec spec) { var collection = await _articlesCollectionProvider.GetCollectionAsync(); var cluster = collection.Scope.Bucket.Cluster; @@ -362,7 +362,8 @@ public async Task>> GetArticles(GetArti u.bio, u.image, {authenticatedFollowProjection} - }} AS author + }} AS author, + COUNT(*) OVER() AS articlesCount FROM `{bucketName}`.`{scopeName}`.`Articles` a JOIN `{bucketName}`.`{scopeName}`.`Users` u ON a.authorUsername = META(u).id @@ -386,7 +387,7 @@ ORDER BY COALESCE(a.updatedAt, a.createdAt) DESC OFFSET $offset "; - var result = await cluster.QueryAsync(sql, options => + var result = await cluster.QueryAsync(sql, options => { options.Parameter("loggedInUsername", spec.Username); options.Parameter("favoritedBy", spec.FavoritedByUsername); @@ -397,6 +398,14 @@ ORDER BY COALESCE(a.updatedAt, a.createdAt) DESC options.ScanConsistency(ScanConsistency); }); - return new DataServiceResult>(await result.Rows.ToListAsync(), DataResultStatus.Ok); + // this next part is a little hacky, but it works + // the goal is to get ArticlesCount (returned with each record) + // separated + var results = await result.Rows.ToListAsync(); + var articlesResults = new ArticlesViewModel(); + articlesResults.ArticlesCount = results.FirstOrDefault()?.ArticlesCount ?? 0; + articlesResults.Articles = results.Cast().ToList(); + + return new DataServiceResult(articlesResults, DataResultStatus.Ok); } } \ No newline at end of file diff --git a/Conduit/Conduit.Web/Articles/ViewModels/ArticleViewModel.cs b/Conduit/Conduit.Web/Articles/ViewModels/ArticleViewModel.cs index ad3dc028ee..4255dbccc0 100644 --- a/Conduit/Conduit.Web/Articles/ViewModels/ArticleViewModel.cs +++ b/Conduit/Conduit.Web/Articles/ViewModels/ArticleViewModel.cs @@ -2,6 +2,12 @@ namespace Conduit.Web.Articles.ViewModels; +public class ArticlesViewModel +{ + public int ArticlesCount { get; set; } + public List Articles { get; set; } +} + public class ArticleViewModel { public string Slug { get; set; } @@ -14,4 +20,9 @@ public class ArticleViewModel public bool Favorited { get; set; } public int FavoritesCount { get; set; } public ProfileViewModel Author { get; set; } +} + +public class ArticleViewModelWithCount : ArticleViewModel +{ + public int ArticlesCount { get; set; } } \ No newline at end of file diff --git a/Conduit/Conduit.Web/DataAccess/Queries/ListArticles.n1qlnb b/Conduit/Conduit.Web/DataAccess/Queries/ListArticles.n1qlnb index cd09b11075..38da0ddbb5 100644 --- a/Conduit/Conduit.Web/DataAccess/Queries/ListArticles.n1qlnb +++ b/Conduit/Conduit.Web/DataAccess/Queries/ListArticles.n1qlnb @@ -1 +1 @@ -{"cells":[{"kind":2,"language":"SQL++","value":"SELECT \r\n a.slug,\r\n a.title,\r\n a.description,\r\n a.body,\r\n a.tagList,\r\n a.createdAt,\r\n a.updatedAt,\r\n false AS favorited,\r\n a.favoritesCount,\r\n {\r\n \"username\": META(u).id,\r\n u.bio,\r\n u.image,\r\n \"following\": true\r\n } AS author\r\n\r\nFROM Conduit._default.Articles a\r\nJOIN Conduit._default.Users u ON a.authorUsername = META(u).id\r\n\r\n\r\n/* these next lines are only for authenticated users */\r\n/* usernames need parameterized */\r\nLEFT JOIN Conduit._default.Favorites favCurrent ON META(favCurrent).id = (\"mgroves\" || \":favorites\")\r\nLEFT JOIN Conduit._default.`Follows` fol ON META(fol).id = (\"mgroves\" || \"::follows\")\r\n\r\n/* for use with optional filter */\r\n/* username need parameterized */\r\nLEFT JOIN Conduit._default.Favorites favFilter ON META(favFilter).id = (\"jake\" || \"::favorites\")\r\n\r\n/* convenience variable for getting the ArticleKey from slug */\r\nLET articleKey = SPLIT(a.slug, \"::\")[1]\r\n\r\nWHERE 1=1\r\n /* optional filters */\r\n AND ARRAY_CONTAINS(a.tagList, \"cruising\")\r\n AND a.authorUsername = 'user-u4tjaxvr.2lw'\r\n AND ARRAY_CONTAINS(favFilter, articleKey)\r\n AND ARRAY_CONTAINS(fol, a.authorUsername) /* used for Feed endpoint */\r\n\r\nORDER BY COALESCE(a.updatedAt, a.createdAt) DESC\r\n\r\n/* needs parameterized */\r\nLIMIT 20\r\nOFFSET 0\r\n"}]} \ No newline at end of file +{"cells":[{"kind":2,"language":"SQL++","value":"SELECT \r\n a.slug,\r\n a.title,\r\n a.description,\r\n a.body,\r\n a.tagList,\r\n a.createdAt,\r\n a.updatedAt,\r\n false AS favorited,\r\n a.favoritesCount,\r\n {\r\n \"username\": META(u).id,\r\n u.bio,\r\n u.image,\r\n \"following\": true\r\n } AS author,\r\n COUNT(*) OVER() AS articlesCount\r\n\r\nFROM Conduit._default.Articles a\r\nJOIN Conduit._default.Users u ON a.authorUsername = META(u).id\r\n\r\n\r\n/* these next lines are only for authenticated users */\r\n/* usernames need parameterized */\r\nLEFT JOIN Conduit._default.Favorites favCurrent ON META(favCurrent).id = (\"mgroves\" || \":favorites\")\r\nLEFT JOIN Conduit._default.`Follows` fol ON META(fol).id = (\"mgroves\" || \"::follows\")\r\n\r\n/* for use with optional filter */\r\n/* username need parameterized */\r\nLEFT JOIN Conduit._default.Favorites favFilter ON META(favFilter).id = (\"jake\" || \"::favorites\")\r\n\r\n/* convenience variable for getting the ArticleKey from slug */\r\nLET articleKey = SPLIT(a.slug, \"::\")[1]\r\n\r\nWHERE 1=1\r\n /* optional filters */\r\n /*AND ARRAY_CONTAINS(a.tagList, \"cruising\")\r\n AND a.authorUsername = 'user-u4tjaxvr.2lw'\r\n AND ARRAY_CONTAINS(favFilter, articleKey)\r\n AND ARRAY_CONTAINS(fol, a.authorUsername)*/ /* used for Feed endpoint */\r\n\r\nORDER BY COALESCE(a.updatedAt, a.createdAt) DESC\r\n\r\n/* needs parameterized */\r\n/*LIMIT 20\r\nOFFSET 0*/\r\n"}]} \ No newline at end of file