Skip to content
Open
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
24 changes: 21 additions & 3 deletions src/Execution/ModelsLoader/PaginatedModelsLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,20 @@ public function extract(Model $model): mixed
*/
protected function loadRelatedModels(EloquentCollection $parents): EloquentCollection
{
if ($this->paginationArgs->first === null) {
$relation = $this->relationInstance($parents);
$relation->addEagerConstraints($parents->all());

($this->decorateBuilder)($relation);

if ($relation instanceof BelongsToMany || $relation instanceof HasManyThrough) {
$select = Utils::callProtected($relation, 'shouldSelect', ['*']);
$relation->addSelect($select);
}

return $relation->get();
}

$relations = $parents->toBase()
->map(function (Model $model) use ($parents): Relation {
$relation = $this->relationInstance($parents);
Expand Down Expand Up @@ -184,9 +198,13 @@ protected function convertRelationToPaginator(EloquentCollection $parents): void
foreach ($parents as $model) {
$total = CountModelsLoader::extractCount($model, $this->relation);

$paginator = $first === 0
? new ZeroPerPageLengthAwarePaginator($total, $page)
: new LengthAwarePaginator($model->getRelation($this->relation), $total, $first, $page);
if ($first === null) {
$paginator = new LengthAwarePaginator($model->getRelation($this->relation), $total, max(1, $total), $page);
} elseif ($first === 0) {
$paginator = new ZeroPerPageLengthAwarePaginator($total, $page);
} else {
$paginator = new LengthAwarePaginator($model->getRelation($this->relation), $total, $first, $page);
}

$model->setRelation($this->relation, $paginator);
}
Expand Down
27 changes: 20 additions & 7 deletions src/Pagination/PaginationArgs.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
namespace Nuwave\Lighthouse\Pagination;

use GraphQL\Error\Error;
use Illuminate\Contracts\Pagination\Paginator;
use Illuminate\Contracts\Pagination\Paginator as PaginatorContract;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Arr;
use Laravel\Scout\Builder as ScoutBuilder;
use Nuwave\Lighthouse\Cache\CacheDirective;
Expand All @@ -17,7 +19,7 @@ class PaginationArgs
{
public function __construct(
public int $page,
public int $first,
public ?int $first,
public PaginationType $type,
) {}

Expand All @@ -28,7 +30,7 @@ public function __construct(
*/
public static function extractArgs(array $args, ResolveInfo $resolveInfo, PaginationType $proposedPaginationType, ?int $paginateMaxCount): self
{
$first = $args['first'];
$first = $args['first'] ?? null;

$page = $proposedPaginationType->isConnection()
? self::calculateCurrentPage(
Expand All @@ -38,13 +40,14 @@ public static function extractArgs(array $args, ResolveInfo $resolveInfo, Pagina
// Handles cases "paginate" and "simple", which both take the same args.
: Arr::get($args, 'page') ?? 1;

if ($first < 0) {
if ($first !== null && $first < 0) {
throw new Error(self::requestedLessThanZeroItems($first));
}

// Make sure the maximum pagination count is not exceeded
if (
$paginateMaxCount !== null
$first !== null
&& $paginateMaxCount !== null
&& $first > $paginateMaxCount
) {
throw new Error(self::requestedTooManyItems($paginateMaxCount, $first));
Expand All @@ -66,7 +69,7 @@ public static function requestedTooManyItems(int $maxCount, int $actualCount): s
}

/** Calculate the current page to inform the user about the pagination state. */
protected static function calculateCurrentPage(int $first, int $after, int $defaultPage = 1): int
protected static function calculateCurrentPage(?int $first, int $after, int $defaultPage = 1): int
{
return $first && $after
? (int) floor(($first + $after) / $first)
Expand Down Expand Up @@ -108,8 +111,18 @@ protected static function optimalPaginationType(PaginationType $proposedType, Re
*
* @return \Illuminate\Contracts\Pagination\Paginator<array-key, TModel>
*/
public function applyToBuilder(QueryBuilder|ScoutBuilder|EloquentBuilder|Relation $builder): Paginator
public function applyToBuilder(QueryBuilder|ScoutBuilder|EloquentBuilder|Relation $builder): PaginatorContract
{
if ($this->first === null) {
$results = $builder->get();
$count = $results->count();

// @phpstan-ignore return.type (generic type does not matter)
return $this->type->isSimple()
? new Paginator($results, max(1, $count), $this->page)
: new LengthAwarePaginator($results, $count, max(1, $count), $this->page);
}

if ($this->first === 0) {
if ($this->type->isSimple()) {
return new ZeroPerPagePaginator($this->page); // @phpstan-ignore return.type (generic type does not matter)
Expand Down
2 changes: 1 addition & 1 deletion src/Pagination/PaginationManipulator.php
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ protected static function countArgument(?int $defaultCount = null, ?int $maxCoun

$description .= "\"\n";

$definition = 'first: Int!';
$definition = 'first: Int';
if ($defaultCount) {
$definition .= " = {$defaultCount}";
}
Expand Down
207 changes: 207 additions & 0 deletions tests/Integration/Pagination/PaginateDirectiveDBTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1051,4 +1051,211 @@ public function testSimplePaginationWithNullPageUsesDefaultPage(): void
],
])->assertJsonCount(2, 'data.users.data');
}

public function testPaginateWithoutFirst(): void
{
factory(User::class, 3)->create();

$this->schema = /** @lang GraphQL */ <<<'GRAPHQL'
type User {
id: ID!
}

type Query {
users: [User!]! @paginate
}
GRAPHQL;

$this->graphQL(/** @lang GraphQL */ <<<'GRAPHQL'
{
users {
paginatorInfo {
count
total
}
data {
id
}
}
}
GRAPHQL)->assertJson([
'data' => [
'users' => [
'paginatorInfo' => [
'count' => 3,
'total' => 3,
],
],
],
])->assertJsonCount(3, 'data.users.data');
}

public function testPaginateWithFirstNull(): void
{
factory(User::class, 3)->create();

$this->schema = /** @lang GraphQL */ <<<'GRAPHQL'
type User {
id: ID!
}

type Query {
users: [User!]! @paginate
}
GRAPHQL;

$this->graphQL(/** @lang GraphQL */ <<<'GRAPHQL'
{
users(first: null) {
paginatorInfo {
count
total
}
data {
id
}
}
}
GRAPHQL)->assertJson([
'data' => [
'users' => [
'paginatorInfo' => [
'count' => 3,
'total' => 3,
],
],
],
])->assertJsonCount(3, 'data.users.data');
}

public function testConnectionWithoutFirst(): void
{
factory(User::class, 3)->create();

$this->schema = /** @lang GraphQL */ <<<'GRAPHQL'
type User {
id: ID!
}

type Query {
users: [User!]! @paginate(type: CONNECTION)
}
GRAPHQL;

$this->graphQL(/** @lang GraphQL */ <<<'GRAPHQL'
{
users {
pageInfo {
count
total
hasNextPage
}
edges {
node {
id
}
}
}
}
GRAPHQL)->assertJson([
'data' => [
'users' => [
'pageInfo' => [
'count' => 3,
'total' => 3,
'hasNextPage' => false,
],
],
],
])->assertJsonCount(3, 'data.users.edges');
}

public function testNestedConnectionWithoutFirst(): void
{
$user = factory(User::class)->create();
$posts = factory(Post::class, 3)->make();
$user->posts()->saveMany($posts);

$this->schema = /** @lang GraphQL */ <<<'GRAPHQL'
type Post {
id: ID!
}

type User {
id: ID!
posts: [Post!]! @hasMany(type: CONNECTION)
}

type Query {
user(id: ID! @eq): User @find
}
GRAPHQL;

$this->graphQL(/** @lang GraphQL */ <<<GRAPHQL
{
user(id: {$user->id}) {
posts {
pageInfo {
count
total
}
edges {
node {
id
}
}
}
}
}
GRAPHQL)->assertJson([
'data' => [
'user' => [
'posts' => [
'pageInfo' => [
'count' => 3,
'total' => 3,
],
],
],
],
])->assertJsonCount(3, 'data.user.posts.edges');
}

public function testPaginateSimpleWithoutFirst(): void
{
factory(User::class, 3)->create();

$this->schema = /** @lang GraphQL */ <<<'GRAPHQL'
type User {
id: ID!
}

type Query {
users: [User!]! @paginate(type: SIMPLE)
}
GRAPHQL;

$this->graphQL(/** @lang GraphQL */ <<<'GRAPHQL'
{
users {
paginatorInfo {
count
hasMorePages
}
data {
id
}
}
}
GRAPHQL)->assertJson([
'data' => [
'users' => [
'paginatorInfo' => [
'count' => 3,
'hasMorePages' => false,
],
],
],
])->assertJsonCount(3, 'data.users.data');
}
}
Loading