Browse Source

Cursor pagination support (#917)

* Ability to add cursor pagination

Extend model pagination with cursor pagination.

```
@apiResourceModel App\Models\User paginate=10,cursor
```

* Add tests for cursor pagination

* fix tests on older laravel versions

* fix per_page

---------

Co-authored-by: fikri-kompanion <101977604+fikri-kompanion@users.noreply.github.com>
Oliver Nybroe 5 months ago
parent
commit
46e8399edd

+ 1 - 0
src/Attributes/ResponseFromApiResource.php

@@ -22,6 +22,7 @@ class ResponseFromApiResource
 
         public ?int $paginate = null,
         public ?int $simplePaginate = null,
+        public ?int $cursorPaginate = null,
         public array $additional = [],
     )
     {

+ 5 - 0
src/Extracting/Shared/ApiResourceResponseTools.php

@@ -7,6 +7,7 @@ use Illuminate\Http\JsonResponse;
 use Illuminate\Http\Request;
 use Illuminate\Http\Resources\Json\JsonResource;
 use Illuminate\Http\Resources\Json\ResourceCollection;
+use Illuminate\Pagination\CursorPaginator;
 use Illuminate\Pagination\LengthAwarePaginator;
 use Illuminate\Pagination\Paginator;
 use Illuminate\Support\Arr;
@@ -86,6 +87,10 @@ class ApiResourceResponseTools
                 $perPage = $paginationStrategy[0];
                 $paginator = new Paginator($models, $perPage);
                 $list = $paginator;
+            } elseif (count($paginationStrategy) == 2 && $paginationStrategy[1] == 'cursor') {
+                $perPage = $paginationStrategy[0];
+                $paginator = new CursorPaginator($models, $perPage);
+                $list = $paginator;
             } else {
                 $list = collect($models);
             }

+ 2 - 0
src/Extracting/Strategies/Responses/UseResponseAttributes.php

@@ -68,6 +68,8 @@ class UseResponseAttributes extends PhpAttributeStrategy
             $pagination = [$attributeInstance->paginate];
         } else if ($attributeInstance->simplePaginate) {
             $pagination = [$attributeInstance->simplePaginate, 'simple'];
+        } else if ($attributeInstance->cursorPaginate) {
+            $pagination = [$attributeInstance->cursorPaginate, 'cursor'];
         }
 
 

+ 51 - 0
tests/Strategies/Responses/UseApiResourceTagsTest.php

@@ -3,6 +3,7 @@
 namespace Knuckles\Scribe\Tests\Strategies\Responses;
 
 use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Foundation\Application;
 use Illuminate\Routing\Route;
 use Illuminate\Support\Facades\Schema;
 use Knuckles\Camel\Extraction\ExtractedEndpointData;
@@ -798,4 +799,54 @@ class UseApiResourceTagsTest extends BaseLaravelTest
             ],
         ], $results);
     }
+
+    /** @test */
+    public function can_parse_apiresourcecollection_tags_with_collection_class_and_cursor_pagination()
+    {
+        $config = new DocumentationConfig([]);
+
+        $route = new Route(['POST'], "/somethingRandom", ['uses' => [TestController::class, 'dummy']]);
+
+        $strategy = new UseApiResourceTags($config);
+        $tags = [
+            new Tag('apiResourceCollection', '\Knuckles\Scribe\Tests\Fixtures\TestUserApiResourceCollection'),
+            new Tag('apiResourceModel', '\Knuckles\Scribe\Tests\Fixtures\TestUser paginate=1,cursor'),
+        ];
+        $results = $strategy->getApiResourceResponseFromTags($strategy->getApiResourceTag($tags), $tags, ExtractedEndpointData::fromRoute($route));
+
+        $nextCursor = base64_encode(json_encode(['_pointsToNextItems' => true]));
+        $this->assertArraySubset([
+            [
+                'status' => 200,
+                'content' => json_encode([
+                    'data' => [
+                        [
+                            'id' => 4,
+                            'name' => 'Tested Again',
+                            'email' => 'a@b.com',
+                        ],
+                    ],
+                    'links' => [
+                        'self' => 'link-value',
+                        "first" => null,
+                        "last" => null,
+                        "prev" => null,
+                        "next" => "/?cursor={$nextCursor}",
+                    ],
+                    "meta" => match (version_compare(Application::VERSION, '9.0', '>=')) {
+                        false => [
+                            "path" => '/',
+                            'per_page' => '1',
+                        ],
+                        true => [
+                            "path" => '/',
+                            'per_page' => 1,
+                            'next_cursor' => $nextCursor,
+                            'prev_cursor' => null,
+                        ]
+                    },
+                ]),
+            ],
+        ], $results);
+    }
 }

+ 63 - 0
tests/Strategies/Responses/UseResponseAttributesTest.php

@@ -2,6 +2,7 @@
 
 namespace Knuckles\Scribe\Tests\Strategies\Responses;
 
+use Illuminate\Foundation\Application;
 use Illuminate\Routing\Route;
 use Knuckles\Camel\Extraction\ExtractedEndpointData;
 use Knuckles\Scribe\Attributes\Response;
@@ -228,6 +229,62 @@ class UseResponseAttributesTest extends BaseLaravelTest
         ], $results);
     }
 
+    /** @test */
+    public function can_parse_apiresource_attributes_with_cursor_pagination()
+    {
+        $factory = app(\Illuminate\Database\Eloquent\Factory::class);
+        $factory->afterMaking(TestUser::class, function (TestUser $user, $faker) {
+            if ($user->id === 4) {
+                $child = Utils::getModelFactory(TestUser::class)->make(['id' => 5, 'parent_id' => 4]);
+                $user->setRelation('children', collect([$child]));
+            }
+        });
+
+        $results = $this->fetch($this->endpoint("apiResourceAttributesWithCursorPaginate"));
+
+
+        $nextCursor = base64_encode(json_encode(['_pointsToNextItems' => true]));
+        $this->assertArraySubset([
+            [
+                'status' => 200,
+                'content' => json_encode([
+                    'data' => [
+                        [
+                            'id' => 4,
+                            'name' => 'Tested Again',
+                            'email' => 'a@b.com',
+                            'children' => [
+                                [
+                                    'id' => 5,
+                                    'name' => 'Tested Again',
+                                    'email' => 'a@b.com',
+                                ],
+                            ],
+                        ],
+                    ],
+                    'links' => [
+                        "first" => null,
+                        "last" => null,
+                        "prev" => null,
+                        "next" => "/?cursor={$nextCursor}",
+                    ],
+                    "meta" => match (version_compare(Application::VERSION, '9.0', '>=')) {
+                        false => [
+                            "path" => '/',
+                            'per_page' => 1,
+                        ],
+                        true => [
+                            "path" => '/',
+                            'per_page' => 1,
+                            'next_cursor' => $nextCursor,
+                            'prev_cursor' => null,
+                        ]
+                    },
+                ]),
+            ],
+        ], $results);
+    }
+
     protected function fetch($endpoint): array
     {
         $strategy = new UseResponseAttributes(new DocumentationConfig([]));
@@ -282,4 +339,10 @@ class ResponseAttributesTestController
     {
 
     }
+
+    #[ResponseFromApiResource(TestUserApiResource::class, collection: true, cursorPaginate: 1)]
+    public function apiResourceAttributesWithCursorPaginate()
+    {
+
+    }
 }