Vendor
This commit is contained in:
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user