Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix args in sub queries #327

Merged
merged 27 commits into from
Jun 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4867c97
Fix args in sub queries
Jun 15, 2019
2d89c71
Apply fixes from StyleCI
sowork Jun 15, 2019
a5f5995
Merge pull request #1 from sowork/analysis-8LaVMn
sowork Jun 15, 2019
771ff00
update NestedRelationLoadingTest.php
Jun 15, 2019
c7e7cd8
solve the problems cacused by variables
Jun 18, 2019
646920c
Apply fixes from StyleCI
sowork Jun 18, 2019
11375af
Merge pull request #2 from sowork/analysis-qy5N3b
sowork Jun 18, 2019
5750416
fix variables
Jun 18, 2019
cf1f570
Merge remote-tracking branch 'origin/master'
Jun 18, 2019
917b1e5
remote merge
Jun 18, 2019
a6076a9
Apply fixes from StyleCI
sowork Jun 18, 2019
93fea9c
Merge pull request #3 from sowork/analysis-8bWAnl
sowork Jun 18, 2019
f5047ae
x
Jun 18, 2019
ad71319
Merge branch 'master' of https://github.com/sowork/graphql-laravel
Jun 18, 2019
5aefa3e
Merge branch 'master' into master
sowork Jun 19, 2019
76a6a0a
strict mode
Jun 19, 2019
b10017b
Merge branch 'master' of https://github.com/sowork/graphql-laravel
Jun 19, 2019
441df57
Merge remote-tracking branch 'pb/master'
Jun 19, 2019
9add6ac
strict mode
Jun 19, 2019
ff7db90
Merge branch 'master' into master
mfn Jun 19, 2019
8b43a63
Move field and argument extraction logic into separate class
mfn Jun 19, 2019
bd883cf
Remove null coalesce fallback
mfn Jun 19, 2019
2171688
Pass field arguments instead of top level arguments to privacy check …
mfn Jun 19, 2019
d444433
Adapt phpstan ignoreErrors due to code changes
mfn Jun 19, 2019
9bca429
Adapt test description now that the bug is fixed
mfn Jun 19, 2019
12381b1
Update CHANGELOG.md
mfn Jun 19, 2019
204e591
Apply fixes from StyleCI
mfn Jun 19, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ Next release
- Replace global helper `is_lumen` with static class call `\Rebing\GraphQL\Helpers::isLumen`

### Fixed
- SelectFields correctly passes field arguments to the custom query [\#327](https://github.com/rebing/graphql-laravel/pull/327)
- This also applies to privacy checks on fields, the callback now receives the field arguments too
- Previously the initial query arguments would be used everywhere
- SelectFields now works with wrapped types (nonNull, listOf)

### Removed
Expand Down
5 changes: 4 additions & 1 deletion phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ parameters:
- '/Strict comparison using === between null and array will always evaluate to false/'
- '/Cannot access offset . on array\|Closure/'
- '/Call to function is_array\(\) with string will always evaluate to false/' # TODO: fix \Rebing\GraphQL\Support\SelectFields::getSelectableFieldsAndRelations
- '/Parameter #1 \$function of function call_user_func expects callable\(\): mixed, array\(mixed, string\) given/'
# \Rebing\GraphQL\Support\SelectFields::handleFields
- '/Parameter #1 \$function of function call_user_func expects callable\(\): mixed, array\(mixed, mixed\) given/'
- '/Binary operation "." between string and array\|string\|null results in an error/'
- '/Parameter #1 \$key of function array_key_exists expects int\|string, string\|false given/'
- "/Parameter #1 \\$function of function call_user_func expects callable\\(\\): mixed, array\\(mixed, 'fire'\\) given/"
Expand All @@ -34,3 +35,5 @@ parameters:
- '/Parameter #4 \$currentPage of class Illuminate\\Pagination\\LengthAwarePaginator constructor expects int\|null, float\|int given/'
- '/Parameter #1 \$offset of method Illuminate\\Support\\Collection::slice\(\) expects int, float\|int given/'
- '/Parameter #1 \$abstract of function app expects string\|null, mixed given/'
# \Rebing\GraphQL\Support\ResolveInfoFieldsAndArguments::getValue
- '/Access to an undefined property GraphQL\\Language\\AST\\ValueNode::\$value/'
146 changes: 146 additions & 0 deletions src/Support/ResolveInfoFieldsAndArguments.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<?php

declare(strict_types=1);

namespace Rebing\GraphQL\Support;

use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\ArgumentNode;
use GraphQL\Language\AST\VariableNode;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Language\AST\SelectionSetNode;
use GraphQL\Language\AST\FragmentSpreadNode;
use GraphQL\Language\AST\InlineFragmentNode;

/**
* This adapts \GraphQL\Type\Definition\ResolveInfo::getFieldSelection
* but with support for both field *and* arguments.
*/
class ResolveInfoFieldsAndArguments
{
/** @var ResolveInfo */
public $info;

public function __construct(ResolveInfo $info)
{
$this->info = $info;
}

/**
* Helper method that returns names of all fields with attributes selected in query for
* $this->fieldName up to $depth levels.
*
* Example:
* query MyQuery{
* {
* root {
* nested(input:value) {
* nested1
* nested2 {
* nested3(input:value)
* }
* }
* }
* }
*
* Given this ResolveInfo instance is a part of "root" field resolution, and $depth === 1,
* method will return:
* [
* 'nested' => [
* 'args' => [
* 'input' => 'value',
* ],
* 'fields' => [
* 'nested1' => [
* 'args' => [],
* 'fields' => true,
* ],
* 'nested2' => [
* 'args' => [],
* 'fields' => [
* 'nested3' => [
* 'args' => [
* 'input' => 'value',
* ],
* 'fields' => true,
* ],
* ],
* ],
* ],
* ],
* ],
*
* Warning: this method it is a naive implementation which does not take into account
* conditional typed fragments. So use it with care for fields of interface and union types.
*
* @param int $depth How many levels to include in output
* @return array
* @see \GraphQL\Type\Definition\ResolveInfo::getFieldSelection
*/
public function getFieldsAndArgumentsSelection(int $depth = 0): array
{
$fields = [];

foreach ($this->info->fieldNodes as $fieldNode) {
if (! $fieldNode->selectionSet) {
continue;
}

$fields = array_merge_recursive($fields, $this->foldSelectionSet($fieldNode->selectionSet, $depth));
}

return $fields;
}

/**
* @param SelectionSetNode $selectionSet
* @param int $descend
* @return array
* @see \GraphQL\Type\Definition\ResolveInfo::foldSelectionSet
*/
private function foldSelectionSet(SelectionSetNode $selectionSet, int $descend): array
{
$fields = [];

foreach ($selectionSet->selections as $selectionNode) {
if ($selectionNode instanceof FieldNode) {
$name = $selectionNode->name->value;

$fields[$name] = [
'args' => [],
'fields' => $descend > 0 && ! empty($selectionNode->selectionSet)
? $this->foldSelectionSet($selectionNode->selectionSet, $descend - 1)
: true,
];

foreach ($selectionNode->arguments ?? [] as $argumentNode) {
$fields[$name]['args'][$argumentNode->name->value] = $this->getValue($argumentNode);
}
} elseif ($selectionNode instanceof FragmentSpreadNode) {
$spreadName = $selectionNode->name->value;
if (isset($this->info->fragments[$spreadName])) {
$fragment = $this->info->fragments[$spreadName];
$fields = array_merge_recursive($this->foldSelectionSet($fragment->selectionSet, $descend),
$fields);
}
} elseif ($selectionNode instanceof InlineFragmentNode) {
$fields = array_merge_recursive($this->foldSelectionSet($selectionNode->selectionSet, $descend),
$fields);
}
}

return $fields;
}

private function getValue(ArgumentNode $argumentNode)
{
$value = $argumentNode->value;
if ($value instanceof VariableNode) {
$variableName = $value->name->value;

return $this->info->variableValues[$variableName] ?? null;
}

return $argumentNode->value->value;
}
}
39 changes: 22 additions & 17 deletions src/Support/SelectFields.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@

class SelectFields
{
/** @var array */
private static $args = [];
/** @var array */
private $select = [];
/** @var array */
Expand All @@ -43,14 +41,20 @@ public function __construct(ResolveInfo $info, GraphqlType $parentType, array $a
$parentType = $parentType->getWrappedType(true);
}

if (! is_null($info->fieldNodes[0]->selectionSet)) {
self::$args = $args;
$requestedFields = $this->getFieldSelection($info, $args, 5);
$fields = self::getSelectableFieldsAndRelations($requestedFields, $parentType);
$this->select = $fields[0];
$this->relations = $fields[1];
}

$fields = self::getSelectableFieldsAndRelations($info->getFieldSelection(5), $parentType);
private function getFieldSelection(ResolveInfo $resolveInfo, array $args, int $depth): array
{
$resolveInfoFieldsAndArguments = new ResolveInfoFieldsAndArguments($resolveInfo);

$this->select = $fields[0];
$this->relations = $fields[1];
}
return [
'args' => $args,
'fields' => $resolveInfoFieldsAndArguments->getFieldsAndArgumentsSelection($depth),
];
}

/**
Expand Down Expand Up @@ -95,9 +99,9 @@ public static function getSelectableFieldsAndRelations(array $requestedFields, G
if ($topLevel) {
return [$select, $with];
} else {
return function ($query) use ($with, $select, $customQuery) {
return function ($query) use ($with, $select, $customQuery, $requestedFields) {
if ($customQuery) {
$query = $customQuery(self::$args, $query);
$query = $customQuery($requestedFields['args'], $query);
}

$query->select($select);
Expand All @@ -118,7 +122,7 @@ protected static function handleFields(array $requestedFields, GraphqlType $pare
{
$parentTable = self::isMongodbInstance($parentType) ? null : self::getTableNameFromParentType($parentType);

foreach ($requestedFields as $key => $field) {
foreach ($requestedFields['fields'] as $key => $field) {
// Ignore __typename, as it's a special case
if ($key === '__typename') {
continue;
Expand All @@ -142,7 +146,7 @@ protected static function handleFields(array $requestedFields, GraphqlType $pare
}

// First check if the field is even accessible
$canSelect = self::validateField($fieldObject);
$canSelect = self::validateField($fieldObject, $field['args']);
if ($canSelect === true) {
// Add a query, if it exists
$customQuery = Arr::get($fieldObject->config, 'query');
Expand All @@ -155,7 +159,7 @@ protected static function handleFields(array $requestedFields, GraphqlType $pare
self::handleFields($field, $fieldObject->config['type']->getWrappedType(), $select, $with);
}
// With
elseif (is_array($field) && $queryable) {
elseif (is_array($field['fields']) && $queryable) {
if (isset($parentType->config['model'])) {
// Get the next parent type, so that 'with' queries could be made
// Both keys for the relation are required (e.g 'id' <-> 'user_id')
Expand Down Expand Up @@ -193,7 +197,7 @@ protected static function handleFields(array $requestedFields, GraphqlType $pare
$segments = explode('.', $foreignKey);
$foreignKey = end($segments);
if (! array_key_exists($foreignKey, $field)) {
$field[$foreignKey] = self::FOREIGN_KEY;
$field['fields'][$foreignKey] = self::FOREIGN_KEY;
}
}

Expand Down Expand Up @@ -240,10 +244,11 @@ protected static function handleFields(array $requestedFields, GraphqlType $pare
* Check the privacy status, if it's given.
*
* @param FieldDefinition $fieldObject
* @param array $fieldArgs Arguments given with the field
* @return bool|null - true, if selectable; false, if not selectable, but allowed;
* null, if not allowed
*/
protected static function validateField(FieldDefinition $fieldObject): ?bool
protected static function validateField(FieldDefinition $fieldObject, array $fieldArgs): ?bool
{
$selectable = true;

Expand All @@ -256,15 +261,15 @@ protected static function validateField(FieldDefinition $fieldObject): ?bool
$privacyClass = $fieldObject->config['privacy'];

// If privacy given as a closure
if (is_callable($privacyClass) && call_user_func($privacyClass, self::$args) === false) {
if (is_callable($privacyClass) && call_user_func($privacyClass, $fieldArgs) === false) {
$selectable = null;
}
// If Privacy class given
elseif (is_string($privacyClass)) {
if (Arr::has(self::$privacyValidations, $privacyClass)) {
$validated = self::$privacyValidations[$privacyClass];
} else {
$validated = call_user_func([app($privacyClass), 'fire'], self::$args);
$validated = call_user_func([app($privacyClass), 'fire'], $fieldArgs);
self::$privacyValidations[$privacyClass] = $validated;
}

Expand Down
Loading