From fe82fa42cba37d8cad192f552133a0cb5017a753 Mon Sep 17 00:00:00 2001 From: spawnia Date: Wed, 16 May 2018 10:17:32 +0200 Subject: [PATCH 1/6] Improvements to validation and field resolvers - Limit the depth of static rule generation to prevent infinite loops with circularly nested Input Objects - Move validation from trait to the generic field class. This makes it so that Queries and Mutations are treated the same in regards to validation. - Unify the generated classes for Fields, Mutations and Queries - Require Fields to implement type() via abstract method - Include default resolver in Field class to be able to apply authentication, authorization and validation even if no resolver is implemented - Deprecate the rules() function in favour of inline arguments defined within args() - Add tests and documentation for all proposed changes --- Readme.md | 147 +++++---- src/Folklore/GraphQL/Console/stubs/field.stub | 37 ++- .../GraphQL/Console/stubs/mutation.stub | 26 +- src/Folklore/GraphQL/Console/stubs/query.stub | 26 +- src/Folklore/GraphQL/GraphQL.php | 24 +- src/Folklore/GraphQL/Support/Field.php | 299 +++++++++++++++--- src/Folklore/GraphQL/Support/Mutation.php | 13 +- src/Folklore/GraphQL/Support/Query.php | 9 +- .../GraphQL/Support/Traits/ShouldValidate.php | 133 -------- src/config/config.php | 10 +- tests/ConfigTest.php | 25 +- tests/FieldTest.php | 28 +- tests/GraphQLQueryTest.php | 29 +- tests/GraphQLTest.php | 18 +- tests/MutationTest.php | 169 +++++----- ...Object.php => ExampleChildInputObject.php} | 17 +- tests/Objects/ExampleField.php | 7 +- tests/Objects/ExampleMutation.php | 91 ++++++ tests/Objects/ExampleParentInputObject.php | 52 +++ tests/Objects/ExampleValidationField.php | 12 +- .../Objects/ExampleValidationInputObject.php | 45 --- tests/Objects/ExamplesContextQuery.php | 4 +- tests/Objects/ExamplesPaginationQuery.php | 5 +- tests/Objects/ExamplesQuery.php | 13 +- tests/Objects/ExamplesRootQuery.php | 4 +- tests/Objects/UpdateExampleMutation.php | 56 ---- .../UpdateExampleMutationWithInputType.php | 71 ----- tests/Objects/queries.php | 10 +- tests/TestCase.php | 8 +- 29 files changed, 743 insertions(+), 645 deletions(-) delete mode 100644 src/Folklore/GraphQL/Support/Traits/ShouldValidate.php rename tests/Objects/{ExampleNestedValidationInputObject.php => ExampleChildInputObject.php} (54%) create mode 100644 tests/Objects/ExampleMutation.php create mode 100644 tests/Objects/ExampleParentInputObject.php delete mode 100644 tests/Objects/ExampleValidationInputObject.php delete mode 100644 tests/Objects/UpdateExampleMutation.php delete mode 100644 tests/Objects/UpdateExampleMutationWithInputType.php diff --git a/Readme.md b/Readme.md index d660d35a..0694edcc 100644 --- a/Readme.md +++ b/Readme.md @@ -127,7 +127,7 @@ config/graphql.php - [Schemas](#schemas) - [Creating a query](#creating-a-query) - [Creating a mutation](#creating-a-mutation) -- [Adding validation to mutation](#adding-validation-to-mutation) +- [Input Validation](#validation) #### Advanced Usage - [Query variables](docs/advanced.md#query-variables) @@ -418,91 +418,100 @@ if you use homestead: http://homestead.app/graphql?query=mutation+users{updateUserPassword(id: "1", password: "newpassword"){id,email}} ``` -#### Adding validation to mutation +### Validation -It is possible to add validation rules to mutation. It uses the laravel `Validator` to performs validation against the `args`. +It is possible to add additional validation rules to inputs, using the Laravel `Validator` to perform validation against the `args`. +Validation is mostly used for Mutations, but can also be applied to Queries that take arguments. -When creating a mutation, you can add a method to define the validation rules that apply by doing the following: +Be aware that GraphQL has native types to define a field as either a List or as NonNull. Use those wrapping +types instead of native Laravel validation via `array` or `required`. This way, those constraints are +reflected through the schema and are validated by the underlying GraphQL implementation. -```php -namespace App\GraphQL\Mutation; +#### Rule definition -use GraphQL; -use GraphQL\Type\Definition\Type; -use Folklore\GraphQL\Support\Mutation; -use App\User; +You can add validation rules directly to the arguments of Mutations or Queries: + + +```php +//... class UpdateUserEmailMutation extends Mutation { - protected $attributes = [ - 'name' => 'UpdateUserEmail' - ]; - - public function type() - { - return GraphQL::type('User'); - } + //... public function args() - { - return [ - 'id' => ['name' => 'id', 'type' => Type::string()], - 'email' => ['name' => 'email', 'type' => Type::string()] - ]; - } - - public function rules() - { - return [ - 'id' => ['required'], - 'email' => ['required', 'email'] - ]; - } - - public function resolve($root, $args) - { - $user = User::find($args['id']); - - if (!$user) { - return null; + { + return [ + 'id' => [ + 'name' => 'id', + 'type' => Type::nonNull(Type::string()), + // Adding a rule for 'required' is not necessary + ], + 'email' => [ + 'name' => 'email', + 'type' => Type::nonNull(Type::string()), + 'rules' => ['email'] + ], + 'links' => [ + 'name' => 'links', + 'type' => Type::listOf(Type::string()), + // Validation is applied to each element of the list + 'rules' => ['url'] + ], + 'name' => [ + 'name' => 'name', + 'type' => GraphQL::type('NameInputObject') + ], + ]; } - $user->email = $args['email']; - $user->save(); - - return $user; - } + //... } ``` -Alternatively you can define rules with each args +#### Input Object Rules -```php -class UpdateUserEmailMutation extends Mutation +Notice how the argument `name` has the type `NameInputObject`? The rules defined in Input Objects are also considered when +validating the Mutation! The definition of those rules looks like this: + +````php + 'NameInputObject' + ]; + + public function fields() { return [ - 'id' => [ - 'name' => 'id', + 'first' => [ + 'name' => 'first', 'type' => Type::string(), - 'rules' => ['required'] + 'rules' => ['alpha'] + ], + 'last' => [ + 'name' => 'last', + 'type' => Type::nonNull(Type::string()), + 'rules' => ['alpha'] ], - 'email' => [ - 'name' => 'email', - 'type' => Type::string(), - 'rules' => ['required', 'email'] - ] ]; } - - //... } -``` +```` -When you execute a mutation, it will returns the validation errors. Since GraphQL specifications define a certain format for errors, the validation errors messages are added to the error object as a extra `validation` attribute. To find the validation error, you should check for the error with a `message` equals to `'validation'`, then the `validation` attribute will contain the normal errors messages returned by the Laravel Validator. +Now, the rules in here ensure that if a name is passed to the above Mutation, it must contain at least a +last name, and the first and last name can only contain alphabetic characters. + +#### Response format + +When you execute a mutation, it will return the validation errors. Since the GraphQL specification defines a certain format for errors, the validation error messages are added to the error object as an extra `validation` attribute. To find the validation error, you should check for the error with a `message` equals to `'validation'`, then the `validation` attribute will contain the normal errors messages returned by the Laravel Validator. ```json { @@ -527,3 +536,19 @@ When you execute a mutation, it will returns the validation errors. Since GraphQ ] } ``` + +#### Validation depth + +Laravel validation works by statically generating validation rules and then applying +them to arguments all at once. However, field arguments may consist of Input Objects, which +could have circular references. Because of this, the maximum depth for validation has to be +set before. The default depth is set to `10`, it can be overwritten this way: + +````php +class ExampleMutation extends Mutation +{ + protected $validationDepth = 15; + + //... +} +```` diff --git a/src/Folklore/GraphQL/Console/stubs/field.stub b/src/Folklore/GraphQL/Console/stubs/field.stub index e7003d00..22c96f9f 100644 --- a/src/Folklore/GraphQL/Console/stubs/field.stub +++ b/src/Folklore/GraphQL/Console/stubs/field.stub @@ -4,23 +4,52 @@ namespace DummyNamespace; use GraphQL\Type\Definition\Type; use Folklore\GraphQL\Support\Field; +use GraphQL\Type\Definition\ResolveInfo; -class DummyClass extends Field { +class DummyClass extends Field +{ protected $attributes = [ + 'name' => 'DummyField', 'description' => 'A field' ]; + /** + * Define a GraphQL Type which is returned by this field. + * + * @return \Folklore\GraphQL\Support\Type + */ public function type() { - return Type::string(); + return Type::listOf(Type::string()); } + /** + * Define the arguments expected by the field. + * + * @return array + */ public function args() { - return []; + return [ + 'example' => [ + 'name' => 'example', + 'type' => Type::nonNull(Type::string()), + 'rules' => ['alpha', 'not_in:forbidden,value'] + ] + ]; } - protected function resolve($root, $args) + /** + * Return a result for the field which should match up with its return type. + * + * @param $root + * @param $args + * @param $context + * @param ResolveInfo $info + * @return array + */ + public function resolve($root, $args, $context, ResolveInfo $info) { + return []; } } diff --git a/src/Folklore/GraphQL/Console/stubs/mutation.stub b/src/Folklore/GraphQL/Console/stubs/mutation.stub index 837319bf..80619327 100644 --- a/src/Folklore/GraphQL/Console/stubs/mutation.stub +++ b/src/Folklore/GraphQL/Console/stubs/mutation.stub @@ -5,7 +5,6 @@ namespace DummyNamespace; use Folklore\GraphQL\Support\Mutation; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; -use GraphQL; class DummyClass extends Mutation { @@ -14,18 +13,41 @@ class DummyClass extends Mutation 'description' => 'A mutation' ]; + /** + * Define a GraphQL Type which is returned by this field. + * + * @return \Folklore\GraphQL\Support\Type + */ public function type() { return Type::listOf(Type::string()); } + /** + * Define the arguments expected by the field. + * + * @return array + */ public function args() { return [ - + 'example' => [ + 'name' => 'example', + 'type' => Type::nonNull(Type::string()), + 'rules' => ['alpha', 'not_in:forbidden,value'] + ] ]; } + /** + * Return a result for the field which should match up with its return type. + * + * @param $root + * @param $args + * @param $context + * @param ResolveInfo $info + * @return array + */ public function resolve($root, $args, $context, ResolveInfo $info) { return []; diff --git a/src/Folklore/GraphQL/Console/stubs/query.stub b/src/Folklore/GraphQL/Console/stubs/query.stub index 6ec158bc..0092d20d 100644 --- a/src/Folklore/GraphQL/Console/stubs/query.stub +++ b/src/Folklore/GraphQL/Console/stubs/query.stub @@ -5,7 +5,6 @@ namespace DummyNamespace; use Folklore\GraphQL\Support\Query; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; -use GraphQL; class DummyClass extends Query { @@ -14,18 +13,41 @@ class DummyClass extends Query 'description' => 'A query' ]; + /** + * Define a GraphQL Type which is returned by this field. + * + * @return \Folklore\GraphQL\Support\Type + */ public function type() { return Type::listOf(Type::string()); } + /** + * Define the arguments expected by the field. + * + * @return array + */ public function args() { return [ - + 'example' => [ + 'name' => 'example', + 'type' => Type::nonNull(Type::string()), + 'rules' => ['alpha', 'not_in:forbidden,value'] + ] ]; } + /** + * Return a result for the field which should match up with its return type. + * + * @param $root + * @param $args + * @param $context + * @param ResolveInfo $info + * @return array + */ public function resolve($root, $args, $context, ResolveInfo $info) { return []; diff --git a/src/Folklore/GraphQL/GraphQL.php b/src/Folklore/GraphQL/GraphQL.php index 53cfb98c..230fbfdc 100644 --- a/src/Folklore/GraphQL/GraphQL.php +++ b/src/Folklore/GraphQL/GraphQL.php @@ -1,23 +1,17 @@ getAttributes(); } - public function type() + /** + * Get the attributes from the container. + * + * @return array + */ + public function getAttributes() { - return null; + return array_merge( + $this->attributes, + [ + 'args' => $this->args(), + 'type' => $this->type(), + 'resolve' => $this->getResolver(), + ] + ); } + /** + * Define the arguments expected by the field. + * + * @return array + */ public function args() { return []; } - protected function getResolver() + /** + * Define a GraphQL Type which is returned by this field. + * + * @return \Folklore\GraphQL\Support\Type + */ + public abstract function type(); + + /** + * Return a result for the field. + * + * @param $root + * @param $args + * @param $context + * @param ResolveInfo $info + * @return mixed + */ + public function resolve($root, $args, $context, ResolveInfo $info) { - if (!method_exists($this, 'resolve')) { - return null; - } + // Use the default resolver, can be overridden in custom Fields, Queries or Mutations + // This enables us to still apply authentication, authorization and validation upon the query + return call_user_func(config('graphql.defaultFieldResolver')); + } - $resolver = array($this, 'resolve'); - $authenticate = [$this, 'authenticated']; + /** + * Returns a function that wraps the resolve function with authentication and validation checks. + * + * @return \Closure + */ + protected function getResolver() + { + $authenticated = [$this, 'authenticated']; $authorize = [$this, 'authorize']; + $resolve = [$this, 'resolve']; - return function () use ($resolver, $authorize, $authenticate) { + return function () use ($authenticated, $authorize, $resolve) { $args = func_get_args(); - // Authenticated - if (call_user_func_array($authenticate, $args) !== true) { + // Check authentication first + if (call_user_func_array($authenticated, $args) !== true) { throw new AuthorizationError('Unauthenticated'); } - // Authorize + // After authentication, check specific authorization if (call_user_func_array($authorize, $args) !== true) { throw new AuthorizationError('Unauthorized'); } - return call_user_func_array($resolver, $args); + // Apply additional validation + $rules = call_user_func_array([$this, 'getRules'], $args); + if (sizeof($rules)) { + $validationErrorMessages = call_user_func_array([$this, 'validationErrorMessages'], $args); + $inputArguments = array_get($args, 1, []); + $validator = $this->getValidator($inputArguments, $rules, $validationErrorMessages); + if ($validator->fails()) { + throw with(new ValidationError('validation'))->setValidator($validator); + } + } + + return call_user_func_array($resolve, $args); }; } /** - * Get the attributes from the container. + * @param $args + * @param $rules + * @param array $messages + * + * @return Validator + */ + protected function getValidator($args, $rules, $messages = []) + { + /** @var Validator $validator */ + $validator = app('validator')->make($args, $rules, $messages); + if (method_exists($this, 'withValidator')) { + $this->withValidator($validator, $args); + } + + return $validator; + } + + /** + * Dynamically retrieve the value of an attribute. + * + * @param string $key + * @return mixed + */ + public function __get($key) + { + $attributes = $this->getAttributes(); + return isset($attributes[$key]) ? $attributes[$key] : null; + } + + /** + * Dynamically check if an attribute is set. + * + * @param string $key + * @return bool + */ + public function __isset($key) + { + $attributes = $this->getAttributes(); + return isset($attributes[$key]); + } + + /** + * Get the combined explicit and type-inferred rules. * * @return array */ - public function getAttributes() + public function getRules() { - $attributes = $this->attributes(); - $args = $this->args(); + $arguments = func_get_args(); + + $argsRules = $this->getRulesFromArgs($this->args(), $this->validationDepth, null, $arguments); - $attributes = array_merge($this->attributes, [ - 'args' => $args - ], $attributes); + // Merge rules that were set separately with those defined through args + $explicitRules = call_user_func_array([$this, 'rules'], $arguments); + return array_merge($argsRules, $explicitRules); + } - $type = $this->type(); - if (isset($type)) { - $attributes['type'] = $type; + /** + * @param $fields + * @param $inputValueDepth + * @param $parentKey + * @param $resolutionArguments + * @return array + */ + protected function getRulesFromArgs($fields, $inputValueDepth, $parentKey, $resolutionArguments) + { + // At depth 0, there are still field rules to gather + // once we get below 0, we have gathered all the rules necessary + // for the nesting depth + if ($inputValueDepth < 0) { + return []; } - $resolver = $this->getResolver(); - if (isset($resolver)) { - $attributes['resolve'] = $resolver; + // We are going one level deeper + $inputValueDepth--; + + $rules = []; + // Merge in the rules of the Input Type + foreach ($fields as $fieldName => $field) { + // Count the depth per field + $fieldDepth = $inputValueDepth; + + // If given, add the parent key + $key = $parentKey ? "{$parentKey}.{$fieldName}" : $fieldName; + + // The values passed in here may be of different types, depending on where they were defined + if ($field instanceof InputObjectField) { + // We can depend on type being set, since a field without type is not valid + $type = $field->type; + // Rules are optional so they may not be set + $fieldRules = isset($field->rules) ? $field->rules : []; + } else { + $type = $field['type']; + $fieldRules = isset($field['rules']) ? $field['rules'] : []; + } + + // Unpack until we get to the root type + while ($type instanceof WrappingType) { + if ($type instanceof ListOfType) { + // This lets us skip one level of validation rules + $fieldDepth--; + // Add this to the prefix to allow Laravel validation for arrays + $key .= '.*'; + } + + $type = $type->getWrappedType(); + } + + // Add explicitly set rules if they apply + if (sizeof($fieldRules)) { + $rules[$key] = $this->resolveRules($fieldRules, $resolutionArguments); + } + + if ($type instanceof InputObjectType) { + // Recursively call the parent method to get nested rules, passing in the new prefix + $rules = array_merge($rules, $this->getRulesFromArgs($type->getFields(), $fieldDepth, $key, $resolutionArguments)); + } } - return $attributes; + return $rules; } /** - * Convert the Fluent instance to an array. - * - * @return array + * @param $rules + * @param $arguments + * @return mixed */ - public function toArray() + protected function resolveRules($rules, $arguments) { - return $this->getAttributes(); + // Rules can be defined as closures + if (is_callable($rules)) { + return call_user_func_array($rules, $arguments); + } + + return $rules; } /** - * Dynamically retrieve the value of an attribute. + * Can be overwritten to define rules. * - * @param string $key - * @return mixed + * @deprecated Will be removed in favour of defining rules together with the args. + * @return array */ - public function __get($key) + protected function rules() { - $attributes = $this->getAttributes(); - return isset($attributes[$key]) ? $attributes[$key]:null; + return []; } /** - * Dynamically check if an attribute is set. + * Return an array of custom validation error messages. * - * @param string $key - * @return void + * @return array */ - public function __isset($key) + protected function validationErrorMessages($root, $args, $context) { - $attributes = $this->getAttributes(); - return isset($attributes[$key]); + return []; } } diff --git a/src/Folklore/GraphQL/Support/Mutation.php b/src/Folklore/GraphQL/Support/Mutation.php index e4b710be..bed633f7 100644 --- a/src/Folklore/GraphQL/Support/Mutation.php +++ b/src/Folklore/GraphQL/Support/Mutation.php @@ -2,11 +2,12 @@ namespace Folklore\GraphQL\Support; -use Validator; -use Folklore\GraphQL\Error\ValidationError; -use Folklore\GraphQL\Support\Traits\ShouldValidate; - -class Mutation extends Field +/** + * Mutations are just fields which are expected to change state on the server. + * + * Class Mutation + * @package Folklore\GraphQL\Support + */ +abstract class Mutation extends Field { - use ShouldValidate; } diff --git a/src/Folklore/GraphQL/Support/Query.php b/src/Folklore/GraphQL/Support/Query.php index c59e09ae..12c70f44 100644 --- a/src/Folklore/GraphQL/Support/Query.php +++ b/src/Folklore/GraphQL/Support/Query.php @@ -2,7 +2,12 @@ namespace Folklore\GraphQL\Support; -class Query extends Field +/** + * Queries are simply Fields which are supposed to be idempotent. + * + * Class Query + * @package Folklore\GraphQL\Support + */ +abstract class Query extends Field { - } diff --git a/src/Folklore/GraphQL/Support/Traits/ShouldValidate.php b/src/Folklore/GraphQL/Support/Traits/ShouldValidate.php deleted file mode 100644 index 03ecf5f2..00000000 --- a/src/Folklore/GraphQL/Support/Traits/ShouldValidate.php +++ /dev/null @@ -1,133 +0,0 @@ -args() as $name => $arg) { - if (isset($arg['rules'])) { - $argsRules[$name] = $this->resolveRules($arg['rules'], $arguments); - } - - if (isset($arg['type'])) { - $argsRules = array_merge($argsRules, $this->inferRulesFromType($arg['type'], $name, $arguments)); - } - } - - return array_merge($rules, $argsRules); - } - - public function resolveRules($rules, $arguments) - { - if (is_callable($rules)) { - return call_user_func_array($rules, $arguments); - } - - return $rules; - } - - public function inferRulesFromType($type, $prefix, $resolutionArguments) - { - $rules = []; - - // if it is an array type, add an array validation component - if ($type instanceof ListOfType) { - $prefix = "{$prefix}.*"; - } - - // make sure we are dealing with the actual type - if ($type instanceof WrappingType) { - $type = $type->getWrappedType(); - } - - // if it is an input object type - the only type we care about here... - if ($type instanceof InputObjectType) { - // merge in the input type's rules - $rules = array_merge($rules, $this->getInputTypeRules($type, $prefix, $resolutionArguments)); - } - - // Ignore scalar types - - return $rules; - } - - public function getInputTypeRules(InputObjectType $input, $prefix, $resolutionArguments) - { - $rules = []; - - foreach ($input->getFields() as $name => $field) { - $key = "{$prefix}.{$name}"; - - // get any explicitly set rules - if (isset($field->rules)) { - $rules[$key] = $this->resolveRules($field->rules, $resolutionArguments); - } - - // then recursively call the parent method to see if this is an - // input object, passing in the new prefix - $rules = array_merge($rules, $this->inferRulesFromType($field->type, $key, $resolutionArguments)); - } - - return $rules; - } - - protected function getValidator($args, $rules, $messages = []) - { - $validator = app('validator')->make($args, $rules, $messages); - if (method_exists($this, 'withValidator')) { - $this->withValidator($validator, $args); - } - - return $validator; - } - - protected function getResolver() - { - $resolver = parent::getResolver(); - if (!$resolver) { - return null; - } - - return function () use ($resolver) { - $arguments = func_get_args(); - - $rules = call_user_func_array([$this, 'getRules'], $arguments); - $validationErrorMessages = call_user_func_array([$this, 'validationErrorMessages'], $arguments); - if (sizeof($rules)) { - $args = array_get($arguments, 1, []); - $validator = $this->getValidator($args, $rules, $validationErrorMessages); - if ($validator->fails()) { - throw with(new ValidationError('validation'))->setValidator($validator); - } - } - - return call_user_func_array($resolver, $arguments); - }; - } -} diff --git a/src/config/config.php b/src/config/config.php index 0d51e6e6..c385e7ed 100644 --- a/src/config/config.php +++ b/src/config/config.php @@ -152,17 +152,17 @@ ], /* - * Overrides the default field resolver - * Useful to setup default loading of eager relationships + * Overrides the default field resolver. + * + * This may be useful to setup default loading of eager relationships * * Example: * * 'defaultFieldResolver' => function ($root, $args, $context, $info) { - * // take a look at the defaultFieldResolver in - * // https://github.com/webonyx/graphql-php/blob/master/src/Executor/Executor.php + * // Implement your custom functionality here * }, */ - 'defaultFieldResolver' => null, + 'defaultFieldResolver' => [\GraphQL\Executor\Executor::class, 'defaultFieldResolver'], /* * The types available in the application. You can access them from the diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php index b57c6d23..46c4d320 100644 --- a/tests/ConfigTest.php +++ b/tests/ConfigTest.php @@ -1,8 +1,6 @@ ExamplesRootQuery::class ], 'mutation' => [ - 'updateExample' => UpdateExampleMutation::class + 'exampleMutation' => ExampleMutation::class ] ], 'custom' => [ @@ -38,7 +36,7 @@ protected function getEnvironmentSetUp($app) 'examplesCustom' => ExamplesQuery::class ], 'mutation' => [ - 'updateExampleCustom' => UpdateExampleMutation::class + 'exampleMutationCustom' => ExampleMutation::class ] ], 'shorthand' => BuildSchema::build(' @@ -62,7 +60,9 @@ protected function getEnvironmentSetUp($app) 'types' => [ 'Example' => ExampleType::class, - CustomExampleType::class + CustomExampleType::class, + 'ExampleParentInputObject' => ExampleParentInputObject::class, + 'ExampleChildInputObject' => ExampleChildInputObject::class, ], 'security' => [ @@ -79,7 +79,7 @@ public function testRouteQuery() 'query' => $this->queries['examplesCustom'] ]); - $this->assertEquals($response->getStatusCode(), 200); + $this->assertEquals(200, $response->getStatusCode()); $content = $response->getData(true); $this->assertArrayHasKey('data', $content); @@ -88,10 +88,13 @@ public function testRouteQuery() public function testRouteMutation() { $response = $this->call('POST', '/graphql_test/mutation', [ - 'query' => $this->queries['updateExampleCustom'] + 'query' => $this->queries['exampleMutation'], + 'params' => [ + 'required' => 'test' + ], ]); - $this->assertEquals($response->getStatusCode(), 200); + $this->assertEquals(200, $response->getStatusCode()); $content = $response->getData(true); $this->assertArrayHasKey('data', $content); @@ -130,7 +133,7 @@ public function testVariablesInputName() ] ]); - $this->assertEquals($response->getStatusCode(), 200); + $this->assertEquals(200, $response->getStatusCode()); $content = $response->getData(true); $this->assertArrayHasKey('data', $content); @@ -150,7 +153,7 @@ public function testVariablesInputNameForShorthandResolver() ], ]); - $this->assertEquals($response->getStatusCode(), 200); + $this->assertEquals(200, $response->getStatusCode()); $content = $response->getData(true); $this->assertArrayHasKey('data', $content); @@ -181,6 +184,6 @@ public function testErrorFormatter() 'graphql.error_formatter' => [$error, 'formatError'] ]); - $result = GraphQL::query($this->queries['examplesWithError']); + GraphQL::query($this->queries['examplesWithError']); } } diff --git a/tests/FieldTest.php b/tests/FieldTest.php index 53817610..90654841 100644 --- a/tests/FieldTest.php +++ b/tests/FieldTest.php @@ -1,16 +1,26 @@ getFieldClass(); + return new $class(); + } + /** * Test get attributes * @@ -18,8 +28,7 @@ protected function getFieldClass() */ public function testGetAttributes() { - $class = $this->getFieldClass(); - $field = new $class(); + $field = $this->getFieldInstance(); $attributes = $field->getAttributes(); $this->assertArrayHasKey('name', $attributes); @@ -32,11 +41,11 @@ public function testGetAttributes() } /** - * Test resolve closure + * Test the calling of a custom resolve function. * * @test */ - public function testResolve() + public function testResolveFunctionIsCalled() { $class = $this->getFieldClass(); $field = $this->getMockBuilder($class) @@ -47,7 +56,7 @@ public function testResolve() ->method('resolve'); $attributes = $field->getAttributes(); - $attributes['resolve'](null, [], [], null); + $attributes['resolve'](null, [], [], new \GraphQL\Type\Definition\ResolveInfo([])); } /** @@ -57,8 +66,7 @@ public function testResolve() */ public function testToArray() { - $class = $this->getFieldClass(); - $field = new $class(); + $field = $this->getFieldInstance(); $array = $field->toArray(); $this->assertInternalType('array', $array); diff --git a/tests/GraphQLQueryTest.php b/tests/GraphQLQueryTest.php index f8a71a99..d0d2cc7b 100644 --- a/tests/GraphQLQueryTest.php +++ b/tests/GraphQLQueryTest.php @@ -1,11 +1,5 @@ queries['examplesWithValidation']); + $result = GraphQL::query($this->queries['examplesWithValidation'], [ + 'index' => 0 + ]); $this->assertArrayHasKey('data', $result); - $this->assertArrayHasKey('errors', $result); - $this->assertArrayHasKey('validation', $result['errors'][0]); - $this->assertTrue($result['errors'][0]['validation']->has('index')); + $this->assertArrayNotHasKey('errors', $result); } /** - * Test query with validation without error + * Test query with validation error. * * @test */ - public function testQueryWithValidation() + public function testQueryWithValidationError() { $result = GraphQL::query($this->queries['examplesWithValidation'], [ - 'index' => 0 + // The validation requires this to be below 100 + 'index' => 9001 ]); $this->assertArrayHasKey('data', $result); - $this->assertArrayNotHasKey('errors', $result); + $this->assertArrayHasKey('errors', $result); + $this->assertArrayHasKey('validation', $result['errors'][0]); + $this->assertTrue($result['errors'][0]['validation']->has('index')); } } diff --git a/tests/GraphQLTest.php b/tests/GraphQLTest.php index 143d6580..910d250d 100644 --- a/tests/GraphQLTest.php +++ b/tests/GraphQLTest.php @@ -1,12 +1,12 @@ assertGraphQLSchema($schema); $this->assertGraphQLSchemaHasQuery($schema, 'examples'); - $this->assertGraphQLSchemaHasMutation($schema, 'updateExample'); + $this->assertGraphQLSchemaHasMutation($schema, 'exampleMutation'); $this->assertArrayHasKey('Example', $schema->getTypeMap()); } @@ -58,7 +58,7 @@ public function testSchemaWithName() $this->assertGraphQLSchema($schema); $this->assertGraphQLSchemaHasQuery($schema, 'examplesCustom'); - $this->assertGraphQLSchemaHasMutation($schema, 'updateExampleCustom'); + $this->assertGraphQLSchemaHasMutation($schema, 'exampleMutationCustom'); $this->assertArrayHasKey('Example', $schema->getTypeMap()); } @@ -74,7 +74,7 @@ public function testSchemaWithArray() 'examplesCustom' => ExamplesQuery::class ], 'mutation' => [ - 'updateExampleCustom' => UpdateExampleMutation::class + 'updateExampleCustom' => ExampleMutation::class ], 'types' => [ CustomExampleType::class @@ -280,7 +280,7 @@ public function testAddSchema() 'examplesCustom' => ExamplesQuery::class ], 'mutation' => [ - 'updateExampleCustom' => UpdateExampleMutation::class + 'updateExampleCustom' => ExampleMutation::class ], 'types' => [ CustomExampleType::class diff --git a/tests/MutationTest.php b/tests/MutationTest.php index 4dc4d2d5..112b03a5 100644 --- a/tests/MutationTest.php +++ b/tests/MutationTest.php @@ -6,142 +6,117 @@ class MutationTest extends FieldTest { protected function getFieldClass() { - return UpdateExampleMutationWithInputType::class; - } - - protected function getEnvironmentSetUp($app) - { - parent::getEnvironmentSetUp($app); - - $app['config']->set('graphql.types', [ - 'Example' => ExampleType::class, - 'ExampleValidationInputObject' => ExampleValidationInputObject::class, - 'ExampleNestedValidationInputObject' => ExampleNestedValidationInputObject::class, - ]); + return ExampleMutation::class; } /** - * Test get rules. + * Laravel based validation rules should be correctly constructed. * * @test */ public function testGetRules() { - $class = $this->getFieldClass(); - $field = new $class(); + $field = $this->getFieldInstance(); $rules = $field->getRules(); $this->assertInternalType('array', $rules); - $this->assertArrayHasKey('test', $rules); - $this->assertArrayHasKey('test_with_rules', $rules); - $this->assertArrayHasKey('test_with_rules_closure', $rules); - $this->assertEquals($rules['test'], ['required']); - $this->assertEquals($rules['test_with_rules'], ['required']); - $this->assertEquals($rules['test_with_rules_closure'], ['required']); - $this->assertEquals($rules['test_with_rules_input_object'], ['required']); - $this->assertEquals(array_get($rules, 'test_with_rules_input_object.val'), ['required']); - $this->assertEquals(array_get($rules, 'test_with_rules_input_object.nest'), ['required']); - $this->assertEquals(array_get($rules, 'test_with_rules_input_object.nest.email'), ['email']); - $this->assertEquals(array_get($rules, 'test_with_rules_input_object.list'), ['required']); - $this->assertEquals(array_get($rules, 'test_with_rules_input_object.list.*.email'), ['email']); - } - /** - * Test resolve. - * - * @test - */ - public function testResolve() - { - $class = $this->getFieldClass(); - $field = $this->getMockBuilder($class) - ->setMethods(['resolve']) - ->getMock(); - - $field->expects($this->once()) - ->method('resolve'); - - $attributes = $field->getAttributes(); - $attributes['resolve'](null, [ - 'test' => 'test', - 'test_with_rules' => 'test', - 'test_with_rules_closure' => 'test', - 'test_with_rules_input_object' => [ - 'val' => 'test', - 'nest' => ['email' => 'test@test.com'], - 'list' => [ - ['email' => 'test@test.com'], - ], - ], - ], [], null); + // Those three definitions must have the same result + $this->assertEquals(['email'], $rules['email_seperate_rules']); + $this->assertEquals(['email'], $rules['email_inline_rules']); + $this->assertEquals(['email'], $rules['email_closure_rules']); + + // Apply array validation to values defined as GraphQL-Lists + $this->assertEquals(['email'], $rules['email_list.*']); + $this->assertEquals(['email'], $rules['email_list_of_lists.*.*']); + + // Inferred rules from Input Objects + $this->assertEquals(['alpha'], $rules['input_object.alpha']); + $this->assertEquals(['email'], $rules['input_object.child.email']); + $this->assertEquals(['email'], $rules['input_object.child-list.*.email']); + + // Self-referencing, nested InputObject + $this->assertEquals(['alpha'], $rules['input_object.self.alpha']); + $this->assertEquals(['email'], $rules['input_object.self.child.email']); + $this->assertEquals(['email'], $rules['input_object.self.child-list.*.email']); + + // Go down a few levels + $this->assertEquals(['alpha'], $rules['input_object.self.self.self.self.alpha']); + $this->assertEquals(['email'], $rules['input_object.self.self.self.self.child.email']); + $this->assertEquals(['email'], $rules['input_object.self.self.self.self.child-list.*.email']); + + $this->assertArrayNotHasKey( + 'input_object.self.self.self.self.self.self.self.self.self.self.alpha', + $rules, + 'Validation rules should not be set for such deep nesting.'); } /** * Test resolve throw validation error. * * @test - * @expectedException \Folklore\GraphQL\Error\ValidationError */ public function testResolveThrowValidationError() { - $class = $this->getFieldClass(); - $field = new $class(); - + $field = $this->getFieldInstance(); $attributes = $field->getAttributes(); - $attributes['resolve'](null, [], [], null); + + $this->expectException('\Folklore\GraphQL\Error\ValidationError'); + $attributes['resolve'](null, [ + 'email_inline_rules' => 'not-an-email' + ], [], null); } /** - * Test validation error. + * Validation error messages are correctly constructed and thrown. * * @test */ - public function testValidationError() + public function testValidationErrorMessages() { - $class = $this->getFieldClass(); - $field = new $class(); - + $field = $this->getFieldInstance(); $attributes = $field->getAttributes(); try { - $attributes['resolve'](null, [], [], null); + $attributes['resolve'](null, [ + 'email_inline_rules' => 'not-an-email', + 'email_list' => ['not-an-email', 'valid@email.com'], + 'email_list_of_lists' => [ + ['valid@email.com'], + ['not-an-email'], + ], + 'input_object' => [ + 'child' => [ + 'email' => 'not-an-email' + ], + 'self' => [ + 'self' => [ + 'alpha' => 'Not alphanumeric !"§)' + ] + ] + ] + ], [], null); } catch (\Folklore\GraphQL\Error\ValidationError $e) { $validator = $e->getValidator(); $this->assertInstanceOf(Validator::class, $validator); + /** @var \Illuminate\Support\MessageBag $messages */ $messages = $e->getValidatorMessages(); - $this->assertTrue($messages->has('test')); - $this->assertTrue($messages->has('test_with_rules')); - $this->assertTrue($messages->has('test_with_rules_closure')); - $this->assertTrue($messages->has('test_with_rules_input_object.val')); - $this->assertTrue($messages->has('test_with_rules_input_object.nest')); - $this->assertTrue($messages->has('test_with_rules_input_object.list')); - } - } - - /** - * Test custom validation error messages. - * - * @test - */ - public function testCustomValidationErrorMessages() - { - $class = $this->getFieldClass(); - $field = new $class(); - $rules = $field->getRules(); - $attributes = $field->getAttributes(); - try { - $attributes['resolve'](null, [ - 'test_with_rules_input_object' => [ - 'nest' => ['email' => 'invalidTestEmail.com'], - ], - ], [], null); - } catch (\Folklore\GraphQL\Error\ValidationError $e) { - $messages = $e->getValidatorMessages(); + $messageKeys = $messages->keys(); + // Ensure that validation errors occurred only where necessary, ignoring the order + $this->assertEquals([ + 'email_inline_rules', + 'email_list.0', + 'email_list_of_lists.1.0', + 'input_object.child.email', + 'input_object.self.self.alpha', + ], $messageKeys, 'Not all the right fields were validated.', 0, 10, true); + + // The custom validation error message should override the default + $this->assertEquals('Has to be a valid email.', $messages->first('email_inline_rules')); + $this->assertEquals('Invalid email: not-an-email', $messages->first('input_object.child.email')); - $this->assertEquals($messages->first('test'), 'A test is required.'); - $this->assertEquals($messages->first('test_with_rules_input_object.nest.email'), 'Invalid your email : invalidTestEmail.com'); } } } diff --git a/tests/Objects/ExampleNestedValidationInputObject.php b/tests/Objects/ExampleChildInputObject.php similarity index 54% rename from tests/Objects/ExampleNestedValidationInputObject.php rename to tests/Objects/ExampleChildInputObject.php index d4a394c9..fa478a64 100644 --- a/tests/Objects/ExampleNestedValidationInputObject.php +++ b/tests/Objects/ExampleChildInputObject.php @@ -1,22 +1,16 @@ 'ExampleNestedValidationInputObject' + 'name' => 'ExampleChildInputObject' ]; - public function type() - { - return Type::listOf(Type::string()); - } - public function fields() { return [ @@ -27,9 +21,4 @@ public function fields() ], ]; } - - public function resolve($root, $args) - { - return ['test']; - } } diff --git a/tests/Objects/ExampleField.php b/tests/Objects/ExampleField.php index 4c1face2..5e0f1c87 100644 --- a/tests/Objects/ExampleField.php +++ b/tests/Objects/ExampleField.php @@ -1,7 +1,7 @@ 'exampleMutation' + ]; + + public function type() + { + return GraphQL::type('Example'); + } + + public function args() + { + return [ + 'required' => [ + 'name' => 'required', + // Define required args through GraphQL types instead of Laravel validation + // This way graphql-php takes care of validating that and the requirements + // show up in the schema. + 'type' => Type::nonNull(Type::string()), + ], + + 'email_seperate_rules' => [ + 'name' => 'email_seperate_rules', + 'type' => Type::string() + ], + + 'email_inline_rules' => [ + 'name' => 'email_inline_rules', + 'type' => Type::string(), + 'rules' => ['email'] + ], + + 'email_closure_rules' => [ + 'name' => 'email_closure_rules', + 'type' => Type::string(), + 'rules' => function () { + return ['email']; + } + ], + + 'email_list' => [ + 'name' => 'email_list', + 'type' => Type::listOf(Type::string()), + 'rules' => ['email'], + ], + + 'email_list_of_lists' => [ + 'name' => 'email_list_of_lists', + 'type' => Type::listOf(Type::listOf(Type::string())), + 'rules' => ['email'], + ], + + 'input_object' => [ + 'name' => 'input_object', + 'type' => GraphQL::type('ExampleParentInputObject'), + ], + ]; + } + + public function resolve($root, $args, $context, ResolveInfo $info) + { + return [ + 'test' => array_get($args, 'test') + ]; + } + + protected function rules() + { + return [ + 'email_seperate_rules' => ['email'] + ]; + } + + protected function validationErrorMessages($root, $args, $context) + { + $invalidEmail = array_get($args, 'input_object.child.email'); + + return [ + 'email_inline_rules.email' => 'Has to be a valid email.', + 'input_object.child.email.email' => 'Invalid email: ' . $invalidEmail, + ]; + } +} diff --git a/tests/Objects/ExampleParentInputObject.php b/tests/Objects/ExampleParentInputObject.php new file mode 100644 index 00000000..89531b1f --- /dev/null +++ b/tests/Objects/ExampleParentInputObject.php @@ -0,0 +1,52 @@ + 'ExampleParentInputObject', + ]; + + public function type() + { + return Type::listOf(Type::string()); + } + + public function fields() + { + return [ + + 'alpha' => [ + 'name' => 'alpha', + 'type' => Type::nonNull(Type::string()), + 'rules' => ['alpha'], + ], + + 'child' => [ + 'name' => 'child', + 'type' => GraphQL::type('ExampleChildInputObject'), + ], + + 'child-list' => [ + 'name' => 'child-list', + 'type' => Type::listOf(GraphQL::type('ExampleChildInputObject')), + ], + + // Reference itself. Used in test for avoiding infinite loop when creating validation rules + 'self' => [ + 'name' => 'self', + 'type' => GraphQL::type('ExampleParentInputObject'), + ], + + ]; + } + + public function resolve($root, $args) + { + return ['test']; + } +} diff --git a/tests/Objects/ExampleValidationField.php b/tests/Objects/ExampleValidationField.php index c2edece5..3de80a65 100644 --- a/tests/Objects/ExampleValidationField.php +++ b/tests/Objects/ExampleValidationField.php @@ -1,13 +1,10 @@ 'example_validation' ]; @@ -23,13 +20,8 @@ public function args() 'index' => [ 'name' => 'index', 'type' => Type::int(), - 'rules' => ['required'] + 'rules' => ['integer', 'max:100'] ] ]; } - - public function resolve($root, $args) - { - return ['test']; - } } diff --git a/tests/Objects/ExampleValidationInputObject.php b/tests/Objects/ExampleValidationInputObject.php deleted file mode 100644 index 738eae58..00000000 --- a/tests/Objects/ExampleValidationInputObject.php +++ /dev/null @@ -1,45 +0,0 @@ - 'ExampleValidationInputObject' - ]; - - public function type() - { - return Type::listOf(Type::string()); - } - - public function fields() - { - return [ - 'val' => [ - 'name' => 'val', - 'type' => Type::int(), - 'rules' => ['required'] - ], - 'nest' => [ - 'name' => 'nest', - 'type' => GraphQL::type('ExampleNestedValidationInputObject'), - 'rules' => ['required'] - ], - 'list' => [ - 'name' => 'list', - 'type' => Type::listOf(GraphQL::type('ExampleNestedValidationInputObject')), - 'rules' => ['required'] - ], - ]; - } - - public function resolve($root, $args) - { - return ['test']; - } -} diff --git a/tests/Objects/ExamplesContextQuery.php b/tests/Objects/ExamplesContextQuery.php index c051ec81..df2cc10c 100644 --- a/tests/Objects/ExamplesContextQuery.php +++ b/tests/Objects/ExamplesContextQuery.php @@ -1,7 +1,7 @@ ['name' => 'index', 'type' => Type::int()] + 'index' => [ + 'name' => 'index', + 'type' => Type::int(), + 'rules' => ['integer', 'max:100'] + ] ]; } - public function resolve($root, $args) + public function resolve($root, $args, $context, ResolveInfo $info) { - $data = include(__DIR__.'/data.php'); + $data = include(__DIR__ . '/data.php'); if (isset($args['index'])) { return [ diff --git a/tests/Objects/ExamplesRootQuery.php b/tests/Objects/ExamplesRootQuery.php index 730673af..f12f1a90 100644 --- a/tests/Objects/ExamplesRootQuery.php +++ b/tests/Objects/ExamplesRootQuery.php @@ -1,7 +1,7 @@ 'updateExample' - ]; - - public function type() - { - return GraphQL::type('Example'); - } - - public function rules() - { - return [ - 'test' => ['required'] - ]; - } - - public function args() - { - return [ - 'test' => [ - 'name' => 'test', - 'type' => Type::string() - ], - - 'test_with_rules' => [ - 'name' => 'test', - 'type' => Type::string(), - 'rules' => ['required'] - ], - - 'test_with_rules_closure' => [ - 'name' => 'test', - 'type' => Type::string(), - 'rules' => function () { - return ['required']; - } - ], - ]; - } - - public function resolve($root, $args) - { - return [ - 'test' => array_get($args, 'test') - ]; - } -} diff --git a/tests/Objects/UpdateExampleMutationWithInputType.php b/tests/Objects/UpdateExampleMutationWithInputType.php deleted file mode 100644 index 2f90a185..00000000 --- a/tests/Objects/UpdateExampleMutationWithInputType.php +++ /dev/null @@ -1,71 +0,0 @@ - 'updateExample', - ]; - - public function type() - { - return GraphQL::type('Example'); - } - - public function rules() - { - return [ - 'test' => ['required'], - ]; - } - - public function validationErrorMessages($root, $args, $context) - { - $inavlidEmail = array_get($args, 'test_with_rules_input_object.nest.email'); - - return [ - 'test.required' => 'A test is required.', - 'test_with_rules_input_object.nest.email.email' => 'Invalid your email : '.$inavlidEmail, - ]; - } - - public function args() - { - return [ - 'test' => [ - 'name' => 'test', - 'type' => Type::string(), - ], - - 'test_with_rules' => [ - 'name' => 'test', - 'type' => Type::string(), - 'rules' => ['required'], - ], - - 'test_with_rules_closure' => [ - 'name' => 'test', - 'type' => Type::string(), - 'rules' => function () { - return ['required']; - }, - ], - - 'test_with_rules_input_object' => [ - 'name' => 'test', - 'type' => GraphQL::type('ExampleValidationInputObject'), - 'rules' => ['required'], - ], - ]; - } - - public function resolve($root, $args) - { - return [ - 'test' => array_get($args, 'test'), - ]; - } -} diff --git a/tests/Objects/queries.php b/tests/Objects/queries.php index 0bcebcb2..4554cd5e 100644 --- a/tests/Objects/queries.php +++ b/tests/Objects/queries.php @@ -75,15 +75,15 @@ 'examplesWithValidation' => " query QueryExamplesWithValidation(\$index: Int) { - examples { - test_validation(index: \$index) + examples(index: \$index) { + test } } ", - 'updateExampleCustom' => " - mutation UpdateExampleCustom(\$test: String) { - updateExampleCustom(test: \$test) { + 'exampleMutation' => " + mutation ExampleMutation(\$required: String) { + exampleMutation(required: \$required) { test } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 3ac5406b..7cdddf82 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -30,7 +30,7 @@ protected function getEnvironmentSetUp($app) 'examplesPagination' => ExamplesPaginationQuery::class, ], 'mutation' => [ - 'updateExample' => UpdateExampleMutation::class + 'exampleMutation' => ExampleMutation::class ] ]); @@ -39,12 +39,14 @@ protected function getEnvironmentSetUp($app) 'examplesCustom' => ExamplesQuery::class, ], 'mutation' => [ - 'updateExampleCustom' => UpdateExampleMutation::class + 'exampleMutationCustom' => ExampleMutation::class ] ]); $app['config']->set('graphql.types', [ - 'Example' => ExampleType::class + 'Example' => ExampleType::class, + 'ExampleParentInputObject' => ExampleParentInputObject::class, + 'ExampleChildInputObject' => ExampleChildInputObject::class, ]); } From 2f0e41aa017088ba8d167616c669e0525dd3a35b Mon Sep 17 00:00:00 2001 From: spawnia Date: Wed, 16 May 2018 10:29:34 +0200 Subject: [PATCH 2/6] Change expectException method call back to annotation --- tests/MutationTest.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/MutationTest.php b/tests/MutationTest.php index 112b03a5..ee9e4f91 100644 --- a/tests/MutationTest.php +++ b/tests/MutationTest.php @@ -53,7 +53,7 @@ public function testGetRules() /** * Test resolve throw validation error. - * + * @expectedException \Folklore\GraphQL\Error\ValidationError * @test */ public function testResolveThrowValidationError() @@ -61,7 +61,6 @@ public function testResolveThrowValidationError() $field = $this->getFieldInstance(); $attributes = $field->getAttributes(); - $this->expectException('\Folklore\GraphQL\Error\ValidationError'); $attributes['resolve'](null, [ 'email_inline_rules' => 'not-an-email' ], [], null); From 6a2788514c9d2a2521d1b3d24cda0a4153504887 Mon Sep 17 00:00:00 2001 From: spawnia Date: Wed, 16 May 2018 10:52:23 +0200 Subject: [PATCH 3/6] Sort rules array before assertion to workaround PHPUnit 4 incompatiblity --- tests/MutationTest.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/MutationTest.php b/tests/MutationTest.php index ee9e4f91..945cdc85 100644 --- a/tests/MutationTest.php +++ b/tests/MutationTest.php @@ -103,14 +103,16 @@ public function testValidationErrorMessages() /** @var \Illuminate\Support\MessageBag $messages */ $messages = $e->getValidatorMessages(); $messageKeys = $messages->keys(); - // Ensure that validation errors occurred only where necessary, ignoring the order - $this->assertEquals([ + $expectedKeys = [ 'email_inline_rules', 'email_list.0', 'email_list_of_lists.1.0', 'input_object.child.email', 'input_object.self.self.alpha', - ], $messageKeys, 'Not all the right fields were validated.', 0, 10, true); + ]; + // Ensure that validation errors occurred only where necessary + // Sort the arrays before comparison so that order does not matter + $this->assertEquals(sort($expectedKeys), sort($messageKeys), 'Not all the right fields were validated.'); // The custom validation error message should override the default $this->assertEquals('Has to be a valid email.', $messages->first('email_inline_rules')); From 467f790d6a5b58d792590119c074e2ed575e0473 Mon Sep 17 00:00:00 2001 From: spawnia Date: Fri, 18 May 2018 10:04:57 +0200 Subject: [PATCH 4/6] Add type hints to ValidationError --- .../GraphQL/Error/ValidationError.php | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/Folklore/GraphQL/Error/ValidationError.php b/src/Folklore/GraphQL/Error/ValidationError.php index b87a50b8..184ce506 100644 --- a/src/Folklore/GraphQL/Error/ValidationError.php +++ b/src/Folklore/GraphQL/Error/ValidationError.php @@ -1,26 +1,39 @@ validator = $validator; return $this; } - + + /** + * @return Validator + */ public function getValidator() { return $this->validator; } - + + /** + * @return array|\Illuminate\Support\MessageBag + */ public function getValidatorMessages() { - return $this->validator ? $this->validator->messages():[]; + return $this->validator ? $this->validator->messages() : []; } } From 364f051a55729c1e8f8b217f66cdfd5b79e9459b Mon Sep 17 00:00:00 2001 From: spawnia Date: Fri, 18 May 2018 10:19:58 +0200 Subject: [PATCH 5/6] Rework validation to create only minimal necessary rules --- Readme.md | 145 ++++++++---- src/Folklore/GraphQL/Support/Field.php | 261 +++++++++++---------- tests/MutationTest.php | 199 ++++++++++------ tests/Objects/ExampleMutation.php | 15 +- tests/Objects/ExampleParentInputObject.php | 4 +- 5 files changed, 386 insertions(+), 238 deletions(-) diff --git a/Readme.md b/Readme.md index 0694edcc..30b2c21c 100644 --- a/Readme.md +++ b/Readme.md @@ -429,12 +429,12 @@ reflected through the schema and are validated by the underlying GraphQL impleme #### Rule definition -You can add validation rules directly to the arguments of Mutations or Queries: +##### Inline Array +The preferred way to add rules is to inline them with the arguments of Mutations or Queries. ```php //... - class UpdateUserEmailMutation extends Mutation { //... @@ -442,37 +442,82 @@ class UpdateUserEmailMutation extends Mutation public function args() { return [ - 'id' => [ - 'name' => 'id', - 'type' => Type::nonNull(Type::string()), - // Adding a rule for 'required' is not necessary - ], 'email' => [ 'name' => 'email', - 'type' => Type::nonNull(Type::string()), - 'rules' => ['email'] - ], - 'links' => [ - 'name' => 'links', - 'type' => Type::listOf(Type::string()), - // Validation is applied to each element of the list - 'rules' => ['url'] - ], - 'name' => [ - 'name' => 'name', - 'type' => GraphQL::type('NameInputObject') + 'type' => Type::string(), + 'rules' => [ + 'email', + 'exists:users,email' + ] ], ]; } - - //... } ``` +##### Inline Closure + +Rules may also be defined as closures. They are called before the resolve function of the field is called +and receive the same arguments. + +````php +'phone' => [ + 'name' => 'phone', + 'type' => Type::nonNull(Type::string()), + 'rules' => function ($root, $args, $context, \GraphQL\Type\Definition\ResolveInfo $resolveInfo){ + return []; + } +], +```` + +##### Rule Overwrites + +You can overwrite inline rules of fields or nested Input Objects by defining them like this: + +````php +public function rules() +{ + return [ + 'email' => ['email', 'min:10'], + 'nested.value' => ['alpha_num'], + ]; +} +```` + +Be aware that those rules are always applied, even if the argument is not given. You may want to prefix +them with `sometimes` if the rule is optional. + +#### Required Arguments + +GraphQL has a built-in way of defining arguments as required, simply wrap them in a `Type::nonNull()`. + +````php +'id' => [ + 'name' => 'id', + 'type' => Type::nonNull(Type::string()), +], +```` + +The presence of such arguments is checked before the arguments even reach the resolver, so there is +no need to validate them through an additional rule, so you will not ever need `required`. +Defining required arguments through the Non-Null type is preferable because it shows up in the schema definition. + +Because GraphQL arguments are optional by default, the validation rules for them will only be applied if they are present. +If you need more sophisticated validation of fields, using additional rules like `required_with` is fine. + #### Input Object Rules -Notice how the argument `name` has the type `NameInputObject`? The rules defined in Input Objects are also considered when -validating the Mutation! The definition of those rules looks like this: +You may use Input Objects as arguments like this: + +````php +'name' => [ + 'name' => 'name', + 'type' => GraphQL::type('NameInputObject') +], +```` + +Rules defined in the Input Object are automatically added to the validation, even if nested Input Objects are used. +The definition of those rules looks like this: ````php [ + 'name' => 'links', + 'type' => Type::listOf(Type::string()), + 'rules' => ['url', 'distinct'], +], +```` + +If validation on the array itself is required, you can do so by defining those rules seperately: + +````php +public function rules() +{ + return [ + 'links' => ['max:10'] + ]; +} +```` + +This ensures that `links` is an array of at most 10, distinct urls. + #### Response format -When you execute a mutation, it will return the validation errors. Since the GraphQL specification defines a certain format for errors, the validation error messages are added to the error object as an extra `validation` attribute. To find the validation error, you should check for the error with a `message` equals to `'validation'`, then the `validation` attribute will contain the normal errors messages returned by the Laravel Validator. +When you execute a field with arguments, it will return the validation errors. +Since the GraphQL specification defines a certain format for errors, the validation error messages +are added to the error object as an extra `validation` attribute. + +To find the validation error, you should check for the error with a `message` +equals to `'validation'`, then the `validation` attribute will contain the normal +errors messages returned by the Laravel Validator. ```json { @@ -536,19 +615,3 @@ When you execute a mutation, it will return the validation errors. Since the Gra ] } ``` - -#### Validation depth - -Laravel validation works by statically generating validation rules and then applying -them to arguments all at once. However, field arguments may consist of Input Objects, which -could have circular references. Because of this, the maximum depth for validation has to be -set before. The default depth is set to `10`, it can be overwritten this way: - -````php -class ExampleMutation extends Mutation -{ - protected $validationDepth = 15; - - //... -} -```` diff --git a/src/Folklore/GraphQL/Support/Field.php b/src/Folklore/GraphQL/Support/Field.php index 79f90fc2..01898d57 100644 --- a/src/Folklore/GraphQL/Support/Field.php +++ b/src/Folklore/GraphQL/Support/Field.php @@ -4,13 +4,12 @@ use Folklore\GraphQL\Error\AuthorizationError; use Folklore\GraphQL\Error\ValidationError; -use GraphQL\Type\Definition\InputObjectField; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\WrappingType; -use Illuminate\Contracts\Validation\Validator; use Illuminate\Support\Fluent; +use Illuminate\Validation\Validator; /** * This is a generic class for fields. @@ -23,20 +22,6 @@ */ abstract class Field extends Fluent { - /** - * Validation rules for the Field arguments. - * - * @var array - */ - protected $rules = []; - - /** - * How many levels of nesting should validation be applied to. - * - * @var int - */ - protected $validationDepth = 10; - /** * Override this in your queries or mutations to provide custom authorization. * @@ -110,22 +95,6 @@ public function args() */ public abstract function type(); - /** - * Return a result for the field. - * - * @param $root - * @param $args - * @param $context - * @param ResolveInfo $info - * @return mixed - */ - public function resolve($root, $args, $context, ResolveInfo $info) - { - // Use the default resolver, can be overridden in custom Fields, Queries or Mutations - // This enables us to still apply authentication, authorization and validation upon the query - return call_user_func(config('graphql.defaultFieldResolver')); - } - /** * Returns a function that wraps the resolve function with authentication and validation checks. * @@ -133,54 +102,40 @@ public function resolve($root, $args, $context, ResolveInfo $info) */ protected function getResolver() { - $authenticated = [$this, 'authenticated']; - $authorize = [$this, 'authorize']; - $resolve = [$this, 'resolve']; - - return function () use ($authenticated, $authorize, $resolve) { - $args = func_get_args(); + return function () { + $resolutionArguments = func_get_args(); // Check authentication first - if (call_user_func_array($authenticated, $args) !== true) { + if (call_user_func_array([$this, 'authenticated'], $resolutionArguments) !== true) { throw new AuthorizationError('Unauthenticated'); } // After authentication, check specific authorization - if (call_user_func_array($authorize, $args) !== true) { + if (call_user_func_array([$this, 'authorize'], $resolutionArguments) !== true) { throw new AuthorizationError('Unauthorized'); } - // Apply additional validation - $rules = call_user_func_array([$this, 'getRules'], $args); - if (sizeof($rules)) { - $validationErrorMessages = call_user_func_array([$this, 'validationErrorMessages'], $args); - $inputArguments = array_get($args, 1, []); - $validator = $this->getValidator($inputArguments, $rules, $validationErrorMessages); - if ($validator->fails()) { - throw with(new ValidationError('validation'))->setValidator($validator); - } - } + call_user_func_array([$this, 'validate'], $resolutionArguments); + - return call_user_func_array($resolve, $args); + return call_user_func_array([$this, 'resolve'], $resolutionArguments); }; } /** - * @param $args - * @param $rules - * @param array $messages + * Return a result for the field. * - * @return Validator + * @param $root + * @param $args + * @param $context + * @param ResolveInfo $info + * @return mixed */ - protected function getValidator($args, $rules, $messages = []) + public function resolve($root, $args, $context, ResolveInfo $info) { - /** @var Validator $validator */ - $validator = app('validator')->make($args, $rules, $messages); - if (method_exists($this, 'withValidator')) { - $this->withValidator($validator, $args); - } - - return $validator; + // Use the default resolver, can be overridden in custom Fields, Queries or Mutations + // This enables us to still apply authentication, authorization and validation upon the query + return call_user_func(config('graphql.defaultFieldResolver')); } /** @@ -207,87 +162,142 @@ public function __isset($key) return isset($attributes[$key]); } + /** + * Overwrite rules at any part in the tree of field arguments. + * + * The rules defined in here take precedence over the rules that are + * defined inline or inferred from nested Input Objects. + * + * @return array + */ + public function rules() + { + return []; + } + + /** + * Gather all the rules and throw if invalid. + */ + protected function validate() + { + $resolutionArguments = func_get_args(); + $inputArguments = array_get($resolutionArguments, 1, []); + + $argumentRules = $this->getRulesForArguments($this->args(), $inputArguments); + $explicitRules = call_user_func_array([$this, 'rules'], $resolutionArguments); + $argumentRules = array_merge($argumentRules, $explicitRules); + + foreach ($argumentRules as $key => $rules) { + $resolvedRules[$key] = $this->resolveRules($rules, $resolutionArguments); + } + + if (isset($resolvedRules)) { + $validationErrorMessages = call_user_func_array([$this, 'validationErrorMessages'], $resolutionArguments); + $validator = $this->getValidator($inputArguments, $resolvedRules, $validationErrorMessages); + if ($validator->fails()) { + throw (new ValidationError('validation'))->setValidator($validator); + } + } + } + /** * Get the combined explicit and type-inferred rules. * + * @param $argDefinitions + * @param $argValues * @return array */ - public function getRules() + protected function getRulesForArguments($argDefinitions, $argValues) { - $arguments = func_get_args(); + $rules = []; + + foreach ($argValues as $name => $value) { + $definition = $argDefinitions[$name]; - $argsRules = $this->getRulesFromArgs($this->args(), $this->validationDepth, null, $arguments); + $typeAndKey = $this->unwrapType($definition['type'], $name); + $key = $typeAndKey['key']; + $type = $typeAndKey['type']; + + // Get rules that are directly defined on the field + if (isset($definition['rules'])) { + $rules[$key] = $definition['rules']; + } + + if ($type instanceof InputObjectType) { + $rules = array_merge($rules, $this->getRulesFromInputObjectType($key, $type, $value)); + } + } - // Merge rules that were set separately with those defined through args - $explicitRules = call_user_func_array([$this, 'rules'], $arguments); - return array_merge($argsRules, $explicitRules); + return $rules; } /** - * @param $fields - * @param $inputValueDepth - * @param $parentKey - * @param $resolutionArguments + * @param $type + * @param $key * @return array */ - protected function getRulesFromArgs($fields, $inputValueDepth, $parentKey, $resolutionArguments) + protected function unwrapType($type, $key) { - // At depth 0, there are still field rules to gather - // once we get below 0, we have gathered all the rules necessary - // for the nesting depth - if ($inputValueDepth < 0) { - return []; + // Unpack until we get to the root type + while ($type instanceof WrappingType) { + if ($type instanceof ListOfType) { + // Add this to the prefix to allow Laravel validation for arrays + $key .= '.*'; + } + + $type = $type->getWrappedType(); } - // We are going one level deeper - $inputValueDepth--; + return [ + 'type' => $type, + 'key' => $key, + ]; + } + protected function getRulesFromInputObjectType($parentKey, InputObjectType $inputObject, $values) + { $rules = []; - // Merge in the rules of the Input Type - foreach ($fields as $fieldName => $field) { - // Count the depth per field - $fieldDepth = $inputValueDepth; - - // If given, add the parent key - $key = $parentKey ? "{$parentKey}.{$fieldName}" : $fieldName; - - // The values passed in here may be of different types, depending on where they were defined - if ($field instanceof InputObjectField) { - // We can depend on type being set, since a field without type is not valid - $type = $field->type; - // Rules are optional so they may not be set - $fieldRules = isset($field->rules) ? $field->rules : []; - } else { - $type = $field['type']; - $fieldRules = isset($field['rules']) ? $field['rules'] : []; - } + // At this point we know we expect InputObjects, but there might be more then one value + // Since they might have different fields, we have to look at each of them individually + // If we have string keys, we are dealing with the values themselves + if ($this->hasStringKeys($values)) { + foreach ($values as $name => $value) { + $key = "{$parentKey}.{$name}"; - // Unpack until we get to the root type - while ($type instanceof WrappingType) { - if ($type instanceof ListOfType) { - // This lets us skip one level of validation rules - $fieldDepth--; - // Add this to the prefix to allow Laravel validation for arrays - $key .= '.*'; - } + $field = $inputObject->getFields()[$name]; - $type = $type->getWrappedType(); - } + $typeAndKey = $this->unwrapType($field->type, $key); + $key = $typeAndKey['key']; + $type = $typeAndKey['type']; - // Add explicitly set rules if they apply - if (sizeof($fieldRules)) { - $rules[$key] = $this->resolveRules($fieldRules, $resolutionArguments); - } + if (isset($field->rules)) { + $rules[$key] = $field->rules; + } - if ($type instanceof InputObjectType) { - // Recursively call the parent method to get nested rules, passing in the new prefix - $rules = array_merge($rules, $this->getRulesFromArgs($type->getFields(), $fieldDepth, $key, $resolutionArguments)); + if ($type instanceof InputObjectType) { + // Recursively call the parent method to get nested rules, passing in the new prefix + $rules = array_merge($rules, $this->getRulesFromInputObjectType($key, $type, $value)); + } + } + } else { + // Go one level deeper so we deal with actual values + foreach ($values as $nestedValues) { + $rules = array_merge($rules, $this->getRulesFromInputObjectType($parentKey, $inputObject, $nestedValues)); } } return $rules; } + /** + * @param array $array + * @return bool + */ + protected function hasStringKeys(array $array) + { + return count(array_filter(array_keys($array), 'is_string')) > 0; + } + /** * @param $rules * @param $arguments @@ -295,7 +305,7 @@ protected function getRulesFromArgs($fields, $inputValueDepth, $parentKey, $reso */ protected function resolveRules($rules, $arguments) { - // Rules can be defined as closures + // Rules can be defined as closures that are passed the resolution arguments if (is_callable($rules)) { return call_user_func_array($rules, $arguments); } @@ -304,14 +314,21 @@ protected function resolveRules($rules, $arguments) } /** - * Can be overwritten to define rules. + * @param $args + * @param $rules + * @param array $messages * - * @deprecated Will be removed in favour of defining rules together with the args. - * @return array + * @return Validator */ - protected function rules() + protected function getValidator($args, $rules, $messages = []) { - return []; + /** @var Validator $validator */ + $validator = app('validator')->make($args, $rules, $messages); + if (method_exists($this, 'withValidator')) { + $this->withValidator($validator, $args); + } + + return $validator; } /** diff --git a/tests/MutationTest.php b/tests/MutationTest.php index 945cdc85..ad8d5b35 100644 --- a/tests/MutationTest.php +++ b/tests/MutationTest.php @@ -1,7 +1,5 @@ getFieldInstance(); - $rules = $field->getRules(); - - $this->assertInternalType('array', $rules); - - // Those three definitions must have the same result - $this->assertEquals(['email'], $rules['email_seperate_rules']); - $this->assertEquals(['email'], $rules['email_inline_rules']); - $this->assertEquals(['email'], $rules['email_closure_rules']); - - // Apply array validation to values defined as GraphQL-Lists - $this->assertEquals(['email'], $rules['email_list.*']); - $this->assertEquals(['email'], $rules['email_list_of_lists.*.*']); - - // Inferred rules from Input Objects - $this->assertEquals(['alpha'], $rules['input_object.alpha']); - $this->assertEquals(['email'], $rules['input_object.child.email']); - $this->assertEquals(['email'], $rules['input_object.child-list.*.email']); - - // Self-referencing, nested InputObject - $this->assertEquals(['alpha'], $rules['input_object.self.alpha']); - $this->assertEquals(['email'], $rules['input_object.self.child.email']); - $this->assertEquals(['email'], $rules['input_object.self.child-list.*.email']); - - // Go down a few levels - $this->assertEquals(['alpha'], $rules['input_object.self.self.self.self.alpha']); - $this->assertEquals(['email'], $rules['input_object.self.self.self.self.child.email']); - $this->assertEquals(['email'], $rules['input_object.self.self.self.self.child-list.*.email']); - - $this->assertArrayNotHasKey( - 'input_object.self.self.self.self.self.self.self.self.self.self.alpha', - $rules, - 'Validation rules should not be set for such deep nesting.'); + $this->callResolveWithInput([ + 'email_inline_rules' => 'not-an-email' + ]); } /** - * Test resolve throw validation error. - * @expectedException \Folklore\GraphQL\Error\ValidationError + * Validation error messages are correctly constructed and thrown. + * * @test */ - public function testResolveThrowValidationError() + public function testCustomValidationErrorMessages() { - $field = $this->getFieldInstance(); - $attributes = $field->getAttributes(); + try { + $this->callResolveWithInput([ + 'email_inline_rules' => 'not-an-email', + 'input_object' => [ + 'child' => [ + 'email' => 'not-an-email' + ], + ] + ]); + } catch (\Folklore\GraphQL\Error\ValidationError $e) { + $messages = $e->getValidatorMessages(); - $attributes['resolve'](null, [ - 'email_inline_rules' => 'not-an-email' - ], [], null); + // The custom validation error message should override the default + $this->assertEquals('Has to be a valid email.', $messages->first('email_inline_rules')); + $this->assertEquals('Invalid email: not-an-email', $messages->first('input_object.child.email')); + } } /** - * Validation error messages are correctly constructed and thrown. - * * @test */ - public function testValidationErrorMessages() + public function testArrayValidationIsApplied() { - $field = $this->getFieldInstance(); - $attributes = $field->getAttributes(); - try { - $attributes['resolve'](null, [ - 'email_inline_rules' => 'not-an-email', + $this->callResolveWithInput([ 'email_list' => ['not-an-email', 'valid@email.com'], 'email_list_of_lists' => [ ['valid@email.com'], ['not-an-email'], ], + ]); + } catch (\Folklore\GraphQL\Error\ValidationError $e) { + $messages = $e->getValidatorMessages(); + + $messageKeys = $messages->keys(); + $expectedKeys = [ + 'email_list.0', + 'email_list_of_lists.1.0', + ]; + // Sort the arrays before comparison so that order does not matter + sort($expectedKeys); + sort($messageKeys); + // Ensure that validation errors occurred only where necessary + $this->assertEquals($expectedKeys, $messageKeys, 'Not all the right fields were validated.'); + } + } + + /** + * @test + */ + public function testRulesForNestedInputObjects() + { + try { + $this->callResolveWithInput([ 'input_object' => [ 'child' => [ 'email' => 'not-an-email' @@ -91,33 +88,95 @@ public function testValidationErrorMessages() 'self' => [ 'self' => [ 'alpha' => 'Not alphanumeric !"§)' + ], + 'child_list' => [ + ['email' => 'abc'], + ['email' => 'def'] ] ] ] - ], [], null); + ]); } catch (\Folklore\GraphQL\Error\ValidationError $e) { $validator = $e->getValidator(); + $rules = $validator->getRules(); - $this->assertInstanceOf(Validator::class, $validator); + $this->assertEquals(['email'], $rules['input_object.child.email']); + $this->assertEquals(['alpha'], $rules['input_object.self.self.alpha']); + $this->assertEquals(['email'], $rules['input_object.self.child_list.0.email']); + $this->assertEquals(['email'], $rules['input_object.self.child_list.1.email']); + } + } + + /** + * @test + */ + public function testExplicitRulesOverwriteInlineRules() + { + try { + $this->callResolveWithInput([ + 'email_seperate_rules' => 'asdf' + ]); + } catch (\Folklore\GraphQL\Error\ValidationError $e) { + $validator = $e->getValidator(); + $rules = $validator->getRules(); + + $this->assertEquals(['email'], $rules['email_seperate_rules']); + } + } + + /** + * @test + */ + public function testCanValidateArraysThroughSeperateRules() + { + try { + $this->callResolveWithInput([ + 'email_list' => [ + 'invalid', + 'asdf@asdf.de', + 'asdf@asdf.de', + ] + ]); + } catch (\Folklore\GraphQL\Error\ValidationError $e) { + $validator = $e->getValidator(); + $rules = $validator->getRules(); + + $this->assertEquals(['max:2'], $rules['email_list']); + $this->assertEquals(['email'], $rules['email_list.0']); + $this->assertEquals(['email'], $rules['email_list.1']); + $this->assertEquals(['email'], $rules['email_list.2']); - /** @var \Illuminate\Support\MessageBag $messages */ $messages = $e->getValidatorMessages(); - $messageKeys = $messages->keys(); - $expectedKeys = [ - 'email_inline_rules', - 'email_list.0', - 'email_list_of_lists.1.0', - 'input_object.child.email', - 'input_object.self.self.alpha', - ]; - // Ensure that validation errors occurred only where necessary - // Sort the arrays before comparison so that order does not matter - $this->assertEquals(sort($expectedKeys), sort($messageKeys), 'Not all the right fields were validated.'); + $this->assertEquals('The email list may not be greater than 2 characters.', $messages->first('email_list')); + $this->assertEquals('The email_list.0 must be a valid email address.', $messages->first('email_list.0')); + } + } - // The custom validation error message should override the default - $this->assertEquals('Has to be a valid email.', $messages->first('email_inline_rules')); - $this->assertEquals('Invalid email: not-an-email', $messages->first('input_object.child.email')); + /** + * @test + */ + public function testRequiredWithRule() + { + try { + $this->callResolveWithInput([ + 'required' => 'whatever' + ]); + } catch (\Folklore\GraphQL\Error\ValidationError $e) { + $validator = $e->getValidator(); + $rules = $validator->getRules(); + + $this->assertEquals(['required_with:required'], $rules['required_with']); + $messages = $e->getValidatorMessages(); + $this->assertEquals('The required with field is required when required is present.', $messages->first('required_with')); } } + + protected function callResolveWithInput($input) + { + $field = $this->getFieldInstance(); + $attributes = $field->getAttributes(); + + $attributes['resolve'](null, $input, [], new \GraphQL\Type\Definition\ResolveInfo([])); + } } diff --git a/tests/Objects/ExampleMutation.php b/tests/Objects/ExampleMutation.php index 1b6c28fc..f4c785e2 100644 --- a/tests/Objects/ExampleMutation.php +++ b/tests/Objects/ExampleMutation.php @@ -27,9 +27,16 @@ public function args() 'type' => Type::nonNull(Type::string()), ], + 'required_with' => [ + 'name' => 'required_with', + 'type' => Type::string(), + ], + 'email_seperate_rules' => [ 'name' => 'email_seperate_rules', - 'type' => Type::string() + 'type' => Type::string(), + // Should be overwritten by those defined in rules() + 'rules' => ['integer'], ], 'email_inline_rules' => [ @@ -72,10 +79,12 @@ public function resolve($root, $args, $context, ResolveInfo $info) ]; } - protected function rules() + public function rules() { return [ - 'email_seperate_rules' => ['email'] + 'email_seperate_rules' => ['email'], + 'email_list' => ['max:2'], + 'required_with' => ['required_with:required'], ]; } diff --git a/tests/Objects/ExampleParentInputObject.php b/tests/Objects/ExampleParentInputObject.php index 89531b1f..a92698f5 100644 --- a/tests/Objects/ExampleParentInputObject.php +++ b/tests/Objects/ExampleParentInputObject.php @@ -31,8 +31,8 @@ public function fields() 'type' => GraphQL::type('ExampleChildInputObject'), ], - 'child-list' => [ - 'name' => 'child-list', + 'child_list' => [ + 'name' => 'child_list', 'type' => Type::listOf(GraphQL::type('ExampleChildInputObject')), ], From 02d66fd9c653eceb1ddb69671d86602616262a74 Mon Sep 17 00:00:00 2001 From: spawnia Date: Fri, 18 May 2018 10:23:30 +0200 Subject: [PATCH 6/6] Add rules function to stubs for generating Fields --- src/Folklore/GraphQL/Console/stubs/field.stub | 13 +++++++++++++ src/Folklore/GraphQL/Console/stubs/mutation.stub | 13 +++++++++++++ src/Folklore/GraphQL/Console/stubs/query.stub | 13 +++++++++++++ 3 files changed, 39 insertions(+) diff --git a/src/Folklore/GraphQL/Console/stubs/field.stub b/src/Folklore/GraphQL/Console/stubs/field.stub index 22c96f9f..b0c14e0d 100644 --- a/src/Folklore/GraphQL/Console/stubs/field.stub +++ b/src/Folklore/GraphQL/Console/stubs/field.stub @@ -39,6 +39,19 @@ class DummyClass extends Field ]; } + /** + * Overwrite rules at any part in the tree of field arguments. + * + * The rules defined in here take precedence over the rules that are + * defined inline or inferred from nested Input Objects. + * + * @return array + */ + public function rules() + { + return []; + } + /** * Return a result for the field which should match up with its return type. * diff --git a/src/Folklore/GraphQL/Console/stubs/mutation.stub b/src/Folklore/GraphQL/Console/stubs/mutation.stub index 80619327..d3d0d146 100644 --- a/src/Folklore/GraphQL/Console/stubs/mutation.stub +++ b/src/Folklore/GraphQL/Console/stubs/mutation.stub @@ -39,6 +39,19 @@ class DummyClass extends Mutation ]; } + /** + * Overwrite rules at any part in the tree of field arguments. + * + * The rules defined in here take precedence over the rules that are + * defined inline or inferred from nested Input Objects. + * + * @return array + */ + public function rules() + { + return []; + } + /** * Return a result for the field which should match up with its return type. * diff --git a/src/Folklore/GraphQL/Console/stubs/query.stub b/src/Folklore/GraphQL/Console/stubs/query.stub index 0092d20d..95464db5 100644 --- a/src/Folklore/GraphQL/Console/stubs/query.stub +++ b/src/Folklore/GraphQL/Console/stubs/query.stub @@ -39,6 +39,19 @@ class DummyClass extends Query ]; } + /** + * Overwrite rules at any part in the tree of field arguments. + * + * The rules defined in here take precedence over the rules that are + * defined inline or inferred from nested Input Objects. + * + * @return array + */ + public function rules() + { + return []; + } + /** * Return a result for the field which should match up with its return type. *