diff --git a/Directory.Build.props b/Directory.Build.props index 111c7cd..68e77e8 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -22,7 +22,7 @@ - 3.0.2 + 3.0.3 DrSkillIssue MIT https://github.com/DrSkillIssue/EFPagination diff --git a/src/EFPagination/Internal/CursorPair.cs b/src/EFPagination/Internal/CursorPair.cs index 4d2465f..df9a22c 100644 --- a/src/EFPagination/Internal/CursorPair.cs +++ b/src/EFPagination/Internal/CursorPair.cs @@ -31,10 +31,11 @@ public static (string? Next, string? Previous) Encode( var options = new PaginationCursorOptions(sortBy, PaginationCount.AsNullable(totalCount)); - var next = hasMore ? PaginationCursor.Encode(definition, items[^1], options) : null; - var previous = (hasInitialReference || direction == PaginationDirection.Backward) - ? PaginationCursor.Encode(definition, items[0], options) - : null; + var hasPrevious = direction == PaginationDirection.Forward ? hasInitialReference : hasMore; + var hasNext = direction == PaginationDirection.Forward ? hasMore : hasInitialReference; + + var next = hasNext ? PaginationCursor.Encode(definition, items[^1], options) : null; + var previous = hasPrevious ? PaginationCursor.Encode(definition, items[0], options) : null; return (next, previous); } } diff --git a/src/EFPagination/Internal/KeysetProjectionExecutor.cs b/src/EFPagination/Internal/KeysetProjectionExecutor.cs index ab112ec..14399f2 100644 --- a/src/EFPagination/Internal/KeysetProjectionExecutor.cs +++ b/src/EFPagination/Internal/KeysetProjectionExecutor.cs @@ -118,13 +118,16 @@ private static (string? Next, string? Previous) EncodeCursorPair( var options = new PaginationCursorOptions(sortBy, PaginationCount.AsNullable(totalCount)); + var hasPrevious = direction == PaginationDirection.Forward ? hasInitialReference : hasMore; + var hasNext = direction == PaginationDirection.Forward ? hasMore : hasInitialReference; + string? next = null; string? previous = null; - if (hasMore) + if (hasNext) next = EncodeFromIndex(definition, page, page.Count - 1, options); - if (hasInitialReference || direction == PaginationDirection.Backward) + if (hasPrevious) previous = EncodeFromIndex(definition, page, 0, options); return (next, previous); diff --git a/test/EFPagination.Tests/CursorExecutorIntegrationTests.cs b/test/EFPagination.Tests/CursorExecutorIntegrationTests.cs index 2947f3a..c9daecf 100644 --- a/test/EFPagination.Tests/CursorExecutorIntegrationTests.cs +++ b/test/EFPagination.Tests/CursorExecutorIntegrationTests.cs @@ -75,6 +75,46 @@ public async Task ExecuteFromCursorAsync_MiddlePage_HasBothCursors() secondPage.Items.Select(x => x.Id).Should().BeEquivalentTo(Enumerable.Range(11, 10), o => o.WithStrictOrdering()); } + [Fact] + public async Task ExecuteFromCursorAsync_BackwardToFirstPage_HasNoPreviousCursor_AndHasNextCursor() + { + var def = PaginationQuery.Build(b => b.Ascending(x => x.Id)); + + var firstPage = await PaginationExecutor.ExecuteFromCursorAsync( + _dbContext.MainModels, def, + new ExecutionOptions(PageSize: 10), + cursor: []); + + var secondPage = await PaginationExecutor.ExecuteFromCursorAsync( + _dbContext.MainModels, def, + new ExecutionOptions(PageSize: 10), + firstPage.NextCursor); + + var back = await PaginationExecutor.ExecuteFromCursorAsync( + _dbContext.MainModels, def, + new ExecutionOptions(PageSize: 10, Direction: PaginationDirection.Backward), + secondPage.PreviousCursor); + + back.Items.Select(x => x.Id).Should().BeEquivalentTo(Enumerable.Range(1, 10), o => o.WithStrictOrdering()); + back.PreviousCursor.Should().BeNull(); + back.NextCursor.Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task ExecuteFromCursorAsync_BackwardWithoutCursor_ReturnsLastPageWithPreviousCursor_AndNoNextCursor() + { + var def = PaginationQuery.Build(b => b.Ascending(x => x.Id)); + + var page = await PaginationExecutor.ExecuteFromCursorAsync( + _dbContext.MainModels, def, + new ExecutionOptions(PageSize: 10, Direction: PaginationDirection.Backward), + cursor: []); + + page.Items.Select(x => x.Id).Should().BeEquivalentTo(Enumerable.Range(90, 10), o => o.WithStrictOrdering()); + page.PreviousCursor.Should().NotBeNullOrEmpty(); + page.NextCursor.Should().BeNull(); + } + [Fact] public async Task ExecuteFromCursorAsync_LastPage_HasNullNextCursor() { diff --git a/test/EFPagination.Tests/KeysetFluentCursorIntegrationTests.cs b/test/EFPagination.Tests/KeysetFluentCursorIntegrationTests.cs new file mode 100644 index 0000000..e01d878 --- /dev/null +++ b/test/EFPagination.Tests/KeysetFluentCursorIntegrationTests.cs @@ -0,0 +1,94 @@ +#nullable enable +using EFPagination.TestModels; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace EFPagination; + +[Collection(SqliteDatabaseCollection.Name)] +public class KeysetFluentCursorIntegrationTests +{ + private readonly TestDbContext _db; + + public KeysetFluentCursorIntegrationTests(SqliteDatabaseFixture fixture) + { + var provider = fixture.BuildServices(); + _db = provider.GetService()!; + } + + [Fact] + public async Task TakeAsync_BackwardToFirstPage_HasNoPreviousCursor_AndHasNextCursor() + { + var def = PaginationQuery.Build(b => b.Ascending(x => x.Id)); + + var first = await _db.MainModels.Keyset(def).TakeAsync(10); + var second = await _db.MainModels.Keyset(def).After(first.NextCursor!).TakeAsync(10); + + var back = await _db.MainModels.Keyset(def).Before(second.PreviousCursor!).TakeAsync(10); + + back.Items.Select(x => x.Id).Should().BeEquivalentTo(Enumerable.Range(1, 10), o => o.WithStrictOrdering()); + back.PreviousCursor.Should().BeNull(); + back.NextCursor.Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task TakeAsync_BackwardToMiddlePage_HasBothCursors() + { + var def = PaginationQuery.Build(b => b.Ascending(x => x.Id)); + + var first = await _db.MainModels.Keyset(def).TakeAsync(10); + var second = await _db.MainModels.Keyset(def).After(first.NextCursor!).TakeAsync(10); + var third = await _db.MainModels.Keyset(def).After(second.NextCursor!).TakeAsync(10); + + var back = await _db.MainModels.Keyset(def).Before(third.PreviousCursor!).TakeAsync(10); + + back.Items.Select(x => x.Id).Should().BeEquivalentTo(Enumerable.Range(11, 10), o => o.WithStrictOrdering()); + back.PreviousCursor.Should().NotBeNullOrEmpty(); + back.NextCursor.Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task TakeAsync_BackwardToFirstPage_WithDescendingMultiColumnDefinition_HasNoPreviousCursor_AndHasNextCursor() + { + var def = PaginationQuery.Build(b => b + .Descending(x => x.Created) + .Descending(x => x.Id)); + + var first = await _db.MainModels.Keyset(def).TakeAsync(10); + var second = await _db.MainModels.Keyset(def).After(first.NextCursor!).TakeAsync(10); + + var back = await _db.MainModels.Keyset(def).Before(second.PreviousCursor!).TakeAsync(10); + + back.Items.Select(x => x.Id).Should().BeEquivalentTo(Enumerable.Range(90, 10).Reverse(), o => o.WithStrictOrdering()); + back.PreviousCursor.Should().BeNull(); + back.NextCursor.Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task TakeAsync_BeforeBoundValues_HasPreviousCursor_AndHasNextCursor() + { + var def = PaginationQuery.Build(b => b.Ascending(x => x.Id)); + var values = PaginationValues.Create(def, 31); + + var page = await _db.MainModels.Keyset(def).Before(values).TakeAsync(10); + + page.Items.Select(x => x.Id).Should().BeEquivalentTo(Enumerable.Range(21, 10), o => o.WithStrictOrdering()); + page.PreviousCursor.Should().NotBeNullOrEmpty(); + page.NextCursor.Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task TakeAsync_BeforeEntity_HasPreviousCursor_AndHasNextCursor() + { + var def = PaginationQuery.Build(b => b.Ascending(x => x.Id)); + var reference = await _db.MainModels.SingleAsync(x => x.Id == 31); + + var page = await _db.MainModels.Keyset(def).BeforeEntity(reference).TakeAsync(10); + + page.Items.Select(x => x.Id).Should().BeEquivalentTo(Enumerable.Range(21, 10), o => o.WithStrictOrdering()); + page.PreviousCursor.Should().NotBeNullOrEmpty(); + page.NextCursor.Should().NotBeNullOrEmpty(); + } +} diff --git a/test/EFPagination.Tests/ServerSideProjectionIntegrationTests.cs b/test/EFPagination.Tests/ServerSideProjectionIntegrationTests.cs index 53053cd..cb7cb3d 100644 --- a/test/EFPagination.Tests/ServerSideProjectionIntegrationTests.cs +++ b/test/EFPagination.Tests/ServerSideProjectionIntegrationTests.cs @@ -97,6 +97,8 @@ public async Task TakeAsync_ProjectedBackwardDirection_ReturnsPreviousPage() .TakeAsync(10, x => new ItemDto(x.Id, x.String, x.Created)); back.Items.Select(i => i.EntityId).Should().BeEquivalentTo(Enumerable.Range(1, 10), o => o.WithStrictOrdering()); + back.PreviousCursor.Should().BeNull(); + back.NextCursor.Should().NotBeNullOrEmpty(); } [Fact]