Skip to content

Commit

Permalink
fix: ensure resource id's are always encoded/decoded when appropriate…
Browse files Browse the repository at this point in the history
… within a cursor

test: test cursor id encoding/decoding
  • Loading branch information
Gregory Haddow committed Aug 6, 2024
1 parent 06ee7ad commit 1ef14ee
Show file tree
Hide file tree
Showing 6 changed files with 427 additions and 60 deletions.
21 changes: 8 additions & 13 deletions src/Pagination/Cursor/CursorBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class CursorBuilder
{
private Builder|Relation $query;

private ?ID $id = null;
private ID $id;

private string $keyName;

Expand All @@ -26,6 +26,8 @@ class CursorBuilder

private bool $keySort = true;

private CursorParser $parser;

/**
* CursorBuilder constructor.
*
Expand All @@ -34,14 +36,16 @@ class CursorBuilder
* @param string|null $key
* the key column that the before/after cursors related to
*/
public function __construct($query, string $key = null)
public function __construct($query, ID $id, string $key = null)
{
if (!$query instanceof Builder && !$query instanceof Relation) {
throw new \InvalidArgumentException('Expecting an Eloquent query builder or relation.');
}

$this->query = $query;
$this->id = $id;
$this->keyName = $key ?: $this->guessKey();
$this->parser = new CursorParser(IdParser::make($this->id), $this->keyName);
}

/**
Expand All @@ -58,15 +62,6 @@ public function withDefaultPerPage(?int $perPage): self
return $this;
}

/**
* @return $this
*/
public function withIdField(?ID $id): self
{
$this->id = $id;

return $this;
}

public function withKeySort(bool $keySort): self
{
Expand Down Expand Up @@ -108,8 +103,8 @@ public function paginate(Cursor $cursor, array $columns = ['*']): CursorPaginato
$this->applyKeySort();

$total = $this->getTotal();
$laravelPaginator = $this->query->cursorPaginate($cursor->getLimit(), $columns, 'cursor', $this->convertCursor($cursor));
$paginator = new CursorPaginator($laravelPaginator, $cursor, $total);
$laravelPaginator = $this->query->cursorPaginate($cursor->getLimit(), $columns, 'cursor', $this->parser->decode($cursor));
$paginator = new CursorPaginator($this->parser, $laravelPaginator, $cursor, $total);

return $paginator->withCurrentPath();
}
Expand Down
6 changes: 3 additions & 3 deletions src/Pagination/Cursor/CursorPaginator.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class CursorPaginator implements \IteratorAggregate, \Countable
/**
* CursorPaginator constructor.
*/
public function __construct(private readonly LaravelCursorPaginator $laravelPaginator, private readonly Cursor $cursor, private readonly int|null $total = null)
public function __construct(private readonly CursorParser $parser, private readonly LaravelCursorPaginator $laravelPaginator, private readonly Cursor $cursor, private readonly int|null $total = null)
{
$this->items = Collection::make($this->laravelPaginator->items());
}
Expand All @@ -34,7 +34,7 @@ public function firstItem(): ?string
return null;
}

return $this->laravelPaginator->getCursorForItem($this->items->first(), false)->encode();
return $this->parser->encode($this->laravelPaginator->getCursorForItem($this->items->first(), false));
}

public function lastItem(): ?string
Expand All @@ -43,7 +43,7 @@ public function lastItem(): ?string
return null;
}

return $this->laravelPaginator->getCursorForItem($this->items->last())->encode();
return $this->parser->encode($this->laravelPaginator->getCursorForItem($this->items->last()));
}

public function hasMorePages(): bool
Expand Down
54 changes: 54 additions & 0 deletions src/Pagination/Cursor/CursorParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

namespace LaravelJsonApi\Eloquent\Pagination\Cursor;

use Illuminate\Pagination\Cursor as LaravelCursor;
use LaravelJsonApi\Core\Schema\IdParser;

class CursorParser
{
public function __construct(private IdParser $idParser, private string $keyName)
{

}

public function encode(LaravelCursor $cursor): string
{
$key = $cursor->parameter($this->keyName);
if (!$key) {
return $cursor->encode();
}

$encodedId = $this->idParser->encode($key);
$parameters = $cursor->toArray();
unset($parameters['_pointsToNextItems']);
$parameters[$this->keyName] = $encodedId;

$newCursor = new LaravelCursor($parameters, $cursor->pointsToNextItems());
return $newCursor->encode();
}

public function decode(Cursor $cursor): ?LaravelCursor
{
$encodedCursor = $cursor->isBefore() ? $cursor->getBefore() : $cursor->getAfter();
if (!is_string($encodedCursor)) {
return null;
}

$parameters = json_decode(base64_decode(str_replace(['-', '_'], ['+', '/'], $encodedCursor)), true);

if (json_last_error() !== JSON_ERROR_NONE) {
return null;
}

$pointsToNextItems = $parameters['_pointsToNextItems'];
unset($parameters['_pointsToNextItems']);
if (isset($parameters[$this->keyName])) {
$parameters[$this->keyName] = $this->idParser->decode(
$parameters[$this->keyName],
);
}

return new LaravelCursor($parameters, $pointsToNextItems);
}
}
3 changes: 1 addition & 2 deletions src/Pagination/CursorPagination.php
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,6 @@ public function paginate($query, array $page): Page

$paginator = $this
->query($query)
->withIdField($this->id)
->withDirection($this->direction)
->withKeySort($this->keySort)
->withDefaultPerPage($this->defaultPerPage)
Expand All @@ -227,7 +226,7 @@ public function paginate($query, array $page): Page
*/
private function query(Builder|Relation $query): CursorBuilder
{
return new CursorBuilder($query, $this->primaryKey);
return new CursorBuilder($query, $this->id, $this->primaryKey);
}

/**
Expand Down
Loading

0 comments on commit 1ef14ee

Please sign in to comment.