Skip to content
Closed
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
38 changes: 36 additions & 2 deletions src/CursorPagination.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

namespace LaravelJsonApi\CursorPagination;

use Closure;
use InvalidArgumentException;
use LaravelJsonApi\Contracts\Pagination\Page;
use LaravelJsonApi\Core\Pagination\Concerns\HasPageMeta;
Expand Down Expand Up @@ -61,6 +62,11 @@ class CursorPagination implements Paginator
*/
private ?string $primaryKey = null;

/**
* @var Closure|null
*/
private ?Closure $keyDecoderCallback = null;

/**
* @var string|array|null
*/
Expand Down Expand Up @@ -194,6 +200,22 @@ public function withKeyName(string $column): self
return $this;
}

/**
* Set the decoder callback for paginator key column values.
*
* In case key column value is stored not as a string, for example uuid in binary format,
* it has to be decoded before passing it to Cursor.
*
* @param callable $callback
* @return $this
*/
public function withKeyDecoder(callable $callback): self
{
$this->keyDecoderCallback = Closure::fromCallable($callback);

return $this;
}

/**
* @inheritDoc
*/
Expand Down Expand Up @@ -260,10 +282,22 @@ private function cursor(array $page): Cursor
$limit = $page[$this->limit] ?? null;

return new Cursor(
!is_null($before) ? strval($before) : null,
!is_null($after) ? strval($after) : null,
!is_null($before) ? $this->decodePaginationKey($before) : null,
!is_null($after) ? $this->decodePaginationKey($after) : null,
!is_null($limit) ? intval($limit) : null,
);
}

/**
* Apply key value decoder callback if it's not null.
*
* @param $value
* @return string
*/
private function decodePaginationKey($value): string
{
$decoded = $this->keyDecoderCallback ? ($this->keyDecoderCallback)($value) : $value;

return strval($decoded);
}
}
4 changes: 2 additions & 2 deletions tests/database/factories/VideoFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ class VideoFactory extends Factory
public function definition()
{
return [
'slug' => $this->faker->unique()->slug,
'slug' => $this->faker->unique()->slug(),
'title' => $this->faker->words(5, true),
'url' => $this->faker->url,
'url' => $this->faker->url(),
];
}

Expand Down
69 changes: 51 additions & 18 deletions tests/lib/Acceptance/Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public function testDefaultPagination(): void
$this->videos->method('defaultPagination')->willReturn(['limit' => '3']);

$videos = Video::factory()->count(5)->create([
'created_at' => fn() => $this->faker->dateTime,
'created_at' => fn() => $this->faker->dateTime(),
])->sortByDesc('created_at')->values();

$expected = $videos->take(3);
Expand Down Expand Up @@ -191,7 +191,7 @@ public function testNoPages(): void
public function testOnlyLimit(): void
{
$videos = Video::factory()->count(5)->create([
'created_at' => fn() => $this->faker->unique()->dateTime,
'created_at' => fn() => $this->faker->unique()->dateTime(),
])->sortByDesc('created_at')->values();

$meta = [
Expand All @@ -213,7 +213,7 @@ public function testOnlyLimit(): void
public function testBefore(): void
{
$videos = Video::factory()->count(10)->create([
'created_at' => fn() => $this->faker->unique()->dateTime,
'created_at' => fn() => $this->faker->unique()->dateTime(),
])->sortByDesc('created_at')->values();

$expected = [$videos[4], $videos[5], $videos[6]];
Expand Down Expand Up @@ -242,7 +242,7 @@ public function testBeforeAscending(): void
$this->paginator->withAscending();

$videos = Video::factory()->count(10)->create([
'created_at' => fn() => $this->faker->unique()->dateTime,
'created_at' => fn() => $this->faker->unique()->dateTime(),
])->sortBy('created_at')->values();

$expected = [$videos[4], $videos[5], $videos[6]];
Expand Down Expand Up @@ -307,14 +307,14 @@ public function testBeforeDoesNotExist(): void
$this->expectExceptionMessage('does not exist');

$this->videos->repository()->queryAll()->paginate([
'before' => $this->faker->uuid,
'before' => $this->faker->uuid(),
]);
}

public function testAfter(): void
{
$videos = Video::factory()->count(10)->create([
'created_at' => fn() => $this->faker->unique()->dateTime,
'created_at' => fn() => $this->faker->unique()->dateTime(),
])->sortByDesc('created_at')->values();

$expected = [$videos[4], $videos[5], $videos[6]];
Expand Down Expand Up @@ -343,7 +343,7 @@ public function testAfterAscending(): void
$this->paginator->withAscending();

$videos = Video::factory()->count(10)->create([
'created_at' => fn() => $this->faker->unique()->dateTime,
'created_at' => fn() => $this->faker->unique()->dateTime(),
])->sortBy('created_at')->values();

$expected = [$videos[4], $videos[5], $videos[6]];
Expand All @@ -370,7 +370,7 @@ public function testAfterAscending(): void
public function testAfterWithoutMore(): void
{
$videos = Video::factory()->count(4)->create([
'created_at' => fn() => $this->faker->unique()->dateTime,
'created_at' => fn() => $this->faker->unique()->dateTime(),
])->sortByDesc('created_at')->values();

$expected = [$videos[2], $videos[3]];
Expand Down Expand Up @@ -434,7 +434,7 @@ public function testAfterWithCustomKeys(): void
->withLimitKey('per-page');

$videos = Video::factory()->count(6)->create([
'created_at' => fn() => $this->faker->dateTime,
'created_at' => fn() => $this->faker->dateTime(),
])->sortByDesc('created_at')->values();

$expected = [$videos[2], $videos[3], $videos[4]];
Expand Down Expand Up @@ -493,7 +493,7 @@ public function testAfterDoesNotExist(): void
$this->expectExceptionMessage('does not exist');

$this->videos->repository()->queryAll()->paginate([
'after' => $this->faker->uuid,
'after' => $this->faker->uuid(),
]);
}

Expand All @@ -503,7 +503,7 @@ public function testAfterDoesNotExist(): void
public function testBeforeAndAfter(): void
{
$videos = Video::factory()->count(6)->create([
'created_at' => fn() => $this->faker->unique()->dateTime,
'created_at' => fn() => $this->faker->unique()->dateTime(),
])->sortByDesc('created_at')->values();

$expected = [$videos[2], $videos[3], $videos[4]];
Expand Down Expand Up @@ -537,7 +537,7 @@ public function testSameColumnAndIdentifier(): void
$this->paginator->withCursorColumn('uuid');

$videos = Video::factory()->count(6)->create([
'created_at' => fn() => $this->faker->unique()->dateTime,
'created_at' => fn() => $this->faker->unique()->dateTime(),
])->sortByDesc('uuid')->values();

$expected = [$videos[1], $videos[2], $videos[3]];
Expand Down Expand Up @@ -567,7 +567,7 @@ public function testSnakeCaseMetaAndCustomMetaKey(): void
$this->paginator->withMetaKey('cursor')->withSnakeCaseMeta();

$videos = Video::factory()->count(6)->create([
'created_at' => fn() => $this->faker->unique()->dateTime,
'created_at' => fn() => $this->faker->unique()->dateTime(),
])->sortByDesc('created_at')->values();

$expected = [$videos[1], $videos[2], $videos[3]];
Expand All @@ -593,7 +593,7 @@ public function testDashCaseMeta(): void
$this->paginator->withDashCaseMeta();

$videos = Video::factory()->count(6)->create([
'created_at' => fn() => $this->faker->unique()->dateTime,
'created_at' => fn() => $this->faker->unique()->dateTime(),
])->sortByDesc('created_at')->values();

$expected = [$videos[1], $videos[2], $videos[3]];
Expand Down Expand Up @@ -622,7 +622,7 @@ public function testColumn(): void
$this->paginator->withCursorColumn('updated_at');

$videos = Video::factory()->count(6)->create([
'updated_at' => fn() => $this->faker->unique()->dateTime,
'updated_at' => fn() => $this->faker->unique()->dateTime(),
])->sortByDesc('updated_at')->values();

$expected = [$videos[1], $videos[2], $videos[3]];
Expand Down Expand Up @@ -654,7 +654,7 @@ public function testItUsesModelDefaultPerPage(): void
$expected = (new Video())->getPerPage();

$videos = Video::factory()->count($expected + 5)->create([
'created_at' => fn() => $this->faker->dateTime,
'created_at' => fn() => $this->faker->dateTime(),
])->sortByDesc('created_at')->values();

$meta = [
Expand Down Expand Up @@ -683,7 +683,7 @@ public function testItUsesDefaultPerPage(): void
$this->paginator->withDefaultPerPage($expected);

$videos = Video::factory()->count($expected + 5)->create([
'created_at' => fn() => $this->faker->dateTime,
'created_at' => fn() => $this->faker->dateTime(),
])->sortByDesc('created_at')->values();

$meta = [
Expand All @@ -707,7 +707,7 @@ public function testItCanRemoveMeta(): void
$this->paginator->withoutMeta();

$videos = Video::factory()->count(4)->create([
'created_at' => fn() => $this->faker->dateTime,
'created_at' => fn() => $this->faker->dateTime(),
])->sortByDesc('created_at')->values();

$links = $this->createLinks($videos[0], $videos[2], 3);
Expand All @@ -719,6 +719,39 @@ public function testItCanRemoveMeta(): void
$this->assertPage($videos->take(3), $page);
}

/**
* Test use of the cursor paginator where the pagination column value is encoded
*/
public function testCursorColumnEncoded(): void
{
$this->paginator->withCursorColumn('uuid')->withKeyDecoder(fn($v) => base64_decode($v));

$videos = Video::factory()->count(6)->create([
'created_at' => fn() => $this->faker->unique()->dateTime(),
])->sortByDesc('uuid')->values();

$expected = [$videos[1], $videos[2], $videos[3]];

$meta = [
'from' => $expected[0]->getRouteKey(),
'hasMore' => true,
'perPage' => 3,
'to' => $expected[2]->getRouteKey(),
];

$links = $this->createLinks($expected[0], $expected[2], 3);

$page = $this->videos->repository()->queryAll()->paginate([
'limit' => '3',
'before' => base64_encode($videos[4]->getRouteKey()),
'after' => base64_encode($videos[1]->getRouteKey()),
]);

$this->assertSame(['page' => $meta], $page->meta());
$this->assertSame($links, $page->links()->toArray());
$this->assertPage($expected, $page);
}

/**
* @param Model $from
* @param Model $to
Expand Down