This commit is contained in:
Your Name
2021-07-26 19:46:18 +02:00
parent e7a49138bb
commit aae17f10a6
818 changed files with 70695 additions and 16408 deletions
+30 -24
View File
@@ -2,63 +2,69 @@
namespace Nuwave\Lighthouse\Pagination;
use Illuminate\Support\Collection;
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
class ConnectionField
{
/**
* Resolve page info for connection.
*
* @param \Illuminate\Contracts\Pagination\LengthAwarePaginator $paginator
* @return array
* @return array<string, mixed>
*/
public function pageInfoResolver(LengthAwarePaginator $paginator): array
{
/** @var int|null $firstItem Laravel type-hints are inaccurate here */
$firstItem = $paginator->firstItem();
/** @var int|null $lastItem Laravel type-hints are inaccurate here */
$lastItem = $paginator->lastItem();
return [
'hasNextPage' => $paginator->hasMorePages(),
'hasPreviousPage' => $paginator->currentPage() > 1,
'startCursor' => $firstItem !== null
? Cursor::encode($firstItem)
: null,
'endCursor' => $lastItem !== null
? Cursor::encode($lastItem)
: null,
'total' => $paginator->total(),
'count' => $paginator->count(),
'currentPage' => $paginator->currentPage(),
'lastPage' => $paginator->lastPage(),
'hasNextPage' => $paginator->hasMorePages(),
'hasPreviousPage' => $paginator->currentPage() > 1,
'startCursor' => $paginator->firstItem()
? Cursor::encode($paginator->firstItem())
: null,
'endCursor' => $paginator->lastItem()
? Cursor::encode($paginator->lastItem())
: null,
];
}
/**
* Resolve edges for connection.
*
* @param \Illuminate\Contracts\Pagination\LengthAwarePaginator $paginator
* @param array $args
* @param \Nuwave\Lighthouse\Support\Contracts\GraphQLContext $context
* @param GraphQL\Type\Definition\ResolveInfo $resolveInfo
* @return \Illuminate\Support\Collection
* @param \Illuminate\Pagination\LengthAwarePaginator<mixed> $paginator
* @param array<string, mixed> $args
*/
public function edgeResolver(LengthAwarePaginator $paginator, $args, GraphQLContext $context, ResolveInfo $resolveInfo): Collection
public function edgeResolver(LengthAwarePaginator $paginator, array $args, GraphQLContext $context, ResolveInfo $resolveInfo): Collection
{
$returnTypeFields = $resolveInfo
->returnType
->ofType
->getFields();
// We know those types because we manipulated them during PaginationManipulator
/** @var \GraphQL\Type\Definition\NonNull $nonNullList */
$nonNullList = $resolveInfo->returnType;
/** @var \GraphQL\Type\Definition\ObjectType|\GraphQL\Type\Definition\InterfaceType $objectLikeType */
$objectLikeType = $nonNullList->getWrappedType(true);
$returnTypeFields = $objectLikeType->getFields();
/** @var int|null $firstItem Laravel type-hints are inaccurate here */
$firstItem = $paginator->firstItem();
return $paginator
->values()
->map(function ($item, $index) use ($firstItem, $returnTypeFields): array {
->map(function ($item, int $index) use ($returnTypeFields, $firstItem): array {
$data = [];
foreach ($returnTypeFields as $field) {
switch ($field->name) {
case 'cursor':
$data['cursor'] = Cursor::encode($firstItem + $index);
$data['cursor'] = Cursor::encode((int) $firstItem + $index);
break;
case 'node':
+3 -7
View File
@@ -22,13 +22,12 @@ class Cursor
* this will return 0. That will effectively reset pagination, so the user gets the
* first slice.
*
* @param array $args
* @return int
* @param array<string, mixed> $args
*/
public static function decode(array $args): int
{
if ($cursor = Arr::get($args, 'after')) {
return (int) base64_decode($cursor);
return (int) \Safe\base64_decode($cursor);
}
return 0;
@@ -36,12 +35,9 @@ class Cursor
/**
* Encode the given offset to make the implementation opaque.
*
* @param int $offset
* @return string
*/
public static function encode(int $offset): string
{
return base64_encode($offset);
return base64_encode((string) $offset);
}
}
@@ -0,0 +1,148 @@
<?php
namespace Nuwave\Lighthouse\Pagination;
use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use GraphQL\Type\Definition\ResolveInfo;
use Illuminate\Contracts\Pagination\Paginator;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Support\Contracts\FieldManipulator;
use Nuwave\Lighthouse\Support\Contracts\FieldResolver;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
class PaginateDirective extends BaseDirective implements FieldResolver, FieldManipulator
{
public static function definition(): string
{
return /** @lang GraphQL */ <<<'GRAPHQL'
"""
Query multiple model entries as a paginated list.
"""
directive @paginate(
"""
Which pagination style should be used.
"""
type: PaginateType = PAGINATOR
"""
Specify the class name of the model to use.
This is only needed when the default model detection does not work.
"""
model: String
"""
Point to a function that provides a Query Builder instance.
This replaces the use of a model.
"""
builder: String
"""
Apply scopes to the underlying query.
"""
scopes: [String!]
"""
Allow clients to query paginated lists without specifying the amount of items.
Overrules the `pagination.default_count` setting from `lighthouse.php`.
"""
defaultCount: Int
"""
Limit the maximum amount of items that clients can request from paginated lists.
Overrules the `pagination.max_count` setting from `lighthouse.php`.
"""
maxCount: Int
) on FIELD_DEFINITION
"""
Options for the `type` argument of `@paginate`.
"""
enum PaginateType {
"""
Offset-based pagination, similar to the Laravel default.
"""
PAGINATOR
"""
Offset-based pagination like the Laravel "Simple Pagination", which does not count the total number of records.
"""
SIMPLE
"""
Cursor-based pagination, compatible with the Relay specification.
"""
CONNECTION
}
GRAPHQL;
}
public function manipulateFieldDefinition(DocumentAST &$documentAST, FieldDefinitionNode &$fieldDefinition, ObjectTypeDefinitionNode &$parentType): void
{
$paginationManipulator = new PaginationManipulator($documentAST);
if ($this->directiveHasArgument('builder')) {
// This is done only for validation
$this->getResolverFromArgument('builder');
} else {
$paginationManipulator->setModelClass(
$this->getModelClass()
);
}
$paginationManipulator->transformToPaginatedField(
$this->paginationType(),
$fieldDefinition,
$parentType,
$this->defaultCount(),
$this->paginateMaxCount()
);
}
public function resolveField(FieldValue $fieldValue): FieldValue
{
return $fieldValue->setResolver(
function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo): Paginator {
if ($this->directiveHasArgument('builder')) {
$builderResolver = $this->getResolverFromArgument('builder');
$query = $builderResolver($root, $args, $context, $resolveInfo);
} else {
$query = $this->getModelClass()::query();
}
$query = $resolveInfo
->argumentSet
->enhanceBuilder(
$query,
$this->directiveArgValue('scopes', [])
);
return PaginationArgs
::extractArgs($args, $this->paginationType(), $this->paginateMaxCount())
->applyToBuilder($query);
}
);
}
protected function paginationType(): PaginationType
{
return new PaginationType(
$this->directiveArgValue('type', PaginationType::PAGINATOR)
);
}
protected function defaultCount(): ?int
{
return $this->directiveArgValue('defaultCount')
?? config('lighthouse.pagination.default_count');
}
protected function paginateMaxCount(): ?int
{
return $this->directiveArgValue('maxCount')
?? config('lighthouse.pagination.max_count');
}
}
@@ -0,0 +1,111 @@
<?php
namespace Nuwave\Lighthouse\Pagination;
use GraphQL\Error\Error;
use Illuminate\Contracts\Pagination\Paginator;
use Illuminate\Support\Arr;
use Laravel\Scout\Builder as ScoutBuilder;
class PaginationArgs
{
/**
* @var int
*/
public $page;
/**
* @var int
*/
public $first;
/**
* @var \Nuwave\Lighthouse\Pagination\PaginationType
*/
public $type;
/**
* Create a new instance from user given args.
*
* @param array<string, mixed> $args
* @param \Nuwave\Lighthouse\Pagination\PaginationType $paginationType
* @return static
*
* @throws \GraphQL\Error\Error
*/
public static function extractArgs(array $args, PaginationType $paginationType, ?int $paginateMaxCount): self
{
$instance = new static();
$instance->type = $paginationType;
if ($paginationType->isConnection()) {
$instance->first = $args['first'];
$instance->page = self::calculateCurrentPage(
$instance->first,
Cursor::decode($args)
);
} else {
// Handles cases "paginate" and "simple", which both take the same args.
$instance->first = $args['first'];
$instance->page = Arr::get($args, 'page', 1);
}
if ($instance->first <= 0) {
throw new Error(
self::requestedZeroOrLessItems($instance->first)
);
}
// Make sure the maximum pagination count is not exceeded
if (
$paginateMaxCount !== null
&& $instance->first > $paginateMaxCount
) {
throw new Error(
self::requestedTooManyItems($paginateMaxCount, $instance->first)
);
}
return $instance;
}
public static function requestedZeroOrLessItems(int $amount): string
{
return "Requested pagination amount must be more than 0, got {$amount}.";
}
public static function requestedTooManyItems(int $maxCount, int $actualCount): string
{
return "Maximum number of {$maxCount} requested items exceeded, got {$actualCount}. Fetch smaller chunks.";
}
/**
* Calculate the current page to inform the user about the pagination state.
*/
protected static function calculateCurrentPage(int $first, int $after, int $defaultPage = 1): int
{
return $first && $after
? (int) floor(($first + $after) / $first)
: $defaultPage;
}
/**
* Apply the args to a builder, constructing a paginator.
*
* @param \Illuminate\Database\Query\Builder|\Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $builder
*/
public function applyToBuilder(object $builder): Paginator
{
$methodName = $this->type->isSimple()
? 'simplePaginate'
: 'paginate';
if ($builder instanceof ScoutBuilder) {
return $builder->{$methodName}($this->first, 'page', $this->page);
}
// @phpstan-ignore-next-line Relation&Builder mixin not recognized
return $builder->{$methodName}($this->first, ['*'], 'page', $this->page);
}
}
@@ -2,15 +2,53 @@
namespace Nuwave\Lighthouse\Pagination;
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
use GraphQL\Language\AST\FieldDefinitionNode;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
use Nuwave\Lighthouse\Schema\AST\PartialParser;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use GraphQL\Language\AST\TypeNode;
use GraphQL\Language\Parser;
use Illuminate\Container\Container;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Nuwave\Lighthouse\Exceptions\DefinitionException;
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
use Nuwave\Lighthouse\Schema\Directives\ModelDirective;
class PaginationManipulator
{
/**
* @var \Nuwave\Lighthouse\Schema\AST\DocumentAST
*/
protected $documentAST;
/**
* The class name of the model that is returned from the field.
*
* Might not be present if we are creating a paginated field
* for a relation, as the model is not required for resolving
* that directive and the user may choose a different type.
*
* @var class-string<\Illuminate\Database\Eloquent\Model>|null
*/
protected $modelClass;
public function __construct(DocumentAST $documentAST)
{
$this->documentAST = $documentAST;
}
/**
* Set the model class to use for code generation.
*
* @param class-string<\Illuminate\Database\Eloquent\Model> $modelClass
* @return $this
*/
public function setModelClass(string $modelClass): self
{
$this->modelClass = $modelClass;
return $this;
}
/**
* Transform the definition for a field to a field with pagination.
*
@@ -18,53 +56,34 @@ class PaginationManipulator
* The types in between are automatically generated and applied to the schema.
*
* @param \Nuwave\Lighthouse\Pagination\PaginationType $paginationType
* @param \GraphQL\Language\AST\FieldDefinitionNode $fieldDefinition
* @param \GraphQL\Language\AST\ObjectTypeDefinitionNode $parentType
* @param \Nuwave\Lighthouse\Schema\AST\DocumentAST $documentAST
* @param int|null $defaultCount
* @param int|null $maxCount
* @param \GraphQL\Language\AST\ObjectTypeDefinitionNode|null $edgeType
* @return void
*/
public static function transformToPaginatedField(
public function transformToPaginatedField(
PaginationType $paginationType,
FieldDefinitionNode &$fieldDefinition,
ObjectTypeDefinitionNode &$parentType,
DocumentAST &$documentAST,
?int $defaultCount = null,
?int $maxCount = null,
?ObjectTypeDefinitionNode $edgeType = null
): void {
if ($paginationType->isConnection()) {
self::registerConnection($fieldDefinition, $parentType, $documentAST, $defaultCount, $maxCount, $edgeType);
$this->registerConnection($fieldDefinition, $parentType, $defaultCount, $maxCount, $edgeType);
} elseif ($paginationType->isSimple()) {
$this->registerSimplePaginator($fieldDefinition, $parentType, $defaultCount, $maxCount);
} else {
self::registerPaginator($fieldDefinition, $parentType, $documentAST, $defaultCount, $maxCount);
$this->registerPaginator($fieldDefinition, $parentType, $defaultCount, $maxCount);
}
}
/**
* Register connection w/ schema.
*
* @param \GraphQL\Language\AST\FieldDefinitionNode $fieldDefinition
* @param \GraphQL\Language\AST\ObjectTypeDefinitionNode $parentType
* @param \Nuwave\Lighthouse\Schema\AST\DocumentAST $documentAST
* @param int|null $defaultCount
* @param int|null $maxCount
* @param \GraphQL\Language\AST\ObjectTypeDefinitionNode|null $edgeType
* @return void
* @throws DefinitionException
*/
public static function registerConnection(
protected function registerConnection(
FieldDefinitionNode &$fieldDefinition,
ObjectTypeDefinitionNode &$parentType,
DocumentAST &$documentAST,
?int $defaultCount = null,
?int $maxCount = null,
?ObjectTypeDefinitionNode $edgeType = null
): void {
$fieldTypeName = ASTHelper::getUnderlyingTypeName($fieldDefinition);
if ($edgeType) {
if ($edgeType !== null) {
$connectionEdgeName = $edgeType->name->value;
$connectionTypeName = "{$connectionEdgeName}Connection";
} else {
@@ -74,51 +93,76 @@ class PaginationManipulator
$connectionFieldName = addslashes(ConnectionField::class);
$connectionType = PartialParser::objectTypeDefinition("
type $connectionTypeName {
pageInfo: PageInfo! @field(resolver: \"{$connectionFieldName}@pageInfoResolver\")
edges: [$connectionEdgeName] @field(resolver: \"{$connectionFieldName}@edgeResolver\")
$connectionType = Parser::objectTypeDefinition(/** @lang GraphQL */ <<<GRAPHQL
"A paginated list of {$fieldTypeName} edges."
type {$connectionTypeName} {
"Pagination information about the list of edges."
pageInfo: PageInfo! @field(resolver: "{$connectionFieldName}@pageInfoResolver")
"A list of $fieldTypeName edges."
edges: [{$connectionEdgeName}!]! @field(resolver: "{$connectionFieldName}@edgeResolver")
}
");
GRAPHQL
);
$this->addPaginationWrapperType($connectionType);
$connectionEdge = $edgeType
?? $documentAST->types[$connectionEdgeName]
?? PartialParser::objectTypeDefinition("
?? $this->documentAST->types[$connectionEdgeName]
?? Parser::objectTypeDefinition(/** @lang GraphQL */ <<<GRAPHQL
"An edge that contains a node of type $fieldTypeName and a cursor."
type $connectionEdgeName {
node: $fieldTypeName
"The $fieldTypeName node."
node: $fieldTypeName!
"A unique cursor that can be used for pagination."
cursor: String!
}
");
GRAPHQL
);
$this->documentAST->setTypeDefinition($connectionEdge);
$inputValueDefinitions = [
self::countArgument('first', $defaultCount, $maxCount),
"\"A cursor after which elements are returned.\"\nafter: String",
];
$fieldDefinition->arguments [] = Parser::inputValueDefinition(
self::countArgument($defaultCount, $maxCount)
);
$fieldDefinition->arguments [] = Parser::inputValueDefinition(/** @lang GraphQL */ <<<'GRAPHQL'
"A cursor after which elements are returned."
after: String
GRAPHQL
);
$connectionArguments = PartialParser::inputValueDefinitions($inputValueDefinitions);
$fieldDefinition->arguments = ASTHelper::mergeNodeList($fieldDefinition->arguments, $connectionArguments);
$fieldDefinition->type = PartialParser::namedType($connectionTypeName);
$parentType->fields = ASTHelper::mergeNodeList($parentType->fields, [$fieldDefinition]);
$documentAST->setTypeDefinition($connectionType);
$documentAST->setTypeDefinition($connectionEdge);
$fieldDefinition->type = $this->paginationResultType($connectionTypeName);
$parentType->fields = ASTHelper::mergeUniqueNodeList($parentType->fields, [$fieldDefinition], true);
}
/**
* Register paginator w/ schema.
*
* @param \GraphQL\Language\AST\FieldDefinitionNode $fieldDefinition
* @param \GraphQL\Language\AST\ObjectTypeDefinitionNode $parentType
* @param \Nuwave\Lighthouse\Schema\AST\DocumentAST $documentAST
* @param int|null $defaultCount
* @param int|null $maxCount
* @return void
*/
public static function registerPaginator(
protected function addPaginationWrapperType(ObjectTypeDefinitionNode $objectType): void
{
$typeName = $objectType->name->value;
// Reuse existing types to preserve directives or other modifications made to it
$existingType = $this->documentAST->types[$typeName] ?? null;
if ($existingType !== null) {
if (! $existingType instanceof ObjectTypeDefinitionNode) {
throw new DefinitionException(
"Expected object type for pagination wrapper {$typeName}, found {$objectType->kind} instead."
);
}
$objectType = $existingType;
}
if (
$this->modelClass
&& ! ASTHelper::hasDirective($objectType, ModelDirective::NAME)
) {
$objectType->directives [] = Parser::constDirective(/** @lang GraphQL */'@model(class: "'.addslashes($this->modelClass).'")');
}
$this->documentAST->setTypeDefinition($objectType);
}
protected function registerPaginator(
FieldDefinitionNode &$fieldDefinition,
ObjectTypeDefinitionNode &$parentType,
DocumentAST &$documentAST,
?int $defaultCount = null,
?int $maxCount = null
): void {
@@ -126,44 +170,80 @@ class PaginationManipulator
$paginatorTypeName = "{$fieldTypeName}Paginator";
$paginatorFieldClassName = addslashes(PaginatorField::class);
$paginatorType = PartialParser::objectTypeDefinition("
type $paginatorTypeName {
paginatorInfo: PaginatorInfo! @field(resolver: \"{$paginatorFieldClassName}@paginatorInfoResolver\")
data: [$fieldTypeName!]! @field(resolver: \"{$paginatorFieldClassName}@dataResolver\")
$paginatorType = Parser::objectTypeDefinition(/** @lang GraphQL */ <<<GRAPHQL
"A paginated list of {$fieldTypeName} items."
type {$paginatorTypeName} {
"Pagination information about the list of items."
paginatorInfo: PaginatorInfo! @field(resolver: "{$paginatorFieldClassName}@paginatorInfoResolver")
"A list of {$fieldTypeName} items."
data: [{$fieldTypeName}!]! @field(resolver: "{$paginatorFieldClassName}@dataResolver")
}
");
GRAPHQL
);
$this->addPaginationWrapperType($paginatorType);
$inputValueDefinitions = [
self::countArgument(config('lighthouse.pagination_amount_argument'), $defaultCount, $maxCount),
"\"The offset from which elements are returned.\"\npage: Int",
];
$fieldDefinition->arguments [] = Parser::inputValueDefinition(
self::countArgument($defaultCount, $maxCount)
);
$fieldDefinition->arguments [] = Parser::inputValueDefinition(/** @lang GraphQL */ <<<'GRAPHQL'
"The offset from which items are returned."
page: Int
GRAPHQL
);
$paginationArguments = PartialParser::inputValueDefinitions($inputValueDefinitions);
$fieldDefinition->type = $this->paginationResultType($paginatorTypeName);
$parentType->fields = ASTHelper::mergeUniqueNodeList($parentType->fields, [$fieldDefinition], true);
}
$fieldDefinition->arguments = ASTHelper::mergeNodeList($fieldDefinition->arguments, $paginationArguments);
$fieldDefinition->type = PartialParser::namedType($paginatorTypeName);
$parentType->fields = ASTHelper::mergeNodeList($parentType->fields, [$fieldDefinition]);
protected function registerSimplePaginator(
FieldDefinitionNode &$fieldDefinition,
ObjectTypeDefinitionNode &$parentType,
?int $defaultCount = null,
?int $maxCount = null
): void {
$fieldTypeName = ASTHelper::getUnderlyingTypeName($fieldDefinition);
$paginatorTypeName = "{$fieldTypeName}SimplePaginator";
$paginatorFieldClassName = addslashes(SimplePaginatorField::class);
$documentAST->setTypeDefinition($paginatorType);
$paginatorType = Parser::objectTypeDefinition(/** @lang GraphQL */ <<<GRAPHQL
"A paginated list of {$fieldTypeName} items."
type {$paginatorTypeName} {
"Pagination information about the list of items."
paginatorInfo: SimplePaginatorInfo! @field(resolver: "{$paginatorFieldClassName}@paginatorInfoResolver")
"A list of {$fieldTypeName} items."
data: [{$fieldTypeName}!]! @field(resolver: "{$paginatorFieldClassName}@dataResolver")
}
GRAPHQL
);
$this->addPaginationWrapperType($paginatorType);
$fieldDefinition->arguments [] = Parser::inputValueDefinition(
self::countArgument($defaultCount, $maxCount)
);
$fieldDefinition->arguments [] = Parser::inputValueDefinition(/** @lang GraphQL */ <<<'GRAPHQL'
"The offset from which items are returned."
page: Int
GRAPHQL
);
$fieldDefinition->type = $this->paginationResultType($paginatorTypeName);
$parentType->fields = ASTHelper::mergeUniqueNodeList($parentType->fields, [$fieldDefinition], true);
}
/**
* Build the count argument definition string, considering default and max values.
*
* @param string $argumentName
* @param int|null $defaultCount
* @param int|null $maxCount
* @return string
*/
protected static function countArgument(string $argumentName, ?int $defaultCount = null, ?int $maxCount = null): string
protected static function countArgument(?int $defaultCount = null, ?int $maxCount = null): string
{
$description = '"Limits number of fetched elements.';
$description = '"Limits number of fetched items.';
if ($maxCount) {
$description .= ' Maximum allowed value: '.$maxCount.'.';
}
$description .= "\"\n";
$definition = $argumentName.': Int'
$definition = 'first: Int'
.($defaultCount
? ' = '.$defaultCount
: '!'
@@ -171,4 +251,25 @@ class PaginationManipulator
return $description.$definition;
}
/**
* @return \GraphQL\Language\AST\NamedTypeNode|\GraphQL\Language\AST\NonNullTypeNode
*/
protected function paginationResultType(string $typeName): TypeNode
{
/** @var \Illuminate\Contracts\Config\Repository $config */
$config = Container::getInstance()->make(ConfigRepository::class);
$nonNull = $config->get('lighthouse.non_null_pagination_results')
? '!'
: '';
/**
* We do not wrap the typename in [], so this will never be a ListOfTypeNode.
*
* @var \GraphQL\Language\AST\NamedTypeNode|\GraphQL\Language\AST\NonNullTypeNode $nonNullTypeNode
*/
$nonNullTypeNode = Parser::typeReference(/** @lang GraphQL */ "{$typeName}{$nonNull}");
return $nonNullTypeNode;
}
}
@@ -0,0 +1,120 @@
<?php
namespace Nuwave\Lighthouse\Pagination;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use GraphQL\Language\Parser;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\ServiceProvider;
use Nuwave\Lighthouse\Events\ManipulateAST;
use Nuwave\Lighthouse\Events\RegisterDirectiveNamespaces;
class PaginationServiceProvider extends ServiceProvider
{
public function boot(Dispatcher $dispatcher): void
{
$dispatcher->listen(
ManipulateAST::class,
function (ManipulateAST $manipulateAST): void {
$documentAST = $manipulateAST->documentAST;
$documentAST->setTypeDefinition(self::paginatorInfo());
$documentAST->setTypeDefinition(self::simplePaginatorInfo());
$documentAST->setTypeDefinition(self::pageInfo());
}
);
$dispatcher->listen(
RegisterDirectiveNamespaces::class,
static function (): string {
return __NAMESPACE__;
}
);
}
protected static function paginatorInfo(): ObjectTypeDefinitionNode
{
return Parser::objectTypeDefinition(/** @lang GraphQL */ '
"Information about pagination using a fully featured paginator."
type PaginatorInfo {
"Number of items in the current page."
count: Int!
"Index of the current page."
currentPage: Int!
"Index of the first item in the current page."
firstItem: Int
"Are there more pages after this one?"
hasMorePages: Boolean!
"Index of the last item in the current page."
lastItem: Int
"Index of the last available page."
lastPage: Int!
"Number of items per page."
perPage: Int!
"Number of total available items."
total: Int!
}
');
}
protected static function simplePaginatorInfo(): ObjectTypeDefinitionNode
{
return Parser::objectTypeDefinition(/** @lang GraphQL */ '
"Information about pagination using a simple paginator."
type SimplePaginatorInfo {
"Number of items in the current page."
count: Int!
"Index of the current page."
currentPage: Int!
"Index of the first item in the current page."
firstItem: Int
"Index of the last item in the current page."
lastItem: Int
"Number of items per page."
perPage: Int!
}
');
}
protected static function pageInfo(): ObjectTypeDefinitionNode
{
return Parser::objectTypeDefinition(/** @lang GraphQL */ '
"Information about pagination using a Relay style cursor connection."
type PageInfo {
"When paginating forwards, are there more items?"
hasNextPage: Boolean!
"When paginating backwards, are there more items?"
hasPreviousPage: Boolean!
"The cursor to continue paginating backwards."
startCursor: String
"The cursor to continue paginating forwards."
endCursor: String
"Total number of nodes in the paginated connection."
total: Int!
"Number of nodes in the current page."
count: Int!
"Index of the current page."
currentPage: Int!
"Index of the last available page."
lastPage: Int!
}
');
}
}
+18 -13
View File
@@ -9,32 +9,32 @@ use Nuwave\Lighthouse\Exceptions\DefinitionException;
*/
class PaginationType
{
const TYPE_PAGINATOR = 'paginator';
const PAGINATION_TYPE_CONNECTION = 'connection';
public const PAGINATOR = 'PAGINATOR';
public const SIMPLE = 'SIMPLE';
public const CONNECTION = 'CONNECTION';
/**
* @var string PAGINATION_TYPE_PAGINATOR|PAGINATION_TYPE_CONNECTION
* @var string One of the constant values in this class
*/
protected $type;
/**
* PaginationType constructor.
*
* @param string $paginationType
* @return void
*
* @throws \Nuwave\Lighthouse\Exceptions\DefinitionException
*/
public function __construct(string $paginationType)
{
switch ($paginationType) {
// TODO remove lowercase and alternate options in v6
switch (strtolower($paginationType)) {
case 'default':
case 'paginator':
$this->type = self::TYPE_PAGINATOR;
$this->type = self::PAGINATOR;
break;
case 'connection':
case 'relay':
$this->type = self::PAGINATION_TYPE_CONNECTION;
$this->type = self::CONNECTION;
break;
case 'simple':
$this->type = self::SIMPLE;
break;
default:
throw new DefinitionException(
@@ -45,11 +45,16 @@ class PaginationType
public function isPaginator(): bool
{
return $this->type === self::TYPE_PAGINATOR;
return $this->type === self::PAGINATOR;
}
public function isConnection(): bool
{
return $this->type === self::PAGINATION_TYPE_CONNECTION;
return $this->type === self::CONNECTION;
}
public function isSimple(): bool
{
return $this->type === self::SIMPLE;
}
}
@@ -1,66 +0,0 @@
<?php
namespace Nuwave\Lighthouse\Pagination;
use GraphQL\Error\Error;
use Illuminate\Support\Arr;
class PaginationUtils
{
/**
* Calculate the current page to inform the user about the pagination state.
*
* @param int $first
* @param int $after
* @param int $defaultPage
* @return int
*/
public static function calculateCurrentPage(int $first, int $after, int $defaultPage = 1): int
{
return $first && $after
? (int) floor(($first + $after) / $first)
: $defaultPage;
}
/**
* @param mixed[] $args
* @param \Nuwave\Lighthouse\Pagination\PaginationType|null $paginationType
* @param int|null $paginateMaxCount
* @return int[] A pair consisting of first and page
*
* @throws \GraphQL\Error\Error
*/
public static function extractArgs(array $args, ?PaginationType $paginationType, ?int $paginateMaxCount): array
{
if ($paginationType->isConnection()) {
/** @var int $first */
$first = $args['first'];
$page = self::calculateCurrentPage(
$first,
Cursor::decode($args)
);
} else {
/** @var int $first */
$first = $args[config('lighthouse.pagination_amount_argument')];
$page = Arr::get($args, 'page', 1);
}
if ($first <= 0) {
throw new Error(
"Requested pagination amount must be more than 0, got $first"
);
}
// Make sure the maximum pagination count is not exceeded
if (
$paginateMaxCount !== null
&& $first > $paginateMaxCount
) {
throw new Error(
"Maximum number of {$paginateMaxCount} requested items exceeded. Fetch smaller chunks."
);
}
return [$first, $page];
}
}
+6 -5
View File
@@ -2,16 +2,16 @@
namespace Nuwave\Lighthouse\Pagination;
use Illuminate\Support\Collection;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
class PaginatorField
{
/**
* Resolve paginator info for connection.
*
* @param \Illuminate\Contracts\Pagination\LengthAwarePaginator $root
* @return array
* @param \Illuminate\Pagination\LengthAwarePaginator<mixed> $root
* @return array<string, mixed>
*/
public function paginatorInfoResolver(LengthAwarePaginator $root): array
{
@@ -30,11 +30,12 @@ class PaginatorField
/**
* Resolve data for connection.
*
* @param \Illuminate\Contracts\Pagination\LengthAwarePaginator $root
* @return \Illuminate\Support\Collection
* @param \Illuminate\Pagination\LengthAwarePaginator<mixed> $root
* @return \Illuminate\Support\Collection<mixed>
*/
public function dataResolver(LengthAwarePaginator $root): Collection
{
// @phpstan-ignore-next-line static refers to the wrong class because it is a proxied method call
return $root->values();
}
}
@@ -0,0 +1,38 @@
<?php
namespace Nuwave\Lighthouse\Pagination;
use Illuminate\Contracts\Pagination\Paginator;
use Illuminate\Support\Collection;
class SimplePaginatorField
{
/**
* Resolve simple paginator info for connection.
*
* @param \Illuminate\Pagination\Paginator<mixed> $root
* @return array<string, mixed>
*/
public function paginatorInfoResolver(Paginator $root): array
{
return [
'count' => $root->count(),
'currentPage' => $root->currentPage(),
'firstItem' => $root->firstItem(),
'lastItem' => $root->lastItem(),
'perPage' => $root->perPage(),
];
}
/**
* Resolve data for connection.
*
* @param \Illuminate\Pagination\Paginator<mixed> $root
* @return \Illuminate\Support\Collection<mixed>
*/
public function dataResolver(Paginator $root): Collection
{
// @phpstan-ignore-next-line static refers to the wrong class because it is a proxied method call
return $root->values();
}
}