Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ private SqlQueryStructure(
AddColumnsForEndCursor();
}

if (PaginationMetadata.RequestedHasNextPage)
if (PaginationMetadata.RequestedHasNextPage || PaginationMetadata.RequestedEndCursor)
{
_limit++;
}
Expand Down
39 changes: 24 additions & 15 deletions src/Core/Resolvers/SqlPaginationUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,26 +62,33 @@ private static JsonObject CreatePaginationConnection(JsonElement root, Paginatio
root = document.RootElement.Clone();
}

IEnumerable<JsonElement> rootEnumerated = root.EnumerateArray();
// If the request includes either hasNextPage or endCursor then to correctly return those
// values we need to determine the correct pagination logic
bool isPaginationRequested = paginationMetadata.RequestedHasNextPage || paginationMetadata.RequestedEndCursor;

IEnumerable<JsonElement> rootEnumerated = root.EnumerateArray();
int returnedElementCount = rootEnumerated.Count();
bool hasExtraElement = false;
if (paginationMetadata.RequestedHasNextPage)
{
// check if the number of elements requested is successfully returned
// structure.Limit() is first + 1 for paginated queries where hasNextPage is requested
hasExtraElement = rootEnumerated.Count() == paginationMetadata.Structure!.Limit();

// add hasNextPage to connection elements
connection.Add(QueryBuilder.HAS_NEXT_PAGE_FIELD_NAME, hasExtraElement);

if (isPaginationRequested)
{
// structure.Limit() is first + 1 for paginated queries where hasNextPage or endCursor is requested
hasExtraElement = returnedElementCount == paginationMetadata.Structure!.Limit();
if (hasExtraElement)
{
// remove the last element
// In a pagination scenario where we have an extra element, this element
// must be removed since it was only used to determine if there are additional
// records after those requested.
rootEnumerated = rootEnumerated.Take(rootEnumerated.Count() - 1);
--returnedElementCount;
}
}

int returnedElemNo = rootEnumerated.Count();
if (paginationMetadata.RequestedHasNextPage)
{
// add hasNextPage to connection elements
connection.Add(QueryBuilder.HAS_NEXT_PAGE_FIELD_NAME, hasExtraElement);
}

if (paginationMetadata.RequestedItems)
{
Expand All @@ -100,11 +107,13 @@ private static JsonObject CreatePaginationConnection(JsonElement root, Paginatio

if (paginationMetadata.RequestedEndCursor)
{
// parse *Connection.endCursor if there are no elements
// if no after is added, but it has been requested HotChocolate will report it as null
if (returnedElemNo > 0)
// Note: if we do not add endCursor to the connection but it was in the request, its value will
// automatically be populated as null.
// Need to validate we have an extra element, because otherwise there is no next page
// and endCursor should be left as null.
if (returnedElementCount > 0 && hasExtraElement)
{
JsonElement lastElemInRoot = rootEnumerated.ElementAtOrDefault(returnedElemNo - 1);
JsonElement lastElemInRoot = rootEnumerated.ElementAtOrDefault(returnedElementCount - 1);
connection.Add(QueryBuilder.PAGINATION_TOKEN_FIELD_NAME,
MakeCursorFromJsonElement(
lastElemInRoot,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ public async Task RequestNoParamFullConnection()
""title"": ""Before Sunset""
}
],
""endCursor"": """ + SqlPaginationUtil.Base64Encode($"[{{\"EntityName\":\"Book\",\"FieldName\":\"id\",\"FieldValue\":14,\"Direction\":0}}]") + @""",
""endCursor"": null,
""hasNextPage"": false
}";

Expand Down Expand Up @@ -196,7 +196,8 @@ public async virtual Task RequestAfterTokenOnly(
object afterValue,
object endCursorValue,
object afterIdValue,
object endCursorIdValue)
object endCursorIdValue,
bool isLastPage)
{
string graphQLQueryName = "supportedTypes";
string after;
Expand All @@ -212,14 +213,16 @@ public async virtual Task RequestAfterTokenOnly(
}

string graphQLQuery = @"{
supportedTypes(first: 3," + $"after: \"{after}\" " +
supportedTypes(first: 2," + $"after: \"{after}\" " +
$"orderBy: {{ {exposedFieldName} : ASC }} )" + @"{
endCursor
}
}";

JsonElement root = await ExecuteGraphQLRequestAsync(graphQLQuery, graphQLQueryName, isAuthenticated: false);
string actual = SqlPaginationUtil.Base64Decode(root.GetProperty(QueryBuilder.PAGINATION_TOKEN_FIELD_NAME).GetString());
string actual = root.GetProperty(QueryBuilder.PAGINATION_TOKEN_FIELD_NAME).GetString();
// Decode if not null
actual = string.IsNullOrEmpty(actual) ? "null" : SqlPaginationUtil.Base64Decode(root.GetProperty(QueryBuilder.PAGINATION_TOKEN_FIELD_NAME).GetString());
string expected;
if ("typeid".Equals(exposedFieldName))
{
Expand All @@ -231,6 +234,11 @@ public async virtual Task RequestAfterTokenOnly(
$"{{\"EntityName\":\"SupportedType\",\"FieldName\":\"typeid\",\"FieldValue\":{endCursorIdValue},\"Direction\":0}}]";
}

if (isLastPage)
{
expected = "null";
}

SqlTestHelper.PerformTestEqualJsonStrings(expected, actual.ToString());
}

Expand Down Expand Up @@ -350,7 +358,7 @@ public async Task RequestNestedPaginationQueries()
""title"": ""US history in a nutshell""
}
],
""endCursor"": """ + SqlPaginationUtil.Base64Encode($"[{{\"EntityName\":\"Book\",\"FieldName\":\"id\",\"FieldValue\":4,\"Direction\":0}}]") + @""",
""endCursor"": null,
""hasNextPage"": false
}
}
Expand Down Expand Up @@ -573,8 +581,6 @@ public async Task PaginateCompositePkTable()
}
}";

after = SqlPaginationUtil.Base64Encode($"[{{\"EntityName\":\"Review\",\"FieldName\":\"book_id\",\"FieldValue\":1,\"Direction\":0}}," +
$"{{\"EntityName\":\"Review\",\"FieldName\":\"id\",\"FieldValue\":569,\"Direction\":0}}]");
JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLQuery, graphQLQueryName, isAuthenticated: false);
string expected = @"{
""items"": [
Expand All @@ -588,7 +594,7 @@ public async Task PaginateCompositePkTable()
}
],
""hasNextPage"": false,
""endCursor"": """ + after + @"""
""endCursor"": null
}";

SqlTestHelper.PerformTestEqualJsonStrings(expected, actual.ToString());
Expand Down Expand Up @@ -625,8 +631,7 @@ public async Task PaginationWithFilterArgument()
""publisher_id"": 2345
}
],
""endCursor"": """ +
SqlPaginationUtil.Base64Encode($"[{{\"EntityName\":\"Book\",\"FieldName\":\"id\",\"FieldValue\":4,\"Direction\":0}}]") + @""",
""endCursor"": null,
""hasNextPage"": false
}";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,55 +25,92 @@ public static async Task SetupAsync(TestContext context)

/// <inheritdoc />
[DataTestMethod]
[DataRow("typeid", 1, 4, "", "",
[DataRow("typeid", 1, 3, "", "", false,
DisplayName = "Test after token for primary key with mapped name.")]
[DataRow("byte_types", 0, 255, 2, 4, DisplayName = "Test after token for byte values.")]
[DataRow("short_types", -32768, 32767, 3, 4, DisplayName = "Test after token for short values.")]
[DataRow("int_types", -2147483648, 2147483647, 3, 4,
[DataRow("typeid", 4, 6, "", "", true,
DisplayName = "Test after token for primary key with mapped name for last page.")]
[DataRow("byte_types", 0, 1, 2, 1, false, DisplayName = "Test after token for byte values.")]
[DataRow("byte_types", 1, "", 1, "", true, DisplayName = "Test after token for byte values for last page.")]
[DataRow("short_types", -32768, 1, 3, 1, false, DisplayName = "Test after token for short values.")]
[DataRow("short_types", 1, "", 1, "", true, DisplayName = "Test after token for short values for last page.")]
[DataRow("int_types", -2147483648, 1, 3, 1, false,
DisplayName = "Test after token for int values.")]
[DataRow("long_types", -9223372036854775808, 9.223372036854776E+18, 3, 4,
[DataRow("int_types", 1, "", 1, "", true,
DisplayName = "Test after token for int values for last page.")]
[DataRow("long_types", -9223372036854775808, 1, 3, 1, false,
DisplayName = "Test after token for long values.")]
[DataRow("string_types", "\"\"", "\"null\"", 1, 4,
[DataRow("long_types", 1, "", 1, "", true,
DisplayName = "Test after token for long values for last page.")]
[DataRow("string_types", "\"\"", "\"null\"", 1, 3, false,
DisplayName = "Test after token for string values.")]
[DataRow("single_types", -3.39E38, 3.4E38, 3, 4,
[DataRow("string_types", "null", "", 3, "", true,
DisplayName = "Test after token for string values for last page.")]
[DataRow("single_types", -3.39E38, .33000001, 3, 1, false,
DisplayName = "Test after token for single values.")]
[DataRow("float_types", -1.7E308, 1.7E308, 3, 4,
[DataRow("single_types", .33, "", 1, "", true,
DisplayName = "Test after token for single values for last page.")]
[DataRow("float_types", -1.7E308, .33, 3, 1, false,
DisplayName = "Test after token for float values.")]
[DataRow("decimal_types", -9.292929, 0.333333, 2, 1,
[DataRow("float_types", .33, "", 1, "", true,
DisplayName = "Test after token for float values for last page.")]
[DataRow("decimal_types", -9.292929, 0.0000000000000292929, 2, 4, false,
DisplayName = "Test after token for decimal values.")]
[DataRow("boolean_types", "false", "true", 2, 4,
[DataRow("decimal_types", 0.333333, "", 1, "", true,
DisplayName = "Test after token for decimal values for last page.")]
[DataRow("boolean_types", "false", "true", 2, 3, false,
DisplayName = "Test after token for boolean values.")]
[DataRow("boolean_types", "true", "", 3, "", true,
DisplayName = "Test after token for boolean values for last page.")]
[DataRow("date_types", "\"0001-01-01\"",
"\"9999-12-31\"", 3, 4,
"\"1999-01-08\"", 3, 2, false,
DisplayName = "Test after token for date values.")]
[DataRow("date_types", "\"1999-01-08\"",
"", 2, "", true,
DisplayName = "Test after token for date values for last page.")]
[DataRow("datetime_types", "\"1753-01-01T00:00:00.000\"",
"\"9999-12-31T23:59:59\"", 3, 4,
"\"1999-01-08T10:23:54\"", 3, 1, false,
DisplayName = "Test after token for datetime values.")]
[DataRow("datetime_types", "\"9999-12-31T23:59:59\"",
"", 4, "", true,
DisplayName = "Test after token for datetime values for last page.")]
[DataRow("datetime2_types", "\"0001-01-01 00:00:00.0000000\"",
"\"9999-12-31T23:59:59.9999999\"", 3, 4,
"\"1999-01-08T10:23:54.9999999\"", 3, 1, false,
DisplayName = "Test after token for datetime2 values.")]
[DataRow("datetime2_types", "\"9999-12-31T23:59:59.9999999\"",
"", 4, "", true,
DisplayName = "Test after token for datetime2 values for last page.")]
[DataRow("datetimeoffset_types", "\"0001-01-01 00:00:00.0000000+0:00\"",
"\"9999-12-31T23:59:59.9999999+14:00\"", 3, 4,
"\"1999-01-08T10:23:54.9999999-14:00\"", 3, 1, false,
DisplayName = "Test after token for datetimeoffset values.")]
[DataRow("datetimeoffset_types", "\"9999-12-31T23:59:59.9999999+14:00\"",
"", 4, "", true,
DisplayName = "Test after token for datetimeoffset values for last page.")]
[DataRow("smalldatetime_types", "\"1900-01-01 00:00:00\"",
"\"2079-06-06T00:00:00\"", 3, 4,
"\"1999-01-08T10:24:00\"", 3, 1, false,
DisplayName = "Test after token for smalldate values.")]
[DataRow("bytearray_types", "\"AAAAAA==\"", "\"/////w==\"", 3, 4,
[DataRow("smalldatetime_types", "\"2079-06-06T00:00:00\"",
"", 4, "", true,
DisplayName = "Test after token for smalldate values for last page.")]
[DataRow("bytearray_types", "\"AAAAAA==\"", "\"q83vASM=\"", 3, 1, false,
DisplayName = "Test after token for bytearray values.")]
[DataRow("bytearray_types", "\"q83vASM=\"", "", 1, "", true,
DisplayName = "Test after token for bytearray values for last page.")]
[TestMethod]
public override async Task RequestAfterTokenOnly(
string exposedFieldName,
object afterValue,
object endCursorValue,
object afterIdValue,
object endCursorIdValue)
object endCursorIdValue,
bool isLastPage)
{
await base.RequestAfterTokenOnly(
exposedFieldName,
afterValue,
endCursorValue,
afterIdValue,
endCursorIdValue);
endCursorIdValue,
isLastPage);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,43 +25,68 @@ public static async Task SetupAsync(TestContext context)

/// <inheritdoc />
[DataTestMethod]
[DataRow("typeid", 1, 4, "", "",
[DataRow("typeid", 1, 3, "", "", false,
DisplayName = "Test after token for primary key with mapped name.")]
[DataRow("byte_types", 0, 255, 2, 4, DisplayName = "Test after token for byte values.")]
[DataRow("short_types", -32768, 32767, 3, 4, DisplayName = "Test after token for short values.")]
[DataRow("int_types", -2147483648, 2147483647, 3, 4,
[DataRow("typeid", 4, 6, "", "", true,
DisplayName = "Test after token for primary key with mapped name for last page.")]
[DataRow("byte_types", 0, 1, 2, 1, false, DisplayName = "Test after token for byte values.")]
[DataRow("byte_types", 1, "", 1, "", true, DisplayName = "Test after token for byte values for last page.")]
[DataRow("short_types", -32768, 1, 3, 1, false, DisplayName = "Test after token for short values.")]
[DataRow("short_types", 1, "", 1, "", true, DisplayName = "Test after token for short values for last page.")]
[DataRow("int_types", -2147483648, 1, 3, 1, false,
DisplayName = "Test after token for int values.")]
[DataRow("long_types", -9223372036854775808, 9.223372036854776E+18, 3, 4,
[DataRow("int_types", 1, "", 1, "", true,
DisplayName = "Test after token for int values for last page.")]
[DataRow("long_types", -9223372036854775808, 1, 3, 1, false,
DisplayName = "Test after token for long values.")]
[DataRow("string_types", "\"\"", "\"null\"", 1, 4,
[DataRow("long_types", 1, "", 1, "", true,
DisplayName = "Test after token for long values for last page.")]
[DataRow("string_types", "\"\"", "\"lksa;jdflasdf;alsdflksdfkldj\"", 1, 2, false,
DisplayName = "Test after token for string values.")]
[DataRow("single_types", -3.39E38, 3.3999999521443642E+38, 3, 4,
[DataRow("string_types", "null", "", 3, "", true,
DisplayName = "Test after token for string values for last page.")]
[DataRow("single_types", -3.39E38, .33000001311302185, 3, 1, false,
DisplayName = "Test after token for single values.")]
[DataRow("float_types", -1.7E308, 1.7E308, 3, 4,
[DataRow("single_types", .33, "", 1, "", true,
DisplayName = "Test after token for single values for last page.")]
[DataRow("float_types", -1.7E308, .33, 3, 1, false,
DisplayName = "Test after token for float values.")]
[DataRow("decimal_types", -9.292929, 0.333333, 2, 1,
[DataRow("float_types", .33, "", 1, "", true,
DisplayName = "Test after token for float values for last page.")]
[DataRow("decimal_types", -9.292929, 0.0000000000000292929, 2, 4, false,
DisplayName = "Test after token for decimal values.")]
[DataRow("boolean_types", "false", "true", 2, 4,
[DataRow("decimal_types", 0.333333, "", 1, "", true,
DisplayName = "Test after token for decimal values for last page.")]
[DataRow("boolean_types", "false", "true", 2, 3, false,
DisplayName = "Test after token for boolean values.")]
[DataRow("boolean_types", "true", "", 3, "", true,
DisplayName = "Test after token for boolean values for last page.")]
[DataRow("datetime_types", "\"1753-01-01T00:00:00.000\"",
"\"9999-12-31 23:59:59.000000\"", 3, 4,
"\"1999-01-08T10:23:54\"", 3, 1, false,
DisplayName = "Test after token for datetime values.")]
[DataRow("bytearray_types", "\"AAAAAA==\"", "\"/////w==\"", 3, 4,
[DataRow("datetime_types", "\"9999-12-31T23:59:59\"",
"", 4, "", true,
DisplayName = "Test after token for datetime values for last page.")]
[DataRow("bytearray_types", "\"AAAAAA==\"", "\"q83vASM=\"", 3, 1, false,
DisplayName = "Test after token for bytearray values.")]
[DataRow("bytearray_types", "\"q83vASM=\"", "", 1, "", true,
DisplayName = "Test after token for bytearray values for last page.")]
[TestMethod]
public override async Task RequestAfterTokenOnly(
string exposedFieldName,
object afterValue,
object endCursorValue,
object afterIdValue,
object endCursorIdValue)
object endCursorIdValue,
bool isLastPage)
{
await base.RequestAfterTokenOnly(
exposedFieldName,
afterValue,
endCursorValue,
afterIdValue,
endCursorIdValue);
endCursorIdValue,
isLastPage);
}
}
}
Loading