diff --git a/CHANGELOG.md b/CHANGELOG.md index bc30192f..341b8b18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ CHANGELOG ### Fixed - Support adding Schema objects directly [\#449 / mfn](https://github.com/rebing/graphql-laravel/pull/449) +- Input arguments are properly parsed when objects or lists are passed [\#419 / sowork](https://github.com/rebing/graphql-laravel/pull/419) 2019-08-05, 2.0.0 ----------------- diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 579f39be..f36ffaa0 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -41,8 +41,6 @@ parameters: - '/Cannot call method set\(\) on Illuminate\\Config\\Repository\|null/' - '/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/' - # \Rebing\GraphQL\Support\ResolveInfoFieldsAndArguments::getValue - - '/Access to an undefined property GraphQL\\Language\\AST\\ValueNode::\$value/' # tests/Unit/GraphQLTest.php - '/Call to an undefined method GraphQL\\Type\\Definition\\Type::getFields\(\)/' - '/Call to an undefined method Mockery\\/' diff --git a/src/Support/ResolveInfoFieldsAndArguments.php b/src/Support/ResolveInfoFieldsAndArguments.php index ee3101ae..34aceea0 100644 --- a/src/Support/ResolveInfoFieldsAndArguments.php +++ b/src/Support/ResolveInfoFieldsAndArguments.php @@ -4,10 +4,20 @@ namespace Rebing\GraphQL\Support; +use RuntimeException; use GraphQL\Language\AST\FieldNode; -use GraphQL\Language\AST\ArgumentNode; +use GraphQL\Language\AST\ValueNode; +use GraphQL\Language\AST\IntValueNode; use GraphQL\Language\AST\VariableNode; +use GraphQL\Language\AST\EnumValueNode; +use GraphQL\Language\AST\ListValueNode; +use GraphQL\Language\AST\NullValueNode; +use GraphQL\Language\AST\FloatValueNode; use GraphQL\Type\Definition\ResolveInfo; +use GraphQL\Language\AST\ObjectFieldNode; +use GraphQL\Language\AST\ObjectValueNode; +use GraphQL\Language\AST\StringValueNode; +use GraphQL\Language\AST\BooleanValueNode; use GraphQL\Language\AST\SelectionSetNode; use GraphQL\Language\AST\FragmentSpreadNode; use GraphQL\Language\AST\InlineFragmentNode; @@ -114,7 +124,7 @@ private function foldSelectionSet(SelectionSetNode $selectionSet, int $descend): ]; foreach ($selectionNode->arguments ?? [] as $argumentNode) { - $fields[$name]['args'][$argumentNode->name->value] = $this->getValue($argumentNode); + $fields[$name]['args'][$argumentNode->name->value] = $this->getValue($argumentNode->value); } } elseif ($selectionNode instanceof FragmentSpreadNode) { $spreadName = $selectionNode->name->value; @@ -132,15 +142,68 @@ private function foldSelectionSet(SelectionSetNode $selectionSet, int $descend): return $fields; } - private function getValue(ArgumentNode $argumentNode) + private function getValue(ValueNode $value) { - $value = $argumentNode->value; if ($value instanceof VariableNode) { $variableName = $value->name->value; return $this->info->variableValues[$variableName] ?? null; } - return $argumentNode->value->value; + if ($value instanceof IntValueNode) { + return (int) $value->value; + } + + if ($value instanceof FloatValueNode) { + return (float) $value->value; + } + + if ($value instanceof StringValueNode) { + return (string) $value->value; + } + + if ($value instanceof BooleanValueNode) { + return (bool) $value->value; + } + + if ($value instanceof EnumValueNode) { + return (string) $value->value; + } + + if ($value instanceof NullValueNode) { + return; + } + + if ($value instanceof ObjectValueNode) { + return $this->getInputObjectValue($value); + } + + if ($value instanceof ListValueNode) { + return $this->getInputListObjectValue($value); + } + + throw new RuntimeException('Failed to resolve unknown ValueNode type'); + } + + private function getInputObjectValue(ObjectValueNode $objectValueNode): array + { + $value = []; + foreach ($objectValueNode->fields->getIterator() as $item) { + if ($item instanceof ObjectFieldNode) { + $value[$item->name->value] = $this->getValue($item->value); + } + } + + return $value; + } + + private function getInputListObjectValue(ListValueNode $listValueNode): array + { + $value = []; + foreach ($listValueNode->values as $valueNode) { + $value[] = $this->getValue($valueNode); + } + + return $value; } } diff --git a/tests/Database/SelectFields/ValidateDiffNodeTests/EpisodeEnum.php b/tests/Database/SelectFields/ValidateDiffNodeTests/EpisodeEnum.php new file mode 100644 index 00000000..e5c7f924 --- /dev/null +++ b/tests/Database/SelectFields/ValidateDiffNodeTests/EpisodeEnum.php @@ -0,0 +1,20 @@ + 'Episode', + 'description' => 'The types of demographic elements', + 'values' => [ + 'NEWHOPE' => 'NEWHOPE', + 'EMPIRE' => 'EMPIRE', + 'JEDI' => 'JEDI', + ], + ]; +} diff --git a/tests/Database/SelectFields/ValidateDiffNodeTests/FilterInput.php b/tests/Database/SelectFields/ValidateDiffNodeTests/FilterInput.php new file mode 100644 index 00000000..478acd8c --- /dev/null +++ b/tests/Database/SelectFields/ValidateDiffNodeTests/FilterInput.php @@ -0,0 +1,31 @@ + 'Filter', + 'description' => 'filter object', + ]; + + public function fields(): array + { + return [ + 'body' => [ + 'type' => Type::string(), + ], + 'id' => [ + 'type' => Type::int(), + ], + 'title' => [ + 'type' => Type::string(), + ], + ]; + } +} diff --git a/tests/Database/SelectFields/ValidateDiffNodeTests/PostType.php b/tests/Database/SelectFields/ValidateDiffNodeTests/PostType.php new file mode 100644 index 00000000..62b98a04 --- /dev/null +++ b/tests/Database/SelectFields/ValidateDiffNodeTests/PostType.php @@ -0,0 +1,29 @@ + 'Post', + 'model' => Post::class, + ]; + + public function fields(): array + { + return [ + 'body' => [ + 'type' => Type::string(), + ], + 'id' => [ + 'type' => Type::ID(), + ], + ]; + } +} diff --git a/tests/Database/SelectFields/ValidateDiffNodeTests/UserType.php b/tests/Database/SelectFields/ValidateDiffNodeTests/UserType.php new file mode 100644 index 00000000..39603204 --- /dev/null +++ b/tests/Database/SelectFields/ValidateDiffNodeTests/UserType.php @@ -0,0 +1,86 @@ + 'User', + 'model' => User::class, + ]; + + public function fields(): array + { + return [ + 'id' => [ + 'type' => Type::int(), + ], + 'name' => [ + 'type' => Type::string(), + ], + 'posts' => [ + 'type' => Type::listOf(Type::nonNull(GraphQL::type('Post'))), + 'args' => [ + 'id' => [ + 'type' => Type::int(), + ], + 'name' => [ + 'type' => Type::string(), + ], + 'price' => [ + 'type' => Type::float(), + ], + 'status' => [ + 'type' => Type::boolean(), + ], + 'flag' => [ + 'type' => Type::string(), + ], + 'author' => [ + 'type' => GraphQL::type('Episode'), + ], + 'post' => [ + 'type' => GraphQL::type('Filter'), + ], + 'keywords' => [ + 'type' => Type::listOf(Type::string()), + ], + 'customType' => [ + 'type' => GraphQL::type('MyCustomScalarString'), + ], + ], + 'query' => function (array $args, $query) { + $expectedQueryArgs = [ + 'id' => 2, + 'name' => 'tom', + 'price' => 1.3, + 'status' => false, + 'flag' => null, + 'author' => 'EMPIRE', + 'post' => [ + 'id' => 2, + 'body' => 'body2', + ], + 'keywords' => [ + 'key4', + 'key5', + 'key6', + ], + 'customType' => 'custom string', + ]; + Assert::assertSame($expectedQueryArgs, $args); + + return $query; + }, + ], + ]; + } +} diff --git a/tests/Database/SelectFields/ValidateDiffNodeTests/UsersQuery.php b/tests/Database/SelectFields/ValidateDiffNodeTests/UsersQuery.php new file mode 100644 index 00000000..e2a4be0c --- /dev/null +++ b/tests/Database/SelectFields/ValidateDiffNodeTests/UsersQuery.php @@ -0,0 +1,87 @@ + 'users', + ]; + + public function args(): array + { + return [ + 'id' => [ + 'type' => Type::int(), + ], + 'name' => [ + 'type' => Type::string(), + ], + 'price' => [ + 'type' => Type::float(), + ], + 'status' => [ + 'type' => Type::boolean(), + ], + 'flag' => [ + 'type' => Type::string(), + ], + 'author' => [ + 'type' => GraphQL::type('Episode'), + ], + 'post' => [ + 'type' => GraphQL::type('Filter'), + ], + 'keywords' => [ + 'type' => Type::listOf(Type::string()), + ], + 'customType' => [ + 'type' => GraphQL::type('MyCustomScalarString'), + ], + ]; + } + + public function type(): Type + { + return Type::nonNull(Type::listOf(Type::nonNull(GraphQL::type('User')))); + } + + public function resolve($root, $args, $context, ResolveInfo $resolveInfo, \Closure $getSelectFields) + { + /** @var SelectFields $fields */ + $fields = $getSelectFields(); + $expectedQueryArgs = [ + 'id' => 1, + 'name' => 'john', + 'price' => 1.2, + 'status' => true, + 'flag' => null, + 'author' => 'NEWHOPE', + 'post' => [ + 'body' => 'body', + 'id' => 1, + ], + 'keywords' => [ + 'key1', + 'key2', + 'key3', + ], + 'customType' => 'hello world', + ]; + Assert::assertSame($expectedQueryArgs, $args); + + return User::select($fields->getSelect()) + ->with($fields->getRelations()) + ->get(); + } +} diff --git a/tests/Database/SelectFields/ValidateDiffNodeTests/ValidateDiffNodeTests.php b/tests/Database/SelectFields/ValidateDiffNodeTests/ValidateDiffNodeTests.php new file mode 100644 index 00000000..98f5d724 --- /dev/null +++ b/tests/Database/SelectFields/ValidateDiffNodeTests/ValidateDiffNodeTests.php @@ -0,0 +1,112 @@ +create() + ->each(function (User $user): void { + factory(Post::class) + ->create([ + 'user_id' => $user->id, + ]); + + factory(Post::class) + ->create([ + 'user_id' => $user->id, + ]); + }); + + $graphql = <<<'GRAQPHQL' +{ + users(id: 1, name: "john", price: 1.2, status: true, flag: null, author: NEWHOPE, post: {id: 1, body: "body"}, keywords: ["key1", "key2", "key3"], customType: "hello world") { + id + name + posts(id: 2, name: "tom", price: 1.3, status: false, flag: null, author: EMPIRE, post: {id: 2, body: "body2"}, keywords: ["key4", "key5", "key6"], customType: "custom string") { + id + body + } + } +} +GRAQPHQL; + + $this->sqlCounterReset(); + + $result = $this->graphql($graphql); + $this->assertSqlQueries(<<<'SQL' +select "users"."id", "users"."name" from "users"; +select "posts"."id", "posts"."body", "posts"."user_id" from "posts" where "posts"."user_id" in (?, ?) order by "posts"."id" asc; +SQL + ); + + $expectedResult = [ + 'data' => [ + 'users' => [ + [ + 'id' => (string) $users[0]->id, + 'name' => $users[0]->name, + 'posts' => [ + [ + 'body' => $users[0]->posts[0]->body, + 'id' => (string) $users[0]->posts[0]->id, + ], + [ + 'body' => $users[0]->posts[1]->body, + 'id' => (string) $users[0]->posts[1]->id, + ], + ], + ], + [ + 'id' => (string) $users[1]->id, + 'name' => $users[1]->name, + 'posts' => [ + [ + 'body' => $users[1]->posts[0]->body, + 'id' => (string) $users[1]->posts[0]->id, + ], + [ + 'body' => $users[1]->posts[1]->body, + 'id' => (string) $users[1]->posts[1]->id, + ], + ], + ], + ], + ], + ]; + + $this->assertEquals($expectedResult, $result); + } + + protected function getEnvironmentSetUp($app) + { + parent::getEnvironmentSetUp($app); + + $app['config']->set('graphql.schemas.default', [ + 'query' => [ + UsersQuery::class, + ], + ]); + + $app['config']->set('graphql.schemas.custom', null); + + $app['config']->set('graphql.types', [ + UserType::class, + FilterInput::class, + EpisodeEnum::class, + PostType::class, + MyCustomScalarString::class, + ]); + } +}