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
12 changes: 10 additions & 2 deletions src/Cursor/Criteria.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Psr\Http\Message\ServerRequestInterface;
use TinyBlocks\HttpQuery\Comparison;
use TinyBlocks\HttpQuery\Exceptions\CursorIsInvalid;
use TinyBlocks\HttpQuery\Exceptions\FilterExpressionIsInvalid;
use TinyBlocks\HttpQuery\Exceptions\FilterFieldNotAllowed;
use TinyBlocks\HttpQuery\Exceptions\FilterOperatorNotAllowed;
Expand All @@ -17,6 +18,7 @@
use TinyBlocks\HttpQuery\Exceptions\SortIsRequired;
use TinyBlocks\HttpQuery\Filter;
use TinyBlocks\HttpQuery\Internal\Query;
use TinyBlocks\HttpQuery\Order;
use TinyBlocks\HttpQuery\Schema;
use TinyBlocks\HttpQuery\Sort;

Expand All @@ -42,8 +44,10 @@ private function __construct(
/**
* Creates a Criteria from the schema and the request.
*
* <p>It parses the request query string and validates it against the schema. The pagination
* always carries the incoming cursor token and the page size.</p>
* <p>It parses the request query string and validates it against the schema, the incoming cursor
* included: a token that cannot be decoded into one value per effective sort order is rejected
* here, and never later while the seek is being built. The pagination always carries the
* incoming cursor token and the page size.</p>
*
* @param Schema $schema The query contract the request is validated against.
* @param ServerRequestInterface $request The incoming PSR-7 server request.
Expand All @@ -56,11 +60,15 @@ private function __construct(
* @throws FilterOperatorNotAllowed If a comparison uses an operator not allowed for its field.
* @throws FilterValueNotAllowed If a compared value falls outside the permitted set or kind.
* @throws SortFieldNotAllowed If the sort orders by a field that was never declared sortable.
* @throws CursorIsInvalid If the incoming cursor cannot be decoded into one value per sort order.
*/
public static function fromQuery(Schema $schema, ServerRequestInterface $request): Criteria
{
$query = Query::from(schema: $schema, request: $request);
$cursor = Token::from(token: $query->cursorToken());
$fields = array_map(static fn(Order $order): string => $order->field(), $query->sort()->orders());

$cursor->keyedBy(fields: $fields);

return new Criteria(
sort: $query->sort(),
Expand Down
32 changes: 22 additions & 10 deletions tests/Unit/Cursor/KeysetTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,24 +125,36 @@ public function testCursorWhenIncomingCursorGivenThenKeysValuesBySortField(): vo
self::assertSame(['created_at' => '2023-01-15T10:30:00Z', 'id' => 5], $cursor);
}

public function testCursorWhenDecodedCountMismatchesThenThrowsCursorIsInvalid(): void
public function testCriteriaWhenDecodedCountMismatchesThenThrowsCursorIsInvalid(): void
{
/** @Given an opaque token carrying a single key value */
$token = Token::fromKeys(keys: [5])->toString();

/** @And a keyset view whose sort carries two fields */
$keyset = Criteria::fromQuery(
schema: $this->schema,
request: Query::from(
parameters: ['sort' => 'created_at,id', 'page' => ['cursor' => $token, 'size' => '2']]
)
)->keyset();
/** @And a request whose sort carries two fields */
$request = Query::from(
parameters: ['sort' => 'created_at,id', 'page' => ['cursor' => $token, 'size' => '2']]
);

/** @Then an exception indicating the cursor is invalid is raised */
$this->expectException(CursorIsInvalid::class);

/** @When reading the incoming cursor key values */
$keyset->cursor();
/** @When the criteria is parsed from the request */
Criteria::fromQuery(schema: $this->schema, request: $request);
}

public function testCriteriaWhenTokenCannotBeDecodedThenThrowsCursorIsInvalid(): void
{
/** @Given a request carrying a token that never came out of the codec */
$request = Query::from(
parameters: ['sort' => 'created_at,id', 'page' => ['cursor' => 'not-a-cursor', 'size' => '2']]
);

/** @Then an exception indicating the cursor is invalid is raised */
$this->expectException(CursorIsInvalid::class);
$this->expectExceptionMessage('Cursor token <not-a-cursor> is invalid and could not be decoded.');

/** @When the criteria is parsed from the request */
Criteria::fromQuery(schema: $this->schema, request: $request);
}

public function testConstructorWhenInvokedThroughReflectionThenInstantiatesTheStaticOnlySortKeys(): void
Expand Down