Skip to content

Commit

Permalink
#35 added count via pagination with total count
Browse files Browse the repository at this point in the history
  • Loading branch information
mgroves committed Sep 28, 2023
1 parent 772e36d commit b4dffd5
Show file tree
Hide file tree
Showing 9 changed files with 46 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -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]
Expand All @@ -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]
Expand All @@ -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)]
Expand All @@ -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]
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public async Task Setup()
{
_mockArticleDataService = new Mock<IArticlesDataService>();
_mockArticleDataService.Setup(m => m.GetArticles(It.IsAny<GetArticlesSpec>()))
.ReturnsAsync(new DataServiceResult<List<ArticleViewModel>>(new List<ArticleViewModel>(), DataResultStatus.Ok));
.ReturnsAsync(new DataServiceResult<ArticlesViewModel>(new ArticlesViewModel(), DataResultStatus.Ok));

var validator = new GetArticlesRequestValidator();
_handler = new GetArticlesHandler(validator, _mockArticleDataService.Object);
Expand Down Expand Up @@ -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<GetArticlesSpec>()))
.ReturnsAsync(new DataServiceResult<List<ArticleViewModel>>(null, DataResultStatus.Error));
.ReturnsAsync(new DataServiceResult<ArticlesViewModel>(null, DataResultStatus.Error));

// act
var result = await _handler.Handle(request, CancellationToken.None);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ public async Task<IActionResult> 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 });
}

/// <summary>
Expand Down Expand Up @@ -298,6 +298,6 @@ public async Task<IActionResult> 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 });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ namespace Conduit.Web.Articles.Handlers;
public class GetArticlesResponse
{
public List<ValidationFailure> ValidationErrors { get; set; }
public List<ArticleViewModel> ArticlesView { get; set; }
public ArticlesViewModel ArticlesView { get; set; }
public bool IsFailure { get; set; }
}
2 changes: 1 addition & 1 deletion Conduit/Conduit.Web/Articles/Handlers/GetFeedHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public async Task<GetFeedResponse> Handle(GetFeedRequest request, CancellationTo

return new GetFeedResponse
{
ArticlesView = result.DataResult
ArticlesView = result.DataResult,
};
}
}
2 changes: 1 addition & 1 deletion Conduit/Conduit.Web/Articles/Handlers/GetFeedResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ namespace Conduit.Web.Articles.Handlers;
public class GetFeedResponse
{
public List<ValidationFailure> ValidationErrors { get; set; }
public List<ArticleViewModel> ArticlesView { get; set; }
public ArticlesViewModel ArticlesView { get; set; }
public bool IsFailure { get; set; }
}
19 changes: 14 additions & 5 deletions Conduit/Conduit.Web/Articles/Services/ArticlesDataService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public interface IArticlesDataService
Task<bool> UpdateArticle(Article newArticle);
Task<DataServiceResult<string>> DeleteArticle(string slug);
Task<bool> IsArticleAuthor(string slug, string username);
Task<DataServiceResult<List<ArticleViewModel>>> GetArticles(GetArticlesSpec request);
Task<DataServiceResult<ArticlesViewModel>> GetArticles(GetArticlesSpec request);
}

public class ArticlesDataService : IArticlesDataService
Expand Down Expand Up @@ -294,7 +294,7 @@ private async Task EnsureFavoritesDocumentExists(string username)
}
}

public async Task<DataServiceResult<List<ArticleViewModel>>> GetArticles(GetArticlesSpec spec)
public async Task<DataServiceResult<ArticlesViewModel>> GetArticles(GetArticlesSpec spec)
{
var collection = await _articlesCollectionProvider.GetCollectionAsync();
var cluster = collection.Scope.Bucket.Cluster;
Expand Down Expand Up @@ -362,7 +362,8 @@ public async Task<DataServiceResult<List<ArticleViewModel>>> 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
Expand All @@ -386,7 +387,7 @@ ORDER BY COALESCE(a.updatedAt, a.createdAt) DESC
OFFSET $offset
";

var result = await cluster.QueryAsync<ArticleViewModel>(sql, options =>
var result = await cluster.QueryAsync<ArticleViewModelWithCount>(sql, options =>
{
options.Parameter("loggedInUsername", spec.Username);
options.Parameter("favoritedBy", spec.FavoritedByUsername);
Expand All @@ -397,6 +398,14 @@ ORDER BY COALESCE(a.updatedAt, a.createdAt) DESC
options.ScanConsistency(ScanConsistency);
});

return new DataServiceResult<List<ArticleViewModel>>(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<ArticleViewModel>().ToList();

return new DataServiceResult<ArticlesViewModel>(articlesResults, DataResultStatus.Ok);
}
}
11 changes: 11 additions & 0 deletions Conduit/Conduit.Web/Articles/ViewModels/ArticleViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

namespace Conduit.Web.Articles.ViewModels;

public class ArticlesViewModel
{
public int ArticlesCount { get; set; }
public List<ArticleViewModel> Articles { get; set; }
}

public class ArticleViewModel
{
public string Slug { get; set; }
Expand All @@ -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; }
}
2 changes: 1 addition & 1 deletion Conduit/Conduit.Web/DataAccess/Queries/ListArticles.n1qlnb
Original file line number Diff line number Diff line change
@@ -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"}]}
{"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"}]}

0 comments on commit b4dffd5

Please sign in to comment.