Skip to content

Commit

Permalink
Added fluent paging helpers (#4277)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelstaib authored Sep 27, 2021
1 parent 95a9e03 commit d356b2a
Show file tree
Hide file tree
Showing 6 changed files with 315 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using HotChocolate.Resolvers;

namespace HotChocolate.Types.Pagination.Extensions
{
Expand Down Expand Up @@ -76,5 +78,106 @@ public static ValueTask<Connection<TEntity>> ApplyCursorPaginationAsync<TEntity>
query,
arguments,
cancellationToken);

/// <summary>
/// Applies the cursor pagination algorithm to the <paramref name="query"/>.
/// </summary>
/// <param name="query">
/// The query on which the the cursor pagination algorithm shall be applied to.
/// </param>
/// <param name="context">
/// The field resolver context.
/// </param>
/// <param name="defaultPageSize">
/// The default page size if no boundaries are set.
/// </param>
/// <param name="totalCount">
/// The total count if already known.
/// </param>
/// <param name="cancellationToken">
/// The cancellation token.
/// </param>
/// <typeparam name="TEntity">
/// The entity type.
/// </typeparam>
/// <returns>
/// Returns a connection instance that represents the result of applying the
/// cursor paging algorithm to the provided <paramref name="query"/>.
/// </returns>
public static ValueTask<Connection<TEntity>> ApplyCursorPaginationAsync<TEntity>(
this IQueryable<TEntity> query,
IResolverContext context,
int? defaultPageSize = null,
int? totalCount = null,
CancellationToken cancellationToken = default)
{
if (query is null)
{
throw new ArgumentNullException(nameof(query));
}

if (context is null)
{
throw new ArgumentNullException(nameof(context));
}

var first = context.ArgumentValue<int?>(CursorPagingArgumentNames.First);
var last = context.ArgumentValue<int?>(CursorPagingArgumentNames.Last);

if (first is null && last is null)
{
first = defaultPageSize;
}

var arguments = new CursorPagingArguments(
first,
last,
context.ArgumentValue<string?>(CursorPagingArgumentNames.After),
context.ArgumentValue<string?>(CursorPagingArgumentNames.Before));

return QueryableCursorPagination<TEntity>.Instance.ApplyPaginationAsync(
query,
arguments,
totalCount,
cancellationToken);
}

/// <summary>
/// Applies the cursor pagination algorithm to the <paramref name="enumerable"/>.
/// </summary>
/// <param name="enumerable">
/// The enumerable on which the the cursor pagination algorithm shall be applied to.
/// </param>
/// <param name="context">
/// The field resolver context.
/// </param>
/// <param name="defaultPageSize">
/// The default page size if no boundaries are set.
/// </param>
/// <param name="totalCount">
/// The total count if already known.
/// </param>
/// <param name="cancellationToken">
/// The cancellation token.
/// </param>
/// <typeparam name="TEntity">
/// The entity type.
/// </typeparam>
/// <returns>
/// Returns a connection instance that represents the result of applying the
/// cursor paging algorithm to the provided <paramref name="enumerable"/>.
/// </returns>
public static ValueTask<Connection<TEntity>> ApplyCursorPaginationAsync<TEntity>(
this System.Collections.Generic.IEnumerable<TEntity> enumerable,
IResolverContext context,
int? defaultPageSize = null,
int? totalCount = null,
CancellationToken cancellationToken = default)
=> ApplyCursorPaginationAsync(
enumerable.AsQueryable(),
context,
defaultPageSize,
totalCount,
cancellationToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using HotChocolate.Execution;
using HotChocolate.Resolvers;
using HotChocolate.Tests;
using HotChocolate.Types.Pagination.Extensions;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Snapshooter.Xunit;
using Xunit;

namespace HotChocolate.Types.Pagination
{
public class CursorPagingQueryableExtensionsTests
{
[Fact]
public async Task Queryable_Query_Is_Null()
{
var mock = new Mock<IResolverContext>();

async Task Fail()
=> await default(IQueryable<Person>)!.ApplyCursorPaginationAsync(mock.Object);

await Assert.ThrowsAsync<ArgumentNullException>(Fail);
}

[Fact]
public async Task Queryable_Context_Is_Null()
{
var mock = new Mock<IQueryable<Person>>();

async Task Fail()
=> await mock.Object.ApplyCursorPaginationAsync(default(IResolverContext)!);

await Assert.ThrowsAsync<ArgumentNullException>(Fail);
}

[Fact]
public async Task Queryable_ApplyCursorPaginationAsync_No_Boundaries()
{
Snapshot.FullName();

await new ServiceCollection()
.AddGraphQL()
.AddQueryType<Query>()
.ExecuteRequestAsync("{ persons { nodes { name } } }")
.MatchSnapshotAsync();
}

[Fact]
public async Task Queryable_ApplyCursorPaginationAsync_First_1()
{
Snapshot.FullName();

await new ServiceCollection()
.AddGraphQL()
.AddQueryType<Query>()
.ExecuteRequestAsync("{ persons(first: 1) { nodes { name } } }")
.MatchSnapshotAsync();
}

[Fact]
public async Task Enumerable_Query_Is_Null()
{
var mock = new Mock<IResolverContext>();

async Task Fail()
=> await default(IEnumerable<Person>)!.ApplyCursorPaginationAsync(mock.Object);

await Assert.ThrowsAsync<ArgumentNullException>(Fail);
}

[Fact]
public async Task Enumerable_Context_Is_Null()
{
var mock = new Mock<IEnumerable<Person>>();

async Task Fail()
=> await mock.Object.ApplyCursorPaginationAsync(default(IResolverContext)!);

await Assert.ThrowsAsync<ArgumentNullException>(Fail);
}

[Fact]
public async Task Enumerable_ApplyCursorPaginationAsync_No_Boundaries()
{
Snapshot.FullName();

await new ServiceCollection()
.AddGraphQL()
.AddQueryType<QueryEnumerable>()
.ExecuteRequestAsync("{ persons { nodes { name } } }")
.MatchSnapshotAsync();
}

[Fact]
public async Task Enumerable_ApplyCursorPaginationAsync_First_1()
{
Snapshot.FullName();

await new ServiceCollection()
.AddGraphQL()
.AddQueryType<QueryEnumerable>()
.ExecuteRequestAsync("{ persons(first: 1) { nodes { name } } }")
.MatchSnapshotAsync();
}

public class Query
{
[UsePaging]
public async Task<Connection<Person>> GetPersons(
IResolverContext context,
CancellationToken cancellationToken)
{
var list = new Person[]
{
new() { Name = "Foo" },
new() { Name = "Bar" },
new() { Name = "Baz" },
new() { Name = "Qux" }
};

return await list.AsQueryable().ApplyCursorPaginationAsync(
context,
defaultPageSize: 2,
totalCount: list.Length,
cancellationToken: cancellationToken);
}
}

public class QueryEnumerable
{
[UsePaging]
public async Task<Connection<Person>> GetPersons(
IResolverContext context,
CancellationToken cancellationToken)
{
var list = new Person[]
{
new() { Name = "Foo" },
new() { Name = "Bar" },
new() { Name = "Baz" },
new() { Name = "Qux" }
};

return await list.ApplyCursorPaginationAsync(
context,
defaultPageSize: 2,
totalCount: list.Length,
cancellationToken: cancellationToken);
}
}

public class Person
{
public string Name { get; set; }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"data": {
"persons": {
"nodes": [
{
"name": "Foo"
}
]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"data": {
"persons": {
"nodes": [
{
"name": "Foo"
},
{
"name": "Bar"
}
]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"data": {
"persons": {
"nodes": [
{
"name": "Foo"
}
]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"data": {
"persons": {
"nodes": [
{
"name": "Foo"
},
{
"name": "Bar"
}
]
}
}
}

0 comments on commit d356b2a

Please sign in to comment.