diff --git a/src/Dialect/DataApiBuilderDialect.php b/src/Dialect/DataApiBuilderDialect.php index b1f04ae..4225b49 100644 --- a/src/Dialect/DataApiBuilderDialect.php +++ b/src/Dialect/DataApiBuilderDialect.php @@ -13,7 +13,7 @@ final class DataApiBuilderDialect implements GraphqlQueryDialect { public function extractCollection(array $data): array { - /** @var array $items */ + /** @var array[] $items */ $items = $data['items'] ?? []; return $items; @@ -32,6 +32,16 @@ public function applyQueryOptions(array $arguments, QueryOptions $options): arra $arguments['orderBy'] = $options->orderBy; + if ($options->paginate) { + if ($options->limit) { + $arguments['first'] = $options->limit; + } + + if ($options->cursor) { + $arguments['after'] = $options->cursor; + } + } + return $arguments; } diff --git a/src/Dialect/GraphqlQueryDialect.php b/src/Dialect/GraphqlQueryDialect.php index 2cea8da..b8fbfc1 100644 --- a/src/Dialect/GraphqlQueryDialect.php +++ b/src/Dialect/GraphqlQueryDialect.php @@ -11,9 +11,9 @@ interface GraphqlQueryDialect { /** - * @param array $data + * @param array[] $data * - * @return array + * @return array[] */ public function extractCollection(array $data): array; diff --git a/src/GraphqlManager.php b/src/GraphqlManager.php index b588b0b..874f91d 100644 --- a/src/GraphqlManager.php +++ b/src/GraphqlManager.php @@ -13,6 +13,8 @@ use GraphqlOrm\Metadata\GraphqlEntityMetadataFactory; use GraphqlOrm\Query\Ast\QueryNode; use GraphqlOrm\Query\GraphqlQueryCompiler; +use GraphqlOrm\Query\Pagination\PaginatedResult; +use GraphqlOrm\Query\QueryOptions; use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; /** @@ -39,14 +41,14 @@ public function __construct( /** * @param array $variables * - * @return T[] + * @return T[]|PaginatedResult */ - public function execute(QueryNode|string $graphql, callable $hydration, array $variables = [], ): array + public function execute(QueryNode|string $graphql, callable $hydration, QueryOptions $options = new QueryOptions(), array $variables = []): array|PaginatedResult { $context = new GraphqlExecutionContext(); if ($graphql instanceof QueryNode) { - $compiled = $this->getQueryCompiler()->compile($graphql); + $compiled = $this->getQueryCompiler()->compile($graphql, $options); /** @var array $ast */ $ast = json_decode(json_encode($graphql, JSON_THROW_ON_ERROR), true); $context->trace->ast = $ast; diff --git a/src/Hydrator/EntityHydrator.php b/src/Hydrator/EntityHydrator.php index f54155f..74b194b 100644 --- a/src/Hydrator/EntityHydrator.php +++ b/src/Hydrator/EntityHydrator.php @@ -24,7 +24,7 @@ public function __construct( } /** - * @param array $data + * @param array $data */ public function hydrate( GraphqlEntityMetadata $metadata, diff --git a/src/Query/GraphqlQuery.php b/src/Query/GraphqlQuery.php index 609a61a..db76c94 100644 --- a/src/Query/GraphqlQuery.php +++ b/src/Query/GraphqlQuery.php @@ -7,6 +7,7 @@ use GraphqlOrm\Exception\InvalidGraphqlResponseException; use GraphqlOrm\GraphqlManager; use GraphqlOrm\Query\Ast\QueryNode; +use GraphqlOrm\Query\Pagination\PaginatedResult; /** * @template T of object @@ -21,36 +22,38 @@ public function __construct( private QueryNode|string $graphql, private string $entityClass, private GraphqlManager $manager, + private QueryOptions $options, ) { } public function getGraphQL(): string { if ($this->graphql instanceof QueryNode) { - return $this->manager->getQueryCompiler()->compile($this->graphql); + return $this->manager->getQueryCompiler()->compile($this->graphql, $this->options); } return $this->graphql; } /** - * @return T[] + * @return T[]|PaginatedResult */ - public function getResult(): array + public function getResult(): array|PaginatedResult { - $metadata = $this - ->manager + $metadata = $this->manager ->metadataFactory ->getMetadata($this->entityClass); - return $this - ->manager - ->execute($this->graphql, hydration: function ($result, $context) use ($metadata) { + return $this->manager->execute( + $this->graphql, + function ($result, $context) use ($metadata) { $dialect = $this->manager->getDialect(); $data = $result['data'] ?? []; if (!\array_key_exists($metadata->name, $data)) { - return []; + return $this->options->paginate + ? new PaginatedResult([], false, false, null, [], fn () => null) + : []; } $root = $data[$metadata->name]; @@ -64,30 +67,47 @@ public function getResult(): array } $collection = $dialect->extractCollection($root); - $rows = !empty($collection) ? $collection : null; - - if ($rows === null) { - return []; - } - if (!\is_array($rows)) { - throw InvalidGraphqlResponseException::expectedArray($rows); + if (array_is_list($collection)) { + $rows = $collection; + } else { + $rows = [$collection]; } if (array_is_list($rows) === false) { $rows = [$rows]; } - return array_map(fn ($row) => $this->manager + $items = array_map( + fn ($row) => $this->manager ->hydrator - ->hydrate( - $metadata, - $row, - $context - ), + ->hydrate($metadata, $row, $context), $rows ); - }); + + if (!$this->options->paginate) { + return $items; + } + + $fetchPage = function (?string $cursor, array $newStack) { + $qb = clone $this; + $qb->options->cursor = $cursor; + $qb->options->cursorStack = $newStack; + + return $qb->getResult(); + }; + + return new PaginatedResult( + $items, + $root['hasNextPage'] ?? false, + \count($this->options->cursorStack) > 0, + $root['endCursor'] ?? null, + $this->options->cursorStack, + $fetchPage + ); + }, + $this->options + ); } /** @@ -95,6 +115,12 @@ public function getResult(): array */ public function getOneOrNullResult(): ?object { - return $this->getResult()[0] ?? null; + $result = $this->getResult(); + + if ($result instanceof PaginatedResult) { + return $result->items[0] ?? null; + } + + return $result[0] ?? null; } } diff --git a/src/Query/GraphqlQueryBuilder.php b/src/Query/GraphqlQueryBuilder.php index 0924c47..0e6a563 100644 --- a/src/Query/GraphqlQueryBuilder.php +++ b/src/Query/GraphqlQueryBuilder.php @@ -85,6 +85,21 @@ public function orderBy(string $orderBy, Direction $direction): self return $this; } + /** + * @return GraphqlQueryBuilder + */ + public function paginate(?string $after = null): self + { + $this->options->paginate = true; + + if ($after !== null) { + $this->options->cursor = $after; + $this->options->cursorStack[] = $after; + } + + return $this; + } + public function expr(): ExpressionBuilder { return new ExpressionBuilder(); @@ -109,7 +124,8 @@ public function getQuery(): GraphqlQuery return new GraphqlQuery( $this->graphql, $this->entityClass, - $this->manager + $this->manager, + $this->options ); } @@ -137,7 +153,8 @@ public function getQuery(): GraphqlQuery return new GraphqlQuery( $ast, $this->entityClass, - $this->manager + $this->manager, + $this->options ); } } diff --git a/src/Query/GraphqlQueryCompiler.php b/src/Query/GraphqlQueryCompiler.php index 0a4be52..d6ed3ef 100644 --- a/src/Query/GraphqlQueryCompiler.php +++ b/src/Query/GraphqlQueryCompiler.php @@ -14,8 +14,8 @@ public function __construct( ) { } - public function compile(QueryNode $node): string + public function compile(QueryNode $node, QueryOptions $options): string { - return $this->walker->walk($node); + return $this->walker->walk($node, $options); } } diff --git a/src/Query/Pagination/PaginatedResult.php b/src/Query/Pagination/PaginatedResult.php new file mode 100644 index 0000000..ca41ec2 --- /dev/null +++ b/src/Query/Pagination/PaginatedResult.php @@ -0,0 +1,58 @@ +|null + */ + public function next(): ?self + { + if (!$this->hasNextPage) { + return null; + } + + $newStack = [ + ...$this->cursorStack, + $this->endCursor, + ]; + + return ($this->fetchPage)($this->endCursor, $newStack); + } + + /** + * @return self|null + */ + public function previous(): ?self + { + if (!$this->hasPreviousPage) { + return null; + } + + $newStack = $this->cursorStack; + array_pop($newStack); + $previousCursor = empty($newStack) ? null : end($newStack); + + return ($this->fetchPage)($previousCursor, $newStack); + } +} diff --git a/src/Query/QueryOptions.php b/src/Query/QueryOptions.php index 02bda5d..380913e 100644 --- a/src/Query/QueryOptions.php +++ b/src/Query/QueryOptions.php @@ -14,4 +14,11 @@ final class QueryOptions /** @var array|null */ public ?array $orderBy = null; + + public bool $paginate = false; + + public ?string $cursor = null; + + /** @var string[] */ + public array $cursorStack = []; } diff --git a/src/Query/Walker/DABGraphqlWalker.php b/src/Query/Walker/DABGraphqlWalker.php index e7e76a0..ed5e0e2 100644 --- a/src/Query/Walker/DABGraphqlWalker.php +++ b/src/Query/Walker/DABGraphqlWalker.php @@ -9,11 +9,16 @@ use GraphqlOrm\Query\Ast\QueryNode; use GraphqlOrm\Query\Direction; use GraphqlOrm\Query\Printer\GraphqlPrinter; +use GraphqlOrm\Query\QueryOptions; final class DABGraphqlWalker extends AbstractGraphqlWalker { - public function walk(QueryNode $query): string + private QueryOptions $options; + + public function walk(QueryNode $query, QueryOptions $options): string { + $this->options = $options; + $this->printer = new GraphqlPrinter(); $this->printer->line($query->operation . ' {'); @@ -33,7 +38,7 @@ public function walk(QueryNode $query): string private function walkRootField(FieldNode $field): void { - $args = $this->formatArguments($field->arguments); + $args = $this->formatArguments($this->applyPaginationArguments($field->arguments)); $this->printer->line($field->name . $args . ' {'); @@ -51,11 +56,38 @@ private function walkRootField(FieldNode $field): void $this->printer->line('}'); + if ($this->options->paginate) { + $this->printer->line('hasNextPage'); + $this->printer->line('endCursor'); + } + $this->printer->outdent(); $this->printer->line('}'); } + /** + * @param array $arguments + * + * @return array + */ + private function applyPaginationArguments(array $arguments): array + { + if (!$this->options->paginate) { + return $arguments; + } + + if ($this->options->limit !== null) { + $arguments['first'] = $this->options->limit; + } + + if ($this->options->cursor !== null) { + $arguments['after'] = $this->options->cursor; + } + + return $arguments; + } + protected function formatValue(mixed $value): string { if (\is_string($value)) { diff --git a/src/Query/Walker/DefaultGraphqlWalker.php b/src/Query/Walker/DefaultGraphqlWalker.php index cda336b..fbcc9e2 100644 --- a/src/Query/Walker/DefaultGraphqlWalker.php +++ b/src/Query/Walker/DefaultGraphqlWalker.php @@ -7,10 +7,11 @@ use GraphqlOrm\Exception\InvalidArgumentException; use GraphqlOrm\Query\Ast\QueryNode; use GraphqlOrm\Query\Printer\GraphqlPrinter; +use GraphqlOrm\Query\QueryOptions; final class DefaultGraphqlWalker extends AbstractGraphqlWalker { - public function walk(QueryNode $query): string + public function walk(QueryNode $query, QueryOptions $options): string { $this->printer = new GraphqlPrinter(); diff --git a/src/Query/Walker/GraphqlWalkerInterface.php b/src/Query/Walker/GraphqlWalkerInterface.php index 21a6383..f43da37 100644 --- a/src/Query/Walker/GraphqlWalkerInterface.php +++ b/src/Query/Walker/GraphqlWalkerInterface.php @@ -5,8 +5,9 @@ namespace GraphqlOrm\Query\Walker; use GraphqlOrm\Query\Ast\QueryNode; +use GraphqlOrm\Query\QueryOptions; interface GraphqlWalkerInterface { - public function walk(QueryNode $query): string; + public function walk(QueryNode $query, QueryOptions $options): string; } diff --git a/src/Repository/GraphqlEntityRepository.php b/src/Repository/GraphqlEntityRepository.php index ea0fe38..37aebaf 100644 --- a/src/Repository/GraphqlEntityRepository.php +++ b/src/Repository/GraphqlEntityRepository.php @@ -60,7 +60,8 @@ public function findBy(array $criteria): array ->fields($fields) ->build(); - return $this + /** @var T[] $result */ + $result = $this ->manager ->execute($graphql, hydration: function (array $result, GraphqlExecutionContext $context) use ($metadata) { $dialect = $this->manager->getDialect(); @@ -106,6 +107,8 @@ public function findBy(array $criteria): array $rows ); }); + + return $result; } /** diff --git a/tests/Fixtures/FakeGraphqlClient.php b/tests/Fixtures/FakeGraphqlClient.php index ce4eebc..866d2a8 100644 --- a/tests/Fixtures/FakeGraphqlClient.php +++ b/tests/Fixtures/FakeGraphqlClient.php @@ -10,16 +10,18 @@ final class FakeGraphqlClient implements GraphqlClientInterface { public string $lastQuery = ''; + private int $callCount = 0; + private array $responses; - public function __construct( - private readonly array $response, - ) { + public function __construct(array ...$responses) + { + $this->responses = $responses; } public function query(string $query, GraphqlExecutionContext $context, array $variables = []): array { $this->lastQuery = $query; - return $this->response; + return $this->responses[$this->callCount++] ?? []; } } diff --git a/tests/Query/GraphqlQueryStringBuilderTest.php b/tests/Query/GraphqlQueryStringBuilderTest.php index 4aa5840..486159e 100644 --- a/tests/Query/GraphqlQueryStringBuilderTest.php +++ b/tests/Query/GraphqlQueryStringBuilderTest.php @@ -13,6 +13,7 @@ use GraphqlOrm\Metadata\GraphqlEntityMetadataFactory; use GraphqlOrm\Metadata\GraphqlFieldMetadata; use GraphqlOrm\Query\GraphqlQueryStringBuilder; +use GraphqlOrm\Query\QueryOptions; use GraphqlOrm\Tests\Fixtures\FakeEntity\Task; use GraphqlOrm\Tests\Fixtures\FakeEntity\User; use GraphqlOrm\Tests\Fixtures\FakeRepository\TaskRepository; @@ -37,7 +38,7 @@ public function testBuildWithArgumentsFormatting(): void ]) ->build(); - $query = $manager->getQueryCompiler()->compile($query); + $query = $manager->getQueryCompiler()->compile($query, new QueryOptions()); self::assertStringContainsString('task(id: 1, active: true, status: "OPEN", tags: ["a", "b"], nullable: null)', $query); } @@ -52,7 +53,7 @@ public function testManualSelectSimpleField(): void ->fields(['title'], true) ->build(); - $query = $manager->getQueryCompiler()->compile($query); + $query = $manager->getQueryCompiler()->compile($query, new QueryOptions()); self::assertSame( <<build(); - $query = $manager->getQueryCompiler()->compile($query); + $query = $manager->getQueryCompiler()->compile($query, new QueryOptions()); self::assertSame( <<fields(['user'], true) ->build(); - $query = $manager->getQueryCompiler()->compile($query); + $query = $manager->getQueryCompiler()->compile($query, new QueryOptions()); self::assertSame( <<fields(['manager'], true) ->build(); - $query = $manager->getQueryCompiler()->compile($query); + $query = $manager->getQueryCompiler()->compile($query, new QueryOptions()); self::assertStringContainsString( <<entity(User::class) ->build(); - $query = $manager->getQueryCompiler()->compile($query); + $query = $manager->getQueryCompiler()->compile($query, new QueryOptions()); self::assertStringContainsString( <<fields(['customGraphqlField'], true) ->build(); - $query = $manager->getQueryCompiler()->compile($query); + $query = $manager->getQueryCompiler()->compile($query, new QueryOptions()); self::assertStringContainsString('customGraphqlField', $query); } @@ -215,7 +216,7 @@ public function testFormatValueThrowsOnObject(): void ]) ->build(); - $manager->getQueryCompiler()->compile($query); + $manager->getQueryCompiler()->compile($query, new QueryOptions()); } private function createManager(): GraphqlManager diff --git a/tests/Query/GraphqlQueryTest.php b/tests/Query/GraphqlQueryTest.php index 208e1a2..6563bae 100644 --- a/tests/Query/GraphqlQueryTest.php +++ b/tests/Query/GraphqlQueryTest.php @@ -11,6 +11,7 @@ use GraphqlOrm\Metadata\GraphqlEntityMetadata; use GraphqlOrm\Metadata\GraphqlEntityMetadataFactory; use GraphqlOrm\Query\GraphqlQuery; +use GraphqlOrm\Query\QueryOptions; use GraphqlOrm\Tests\Fixtures\FakeEntity\FakeEntity; use GraphqlOrm\Tests\Fixtures\FakeRepository\FakeRepository; use PHPUnit\Framework\TestCase; @@ -19,7 +20,7 @@ final class GraphqlQueryTest extends TestCase { public function testGetGraphQL(): void { - $query = new GraphqlQuery('query { tasks { id } }', FakeEntity::class, $this->createManager([])); + $query = new GraphqlQuery('query { tasks { id } }', FakeEntity::class, $this->createManager([]), new QueryOptions()); self::assertSame('query { tasks { id } }', $query->getGraphQL()); } @@ -30,7 +31,7 @@ public function testReturnsEmptyWhenNoData(): void 'data' => [], ]); - $query = new GraphqlQuery('query', FakeEntity::class, $manager); + $query = new GraphqlQuery('query', FakeEntity::class, $manager, new QueryOptions()); self::assertSame([], $query->getResult()); } @@ -45,7 +46,7 @@ public function testThrowsWhenInvalidResponse(): void ], ]); - (new GraphqlQuery('query', FakeEntity::class, $manager))->getResult(); + (new GraphqlQuery('query', FakeEntity::class, $manager, new QueryOptions()))->getResult(); } public function testHydratesSingleObject(): void @@ -66,7 +67,7 @@ public function testHydratesSingleObject(): void ], ], $hydrator); - $query = new GraphqlQuery('query', FakeEntity::class, $manager); + $query = new GraphqlQuery('query', FakeEntity::class, $manager, new QueryOptions()); self::assertSame([$stdClass], $query->getResult()); } @@ -91,7 +92,7 @@ public function testHydratesList(): void ], ], $hydrator); - $query = new GraphqlQuery('query', FakeEntity::class, $manager); + $query = new GraphqlQuery('query', FakeEntity::class, $manager, new QueryOptions()); self::assertSame([$stdClass1, $stdClass2], $query->getResult()); } @@ -111,7 +112,7 @@ public function testGetOneOrNullResult(): void ], ], $hydrator); - $query = new GraphqlQuery('query', FakeEntity::class, $manager); + $query = new GraphqlQuery('query', FakeEntity::class, $manager, new QueryOptions()); self::assertSame($stdClass, $query->getOneOrNullResult()); } @@ -122,7 +123,7 @@ public function testGetOneOrNullResultReturnsNull(): void 'data' => [], ]); - $query = new GraphqlQuery('query', FakeEntity::class, $manager); + $query = new GraphqlQuery('query', FakeEntity::class, $manager, new QueryOptions()); self::assertNull($query->getOneOrNullResult()); } diff --git a/tests/Query/Walker/DABGraphqlWalkerTest.php b/tests/Query/Walker/DABGraphqlWalkerTest.php index edc2036..ea641bd 100644 --- a/tests/Query/Walker/DABGraphqlWalkerTest.php +++ b/tests/Query/Walker/DABGraphqlWalkerTest.php @@ -8,6 +8,7 @@ use GraphqlOrm\Query\Ast\QueryNode; use GraphqlOrm\Query\Ast\SelectionSetNode; use GraphqlOrm\Query\GraphqlQueryCompiler; +use GraphqlOrm\Query\QueryOptions; use GraphqlOrm\Query\Walker\DABGraphqlWalker; use PHPUnit\Framework\TestCase; @@ -23,7 +24,7 @@ public function testWrapsItems(): void $query->fields[] = new FieldNode('tasks', selectionSet: $selection); $walker = new DABGraphqlWalker(); - $graphql = $walker->walk($query); + $graphql = $walker->walk($query, new QueryOptions()); self::assertSame( <<compile($query); + $graphql = $compiler->compile($query, new QueryOptions()); self::assertStringContainsString('tasks', $graphql); } diff --git a/tests/Query/Walker/DefaultGraphqlWalkerTest.php b/tests/Query/Walker/DefaultGraphqlWalkerTest.php index 8b70ca9..67fa09f 100644 --- a/tests/Query/Walker/DefaultGraphqlWalkerTest.php +++ b/tests/Query/Walker/DefaultGraphqlWalkerTest.php @@ -9,6 +9,7 @@ use GraphqlOrm\Query\Ast\QueryNode; use GraphqlOrm\Query\Ast\SelectionSetNode; use GraphqlOrm\Query\GraphqlQueryCompiler; +use GraphqlOrm\Query\QueryOptions; use GraphqlOrm\Query\Walker\DefaultGraphqlWalker; use PHPUnit\Framework\TestCase; @@ -25,7 +26,7 @@ public function testSimpleQuery(): void $query->fields[] = new FieldNode(name: 'tasks', selectionSet: $selection); $walker = new DefaultGraphqlWalker(); - $graphql = $walker->walk($query); + $graphql = $walker->walk($query, new QueryOptions()); self::assertSame( <<fields[] = new FieldNode('tasks', selectionSet: $taskSelection); $walker = new DefaultGraphqlWalker(); - $graphql = $walker->walk($query); + $graphql = $walker->walk($query, new QueryOptions()); self::assertSame( <<walk($query); + $graphql = $walker->walk($query, new QueryOptions()); self::assertStringContainsString('task(id: 1, active: true)', $graphql); } @@ -132,7 +133,7 @@ public function testCompilerUsesWalker(): void $compiler = new GraphqlQueryCompiler(new DefaultGraphqlWalker()); - $graphql = $compiler->compile($query); + $graphql = $compiler->compile($query, new QueryOptions()); self::assertStringContainsString('tasks', $graphql); } diff --git a/tests/QueryBuilderEndToEndTest.php b/tests/QueryBuilderEndToEndTest.php index a0b34da..7b79a21 100644 --- a/tests/QueryBuilderEndToEndTest.php +++ b/tests/QueryBuilderEndToEndTest.php @@ -10,6 +10,7 @@ use GraphqlOrm\Hydrator\EntityHydrator; use GraphqlOrm\Metadata\GraphqlEntityMetadataFactory; use GraphqlOrm\Query\Direction; +use GraphqlOrm\Query\Pagination\PaginatedResult; use GraphqlOrm\Repository\GraphqlEntityRepository; use GraphqlOrm\Tests\Fixtures\Entity\Task; use GraphqlOrm\Tests\Fixtures\FakeGraphqlClient; @@ -84,4 +85,134 @@ public function testFilteringPaginationOrderingAndRelations(): void self::assertStringContainsString('user {', $query); self::assertStringContainsString('items {', $query); } + + public function testPaginationNext(): void + { + $client = new FakeGraphqlClient( + [ + 'data' => [ + 'tasks' => [ + 'items' => [ + ['id' => 1, 'title' => 'Task 1'], + ], + 'hasNextPage' => true, + 'endCursor' => 'cursor1', + ], + ], + ], + [ + 'data' => [ + 'tasks' => [ + 'items' => [ + ['id' => 2, 'title' => 'Task 2'], + ], + 'hasNextPage' => false, + 'endCursor' => 'cursor2', + ], + ], + ], + ); + + $manager = $this->createManager($client); + $manager->dialect = new DataApiBuilderDialect(); + + $repo = new GraphqlEntityRepository($manager, Task::class); + + $page1 = $repo->createQueryBuilder() + ->limit(1) + ->paginate() + ->getQuery() + ->getResult(); + self::assertTrue($page1->hasNextPage); + self::assertCount(1, $page1->items); + self::assertSame(1, $page1->items[0]->id); + + $page2 = $page1->next(); + self::assertNotNull($page2); + self::assertFalse($page2->hasNextPage); + self::assertSame(2, $page2->items[0]->id); + + self::assertStringContainsString('after: "cursor1"', $client->lastQuery); + } + + public function testPaginationPrevious(): void + { + $client = new FakeGraphqlClient( + [ + 'data' => [ + 'tasks' => [ + 'items' => [ + ['id' => 1, 'title' => 'Task 1'], + ], + 'hasNextPage' => true, + 'endCursor' => 'cursor1', + ], + ], + ], + [ + 'data' => [ + 'tasks' => [ + 'items' => [ + ['id' => 2, 'title' => 'Task 2'], + ], + 'hasNextPage' => true, + 'endCursor' => 'cursor2', + ], + ], + ], + [ + 'data' => [ + 'tasks' => [ + 'items' => [ + ['id' => 1, 'title' => 'Task 1'], + ], + 'hasNextPage' => true, + 'endCursor' => 'cursor1', + ], + ], + ], + ); + + $manager = $this->createManager($client); + $manager->dialect = new DataApiBuilderDialect(); + + $repo = new GraphqlEntityRepository($manager, Task::class); + + $page1 = $repo->createQueryBuilder() + ->limit(1) + ->paginate() + ->getQuery() + ->getResult(); + + $page2 = $page1->next(); + $page1Again = $page2->previous(); + + self::assertSame(1, $page1Again->items[0]->id); + } + + public function testPaginateReturnsPaginatedResult(): void + { + $client = new FakeGraphqlClient([ + 'data' => [ + 'tasks' => [ + 'items' => [], + 'hasNextPage' => false, + 'endCursor' => null, + ], + ], + ]); + + $manager = $this->createManager($client); + $manager->dialect = new DataApiBuilderDialect(); + + $repo = new GraphqlEntityRepository($manager, Task::class); + + $result = $repo->createQueryBuilder() + ->limit(10) + ->paginate() + ->getQuery() + ->getResult(); + + self::assertInstanceOf(PaginatedResult::class, $result); + } }