This commit is contained in:
Kilian Hofmann
2021-06-01 19:55:55 +02:00
parent 052cbe3038
commit d8c489c714
328 changed files with 36645 additions and 0 deletions
@@ -0,0 +1,80 @@
<?php
namespace Nuwave\Lighthouse\Pagination;
use Illuminate\Support\Collection;
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
class ConnectionField
{
/**
* Resolve page info for connection.
*
* @param \Illuminate\Contracts\Pagination\LengthAwarePaginator $paginator
* @return array
*/
public function pageInfoResolver(LengthAwarePaginator $paginator): array
{
return [
'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
*/
public function edgeResolver(LengthAwarePaginator $paginator, $args, GraphQLContext $context, ResolveInfo $resolveInfo): Collection
{
$returnTypeFields = $resolveInfo
->returnType
->ofType
->getFields();
$firstItem = $paginator->firstItem();
return $paginator
->values()
->map(function ($item, $index) use ($firstItem, $returnTypeFields): array {
$data = [];
foreach ($returnTypeFields as $field) {
switch ($field->name) {
case 'cursor':
$data['cursor'] = Cursor::encode($firstItem + $index);
break;
case 'node':
$data['node'] = $item;
break;
default:
// All other fields on the return type are assumed to be part
// of the edge, so we try to locate them in the pivot attribute
if (isset($item->pivot->{$field->name})) {
$data[$field->name] = $item->pivot->{$field->name};
}
}
}
return $data;
});
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace Nuwave\Lighthouse\Pagination;
use Illuminate\Support\Arr;
/**
* Encode and decode pagination cursors.
*
* Currently, the underlying pagination Query uses offset based navigation, so
* this basically just encodes an offset. This is enough to satisfy the constraints
* that Relay has, but not a clean permanent solution.
*
* TODO Implement actual cursor pagination https://github.com/nuwave/lighthouse/issues/311
*/
class Cursor
{
/**
* Decode cursor from query arguments.
*
* If no 'after' argument is provided or the contents are not a valid base64 string,
* this will return 0. That will effectively reset pagination, so the user gets the
* first slice.
*
* @param array $args
* @return int
*/
public static function decode(array $args): int
{
if ($cursor = Arr::get($args, 'after')) {
return (int) base64_decode($cursor);
}
return 0;
}
/**
* 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);
}
}
@@ -0,0 +1,174 @@
<?php
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 Nuwave\Lighthouse\Exceptions\DefinitionException;
class PaginationManipulator
{
/**
* Transform the definition for a field to a field with pagination.
*
* This makes either an offset-based Paginator or a cursor-based Connection.
* 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(
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);
} else {
self::registerPaginator($fieldDefinition, $parentType, $documentAST, $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(
FieldDefinitionNode &$fieldDefinition,
ObjectTypeDefinitionNode &$parentType,
DocumentAST &$documentAST,
?int $defaultCount = null,
?int $maxCount = null,
?ObjectTypeDefinitionNode $edgeType = null
): void {
$fieldTypeName = ASTHelper::getUnderlyingTypeName($fieldDefinition);
if ($edgeType) {
$connectionEdgeName = $edgeType->name->value;
$connectionTypeName = "{$connectionEdgeName}Connection";
} else {
$connectionEdgeName = "{$fieldTypeName}Edge";
$connectionTypeName = "{$fieldTypeName}Connection";
}
$connectionFieldName = addslashes(ConnectionField::class);
$connectionType = PartialParser::objectTypeDefinition("
type $connectionTypeName {
pageInfo: PageInfo! @field(resolver: \"{$connectionFieldName}@pageInfoResolver\")
edges: [$connectionEdgeName] @field(resolver: \"{$connectionFieldName}@edgeResolver\")
}
");
$connectionEdge = $edgeType
?? $documentAST->types[$connectionEdgeName]
?? PartialParser::objectTypeDefinition("
type $connectionEdgeName {
node: $fieldTypeName
cursor: String!
}
");
$inputValueDefinitions = [
self::countArgument('first', $defaultCount, $maxCount),
"\"A cursor after which elements are returned.\"\nafter: String",
];
$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);
}
/**
* 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(
FieldDefinitionNode &$fieldDefinition,
ObjectTypeDefinitionNode &$parentType,
DocumentAST &$documentAST,
?int $defaultCount = null,
?int $maxCount = null
): void {
$fieldTypeName = ASTHelper::getUnderlyingTypeName($fieldDefinition);
$paginatorTypeName = "{$fieldTypeName}Paginator";
$paginatorFieldClassName = addslashes(PaginatorField::class);
$paginatorType = PartialParser::objectTypeDefinition("
type $paginatorTypeName {
paginatorInfo: PaginatorInfo! @field(resolver: \"{$paginatorFieldClassName}@paginatorInfoResolver\")
data: [$fieldTypeName!]! @field(resolver: \"{$paginatorFieldClassName}@dataResolver\")
}
");
$inputValueDefinitions = [
self::countArgument(config('lighthouse.pagination_amount_argument'), $defaultCount, $maxCount),
"\"The offset from which elements are returned.\"\npage: Int",
];
$paginationArguments = PartialParser::inputValueDefinitions($inputValueDefinitions);
$fieldDefinition->arguments = ASTHelper::mergeNodeList($fieldDefinition->arguments, $paginationArguments);
$fieldDefinition->type = PartialParser::namedType($paginatorTypeName);
$parentType->fields = ASTHelper::mergeNodeList($parentType->fields, [$fieldDefinition]);
$documentAST->setTypeDefinition($paginatorType);
}
/**
* 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
{
$description = '"Limits number of fetched elements.';
if ($maxCount) {
$description .= ' Maximum allowed value: '.$maxCount.'.';
}
$description .= "\"\n";
$definition = $argumentName.': Int'
.($defaultCount
? ' = '.$defaultCount
: '!'
);
return $description.$definition;
}
}
@@ -0,0 +1,55 @@
<?php
namespace Nuwave\Lighthouse\Pagination;
use Nuwave\Lighthouse\Exceptions\DefinitionException;
/**
* An enum-like class that contains the supported types of pagination.
*/
class PaginationType
{
const TYPE_PAGINATOR = 'paginator';
const PAGINATION_TYPE_CONNECTION = 'connection';
/**
* @var string PAGINATION_TYPE_PAGINATOR|PAGINATION_TYPE_CONNECTION
*/
protected $type;
/**
* PaginationType constructor.
*
* @param string $paginationType
* @return void
*
* @throws \Nuwave\Lighthouse\Exceptions\DefinitionException
*/
public function __construct(string $paginationType)
{
switch ($paginationType) {
case 'default':
case 'paginator':
$this->type = self::TYPE_PAGINATOR;
break;
case 'connection':
case 'relay':
$this->type = self::PAGINATION_TYPE_CONNECTION;
break;
default:
throw new DefinitionException(
"Found invalid pagination type: {$paginationType}"
);
}
}
public function isPaginator(): bool
{
return $this->type === self::TYPE_PAGINATOR;
}
public function isConnection(): bool
{
return $this->type === self::PAGINATION_TYPE_CONNECTION;
}
}
@@ -0,0 +1,66 @@
<?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];
}
}
@@ -0,0 +1,40 @@
<?php
namespace Nuwave\Lighthouse\Pagination;
use Illuminate\Support\Collection;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
class PaginatorField
{
/**
* Resolve paginator info for connection.
*
* @param \Illuminate\Contracts\Pagination\LengthAwarePaginator $root
* @return array
*/
public function paginatorInfoResolver(LengthAwarePaginator $root): array
{
return [
'count' => $root->count(),
'currentPage' => $root->currentPage(),
'firstItem' => $root->firstItem(),
'hasMorePages' => $root->hasMorePages(),
'lastItem' => $root->lastItem(),
'lastPage' => $root->lastPage(),
'perPage' => $root->perPage(),
'total' => $root->total(),
];
}
/**
* Resolve data for connection.
*
* @param \Illuminate\Contracts\Pagination\LengthAwarePaginator $root
* @return \Illuminate\Support\Collection
*/
public function dataResolver(LengthAwarePaginator $root): Collection
{
return $root->values();
}
}