new Deps
This commit is contained in:
+30
-24
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
+185
-84
@@ -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
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user