Skip to content

Commit f89d557

Browse files
authored
Merge pull request #1180 from Davidnadejdin/master
Add cursor pagination type
2 parents 842cef8 + d3c4980 commit f89d557

File tree

9 files changed

+220
-2
lines changed

9 files changed

+220
-2
lines changed

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
CHANGELOG
22
=========
33

4-
[Next release](https://github.com/rebing/graphql-laravel/compare/9.9.0...master)
4+
[Next release](https://github.com/rebing/graphql-laravel/compare/9.10.0...master)
5+
6+
2025-05-17, 9.10.0
7+
------------------
8+
9+
## Added
10+
- Support Laravels cursor pagination [\#1180 / Davidnadejdin](https://github.com/rebing/graphql-laravel/pull/1180)
511

612
2025-02-24, 9.9.0
713
-----------------

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1895,6 +1895,37 @@ class PostsQuery extends Query
18951895
}
18961896
```
18971897

1898+
[Cursor Pagination](https://laravel.com/docs/pagination#cursor-pagination) will be used, if a query or mutation returns a `CursorPaginationType`.
1899+
1900+
```php
1901+
namespace App\GraphQL\Queries;
1902+
1903+
use Closure;
1904+
use GraphQL\Type\Definition\ResolveInfo;
1905+
use GraphQL\Type\Definition\Type;
1906+
use Rebing\GraphQL\Support\Facades\GraphQL;
1907+
use Rebing\GraphQL\Support\Query;
1908+
1909+
class PostsQuery extends Query
1910+
{
1911+
public function type(): Type
1912+
{
1913+
return GraphQL::cursorPaginate('posts');
1914+
}
1915+
1916+
// ...
1917+
1918+
public function resolve($root, array $args, $context, ResolveInfo $info, Closure $getSelectFields)
1919+
{
1920+
$fields = $getSelectFields();
1921+
1922+
return Post::with($fields->getRelations())
1923+
->select($fields->getSelect())
1924+
->cursorPaginate($args['limit'], ['*'], 'cursorName', $args['cursor']);
1925+
}
1926+
}
1927+
```
1928+
18981929
### Batching
18991930

19001931
Batched requests are required to be sent via a POST request.

config/config.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,12 @@
152152
*/
153153
'simple_pagination_type' => Rebing\GraphQL\Support\SimplePaginationType::class,
154154

155+
/*
156+
* You can define your own cursor pagination type.
157+
* Reference Rebing\GraphQL\Support\CursorPaginationType::class
158+
*/
159+
'cursor_pagination_type' => Rebing\GraphQL\Support\CursorPaginationType::class,
160+
155161
/*
156162
* Overrides the default field resolver
157163
* See http://webonyx.github.io/graphql-php/data-fetching/#default-field-resolver

phpstan-baseline.neon

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,18 @@ parameters:
120120
count: 1
121121
path: src/Support/AliasArguments/ArrayKeyChange.php
122122

123+
-
124+
message: '#^Call to an undefined method Illuminate\\Contracts\\Pagination\\CursorPaginator\:\:getCollection\(\)\.$#'
125+
identifier: method.notFound
126+
count: 1
127+
path: src/Support/CursorPaginationType.php
128+
129+
-
130+
message: '#^Parameter \#1 \$type of static method GraphQL\\Type\\Definition\\Type\:\:nonNull\(\) expects \(callable\(\)\: \(GraphQL\\Type\\Definition\\NullableType&GraphQL\\Type\\Definition\\Type\)\)\|\(GraphQL\\Type\\Definition\\NullableType&GraphQL\\Type\\Definition\\Type\), GraphQL\\Type\\Definition\\Type given\.$#'
131+
identifier: argument.type
132+
count: 1
133+
path: src/Support/CursorPaginationType.php
134+
123135
-
124136
message: '#^Parameter \#1 \$config of class GraphQL\\Type\\Definition\\EnumType constructor expects array\{name\?\: string\|null, description\?\: string\|null, values\: \(callable\(\)\: iterable\<int\|string, mixed\>\)\|iterable\<int\|string, mixed\>, astNode\?\: GraphQL\\Language\\AST\\EnumTypeDefinitionNode\|null, extensionASTNodes\?\: array\<GraphQL\\Language\\AST\\EnumTypeExtensionNode\>\|null\}, array\<string, mixed\> given\.$#'
125137
identifier: argument.type
@@ -1332,6 +1344,24 @@ parameters:
13321344
count: 1
13331345
path: tests/Support/Objects/ExamplesQuery.php
13341346

1347+
-
1348+
message: '#^Method Rebing\\GraphQL\\Tests\\Support\\Queries\\PostNonNullCursorPaginationQuery\:\:resolve\(\) has parameter \$args with no type specified\.$#'
1349+
identifier: missingType.parameter
1350+
count: 1
1351+
path: tests/Support/Queries/PostNonNullCursorPaginationQuery.php
1352+
1353+
-
1354+
message: '#^Method Rebing\\GraphQL\\Tests\\Support\\Queries\\PostNonNullCursorPaginationQuery\:\:resolve\(\) has parameter \$context with no type specified\.$#'
1355+
identifier: missingType.parameter
1356+
count: 1
1357+
path: tests/Support/Queries/PostNonNullCursorPaginationQuery.php
1358+
1359+
-
1360+
message: '#^Method Rebing\\GraphQL\\Tests\\Support\\Queries\\PostNonNullCursorPaginationQuery\:\:resolve\(\) has parameter \$root with no type specified\.$#'
1361+
identifier: missingType.parameter
1362+
count: 1
1363+
path: tests/Support/Queries/PostNonNullCursorPaginationQuery.php
1364+
13351365
-
13361366
message: '#^Method Rebing\\GraphQL\\Tests\\Support\\Queries\\PostNonNullPaginationQuery\:\:resolve\(\) has parameter \$args with no type specified\.$#'
13371367
identifier: missingType.parameter

src/GraphQL.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
use Rebing\GraphQL\Exception\TypeNotFound;
2929
use Rebing\GraphQL\Support\Contracts\ConfigConvertible;
3030
use Rebing\GraphQL\Support\Contracts\TypeConvertible;
31+
use Rebing\GraphQL\Support\CursorPaginationType;
3132
use Rebing\GraphQL\Support\ExecutionMiddleware\GraphqlExecutionMiddleware;
3233
use Rebing\GraphQL\Support\Field;
3334
use Rebing\GraphQL\Support\OperationParams;
@@ -511,6 +512,18 @@ public function simplePaginate(string $typeName, ?string $customName = null): Ty
511512
return $this->typesInstances[$name];
512513
}
513514

515+
public function cursorPaginate(string $typeName, ?string $customName = null): Type
516+
{
517+
$name = $customName ?: $typeName . 'CursorPagination';
518+
519+
if (!isset($this->typesInstances[$name])) {
520+
$paginationType = $this->config->get('graphql.cursor_pagination_type', CursorPaginationType::class);
521+
$this->wrapType($typeName, $name, $paginationType);
522+
}
523+
524+
return $this->typesInstances[$name];
525+
}
526+
514527
/**
515528
* To add customs result to the query or mutations.
516529
*
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
namespace Rebing\GraphQL\Support;
5+
6+
use GraphQL\Type\Definition\ObjectType;
7+
use GraphQL\Type\Definition\Type as GraphQLType;
8+
use Illuminate\Contracts\Pagination\CursorPaginator;
9+
use Illuminate\Support\Collection;
10+
use Rebing\GraphQL\Support\Facades\GraphQL;
11+
12+
class CursorPaginationType extends ObjectType
13+
{
14+
public function __construct(string $typeName, ?string $customName = null)
15+
{
16+
$name = $customName ?: $typeName . 'CursorPagination';
17+
18+
$underlyingType = GraphQL::type($typeName);
19+
20+
$config = [
21+
'name' => $name,
22+
'fields' => $this->getPaginationFields($underlyingType),
23+
];
24+
25+
if (isset($underlyingType->config['model'])) {
26+
$config['model'] = $underlyingType->config['model'];
27+
}
28+
29+
parent::__construct($config);
30+
}
31+
32+
/**
33+
* @return array<string, array<string,mixed>>
34+
*/
35+
protected function getPaginationFields(GraphQLType $underlyingType): array
36+
{
37+
return [
38+
'data' => [
39+
'type' => GraphQLType::nonNull(GraphQLType::listOf(GraphQLType::nonNull($underlyingType))),
40+
'description' => 'List of items on the current page',
41+
'resolve' => function (CursorPaginator $data): Collection {
42+
return $data->getCollection();
43+
},
44+
],
45+
'per_page' => [
46+
'type' => GraphQLType::nonNull(GraphQLType::int()),
47+
'description' => 'Number of items returned per page',
48+
'resolve' => function (CursorPaginator $data): int {
49+
return $data->perPage();
50+
},
51+
'selectable' => false,
52+
],
53+
'previous_cursor' => [
54+
'type' => GraphQLType::string(),
55+
'description' => 'Previous page cursor',
56+
'resolve' => function (CursorPaginator $data): ?string {
57+
return $data->previousCursor()?->encode();
58+
},
59+
'selectable' => false,
60+
],
61+
'next_cursor' => [
62+
'type' => GraphQLType::string(),
63+
'description' => 'Next page cursor',
64+
'resolve' => function (CursorPaginator $data): ?string {
65+
return $data->nextCursor()?->encode();
66+
},
67+
'selectable' => false,
68+
],
69+
];
70+
}
71+
}

src/Support/SelectFields.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ protected static function handleFields(
179179

180180
// Pagination
181181
if (is_a($parentType, Config::get('graphql.pagination_type', PaginationType::class)) ||
182-
is_a($parentType, Config::get('graphql.simple_pagination_type', SimplePaginationType::class))) {
182+
is_a($parentType, Config::get('graphql.simple_pagination_type', SimplePaginationType::class)) || is_a($parentType, Config::get('graphql.cursor_pagination_type', CursorPaginationType::class))) {
183183
/* @var GraphqlType $fieldType */
184184
$fieldType = $fieldObject->config['type'];
185185
static::handleFields(

tests/Database/SelectFieldsTest.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Rebing\GraphQL\Support\Facades\GraphQL;
99
use Rebing\GraphQL\Tests\Support\Models\Comment;
1010
use Rebing\GraphQL\Tests\Support\Models\Post;
11+
use Rebing\GraphQL\Tests\Support\Queries\PostNonNullCursorPaginationQuery;
1112
use Rebing\GraphQL\Tests\Support\Queries\PostNonNullPaginationQuery;
1213
use Rebing\GraphQL\Tests\Support\Queries\PostNonNullSimplePaginationQuery;
1314
use Rebing\GraphQL\Tests\Support\Queries\PostNonNullWithSelectFieldsAndModelQuery;
@@ -706,6 +707,35 @@ public function testPostNonNullPaginationTypes(): void
706707
self::assertStringContainsString($queryFragment, $gql);
707708
}
708709

710+
public function testPostNonNullCursorPaginationTypes(): void
711+
{
712+
$schema = GraphQL::buildSchemaFromConfig([
713+
'query' => [
714+
'postNonNullPaginationQuery' => PostNonNullCursorPaginationQuery::class,
715+
],
716+
]);
717+
718+
$gql = SchemaPrinter::doPrint($schema);
719+
720+
$queryFragment = <<<'GQL'
721+
type PostWithModelCursorPagination {
722+
"List of items on the current page"
723+
data: [PostWithModel!]!
724+
725+
"Number of items returned per page"
726+
per_page: Int!
727+
728+
"Previous page cursor"
729+
previous_cursor: String
730+
731+
"Next page cursor"
732+
next_cursor: String
733+
}
734+
GQL;
735+
736+
self::assertStringContainsString($queryFragment, $gql);
737+
}
738+
709739
public function testPostNonNullSimplePaginationQuery(): void
710740
{
711741
/** @var Post $post */
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
namespace Rebing\GraphQL\Tests\Support\Queries;
5+
6+
use Closure;
7+
use GraphQL\Type\Definition\ResolveInfo;
8+
use GraphQL\Type\Definition\Type;
9+
use Illuminate\Contracts\Pagination\CursorPaginator;
10+
use Rebing\GraphQL\Support\Facades\GraphQL;
11+
use Rebing\GraphQL\Support\Query;
12+
use Rebing\GraphQL\Tests\Support\Models\Post;
13+
14+
class PostNonNullCursorPaginationQuery extends Query
15+
{
16+
protected $attributes = [
17+
'name' => 'postNonNullCursorPaginationQuery',
18+
];
19+
20+
public function type(): Type
21+
{
22+
return GraphQL::cursorPaginate('PostWithModel');
23+
}
24+
25+
public function resolve($root, $args, $context, ResolveInfo $resolveInfo, Closure $getSelectFields): CursorPaginator
26+
{
27+
return Post::query()
28+
->select($getSelectFields()->getSelect())
29+
->cursorPaginate();
30+
}
31+
}

0 commit comments

Comments
 (0)