Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Connection to PersonQuery to page the result set. #37

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Dapper.GraphQL.Test/Dapper.GraphQL.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
<FileVersion>0.4.2.0</FileVersion>

<Version>0.4.2-beta</Version>

<LangVersion>latest</LangVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
65 changes: 65 additions & 0 deletions Dapper.GraphQL.Test/GraphQL/Cursor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;

namespace Dapper.GraphQL.Test.GraphQL
{
public static class Cursor
{
public static T FromCursor<T>(string cursor)
{
if (string.IsNullOrEmpty(cursor))
{
return default;
}

string decodedValue;
try
{
decodedValue = Base64Decode(cursor);
}
catch (FormatException)
{
return default;
}

return (T)Convert.ChangeType(decodedValue, Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T), CultureInfo.InvariantCulture);
}

public static (string firstCursor, string lastCursor) GetFirstAndLastCursor<TItem, TCursor>(
IEnumerable<TItem> enumerable,
Func<TItem, TCursor> getCursorProperty)
{
if (getCursorProperty == null)
{
throw new ArgumentNullException(nameof(getCursorProperty));
}

if (enumerable == null || enumerable.Count() == 0)
{
return (null, null);
}

var firstCursor = ToCursor(getCursorProperty(enumerable.First()));
var lastCursor = ToCursor(getCursorProperty(enumerable.Last()));

return (firstCursor, lastCursor);
}

public static string ToCursor<T>(T value)
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}

return Base64Encode(value.ToString());
}

private static string Base64Decode(string value) => Encoding.UTF8.GetString(Convert.FromBase64String(value));

private static string Base64Encode(string value) => Convert.ToBase64String(Encoding.UTF8.GetBytes(value));
}
}
103 changes: 97 additions & 6 deletions Dapper.GraphQL.Test/GraphQL/PersonQuery.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
using Dapper.GraphQL.Test.EntityMappers;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Dapper.GraphQL.Test.EntityMappers;
using Dapper.GraphQL.Test.Models;
using GraphQL.Builders;
using GraphQL.Types;
using GraphQL.Types.Relay.DataObjects;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Data;
using System.Data.Common;
using System.Linq;
using Dapper.GraphQL.Test.Repositories;


namespace Dapper.GraphQL.Test.GraphQL
{
public class PersonQuery :
ObjectGraphType
{
private const int MaxPageSize = 10;
private readonly IPersonRepository _personRepository;

public PersonQuery(
IQueryBuilder<Person> personQueryBuilder,
IServiceProvider serviceProvider)
IServiceProvider serviceProvider,
IPersonRepository personRepository)
{
_personRepository = personRepository;

Field<ListGraphType<PersonType>>(
"people",
description: "A list of people.",
Expand Down Expand Up @@ -99,6 +111,85 @@ public PersonQuery(
}
}
);

Connection<PersonType>()
.Name("personConnection")
.Description("Gets pages of Person objects.")
// Enable the last and before arguments to do paging in reverse.
.Bidirectional()
// Set the maximum size of a page, use .ReturnAll() to set no maximum size.
.PageSize(MaxPageSize)
.ResolveAsync(context => ResolveConnection(context, personQueryBuilder));
}

private async Task<object> ResolveConnection(ResolveConnectionContext<object> context, IQueryBuilder<Person> personQueryBuilder)
{
_personRepository.Context = context;

var first = context.First;
var afterCursor = Cursor.FromCursor<DateTime?>(context.After);
var last = context.Last;
var beforeCursor = Cursor.FromCursor<DateTime?>(context.Before);
var cancellationToken = context.CancellationToken;

var getPersonTask = GetPeople(first, afterCursor, last, beforeCursor, cancellationToken);
var getHasNextPageTask = GetHasNextPage(first, afterCursor, cancellationToken);
var getHasPreviousPageTask = GetHasPreviousPage(last, beforeCursor, cancellationToken);
var totalCountTask = _personRepository.GetTotalCount(cancellationToken);

await Task.WhenAll(getPersonTask, getHasNextPageTask, getHasPreviousPageTask, totalCountTask);
var people = getPersonTask.Result;
var hasNextPage = getHasNextPageTask.Result;
var hasPreviousPage = getHasPreviousPageTask.Result;
var totalCount = totalCountTask.Result;
var (firstCursor, lastCursor) = Cursor.GetFirstAndLastCursor(people, x => x.CreateDate);

return new Connection<Person>()
{
Edges = people
.Select(x =>
new Edge<Person>()
{
Cursor = Cursor.ToCursor(x.CreateDate),
Node = x
})
.ToList(),
PageInfo = new PageInfo()
{
HasNextPage = hasNextPage,
HasPreviousPage = hasPreviousPage,
StartCursor = firstCursor,
EndCursor = lastCursor,
},
TotalCount = totalCount,
};
}

private async Task<bool> GetHasNextPage(
int? first,
DateTime? afterCursor,
CancellationToken cancellationToken)
{
return first.HasValue ? await _personRepository.GetHasNextPage(first, afterCursor, cancellationToken) : false;
}

private async Task<bool> GetHasPreviousPage(
int? last,
DateTime? beforeCursor,
CancellationToken cancellationToken)
{
return last.HasValue ? await _personRepository.GetHasPreviousPage(last, beforeCursor, cancellationToken) : false;
}

private Task<List<Person>> GetPeople(
int? first,
DateTime? afterCursor,
int? last,
DateTime? beforeCursor,
CancellationToken cancellationToken)
{
return first.HasValue ? _personRepository.GetPeople(first, afterCursor, cancellationToken) :
_personRepository.GetPeopleReversed(last, beforeCursor, cancellationToken);
}
}
}
55 changes: 55 additions & 0 deletions Dapper.GraphQL.Test/GraphQLTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -366,5 +366,60 @@ public async Task SimplePersonInsert()

Assert.True(fixture.JsonEquals(expectedJson, json));
}

[Fact(DisplayName = "People connection query should succeed")]
public async Task PeopleConnectionQuery()
{
var json = await fixture.QueryGraphQLAsync(@"
query {
personConnection(first:2) {
edges {
node {
firstName
lastName
}
cursor
}
pageInfo {
hasNextPage
hasPreviousPage
endCursor
startCursor
}
}
}");

var expectedJson = @"
{
'data': {
'personConnection': {
'edges': [
{
'node': {
'firstName': 'Hyrum',
'lastName': 'Clyde'
},
'cursor': 'MS8xLzIwMTkgMTI6MDA6MDAgQU0='
},
{
'node': {
'firstName': 'Doug',
'lastName': 'Day'
},
'cursor': 'MS8yLzIwMTkgMTI6MDA6MDAgQU0='
}
],
'pageInfo': {
'hasNextPage': true,
'hasPreviousPage': false,
'endCursor': 'MS8yLzIwMTkgMTI6MDA6MDAgQU0=',
'startCursor': 'MS8xLzIwMTkgMTI6MDA6MDAgQU0='
}
}
}
}";

Assert.True(fixture.JsonEquals(expectedJson, json));
}
}
}
1 change: 1 addition & 0 deletions Dapper.GraphQL.Test/Models/Person.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public class Person
public int MergedToPersonId { get; set; }
public IList<Phone> Phones { get; set; }
public Person Supervisor { get; set; }
public DateTime CreateDate { get; set; }

public Person()
{
Expand Down
7 changes: 6 additions & 1 deletion Dapper.GraphQL.Test/QueryBuilders/PersonQueryBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ public SqlQueryContext Build(SqlQueryContext query, IHaveSelectionSet context, s
query.Select($"{alias}.Id", $"{alias}.MergedToPersonId");
query.SplitOn<Person>("Id");

var fields = context.GetSelectedFields();
var fields = QueryBuilderHelper.CollectFields(context.SelectionSet);

if (QueryBuilderHelper.IsConnection(context.SelectionSet))
{
query.Select($"{alias}.CreateDate");
}

if (fields.ContainsKey("firstName"))
{
Expand Down
58 changes: 58 additions & 0 deletions Dapper.GraphQL.Test/QueryBuilders/QuerybuilderHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System.Collections.Generic;
using GraphQL.Language.AST;
using System.Linq;
using GraphQL;


namespace Dapper.GraphQL.Test.QueryBuilders
{
public static class QueryBuilderHelper
{
public static Dictionary<string, Field> CollectFields(SelectionSet selectionSet)
{
return CollectFields(selectionSet, Fields.Empty());
}

private static Fields CollectFields(SelectionSet selectionSet, Fields fields)
{
List<string> skipList = new List<string> { "edges", "node", "cursor" };
selectionSet?.Selections.Apply(selection =>
{
if (selection is Field field)
{
if (!skipList.Exists(name => name.ToLower().Equals(field.Name)))
{
fields.Add(field);
}

CollectFields(field.SelectionSet, fields);
}
});

return fields;
}

public static bool IsConnection(SelectionSet selectionSet)
{
return IsConnection(selectionSet, new Dictionary<string, Field>());
}

public static bool IsConnection(SelectionSet selectionSet, Dictionary<string, Field> fields)
{
selectionSet?.Selections.Apply(selection =>
{
if (selection is Field field)
{
if (field.Name == "edges")
{
fields.Add(field.Name, field);
}

IsConnection(field.SelectionSet, fields);
}
});

return fields.Any();
}
}
}
Loading