diff --git a/composer.json b/composer.json index 3bd0054..ea6342d 100644 --- a/composer.json +++ b/composer.json @@ -35,14 +35,12 @@ }, "autoload": { "psr-4": { - "GraphQL\\": "src/" + "GraphQlPhpValidationToolkit\\": "src/" } }, "autoload-dev": { "psr-4": { - "GraphQL\\Tests\\": "tests/", - "GraphQL\\Benchmarks\\": "benchmarks/", - "GraphQL\\Examples\\Blog\\": "examples/01-blog/Blog/" + "GraphQlPhpValidationToolkit\\Tests\\": "tests/" } }, "scripts": { diff --git a/examples/01-basic-scalar-validation/README.md b/examples/01-basic-scalar-validation/README.md index 294a23b..401deb0 100644 --- a/examples/01-basic-scalar-validation/README.md +++ b/examples/01-basic-scalar-validation/README.md @@ -1,37 +1,41 @@ # Simple scalar validation -The simplest possible type of user input validation. Mutation expects an `authorId` arg, and will respond with a nicely formatted error code and message if an author by that id doesn't exist. + +The simplest possible type of user input validation. Mutation expects an `authorId` arg, and will respond with a nicely +formatted error code and message if an author by that id doesn't exist. ### Run locally + ``` php -S localhost:8000 ./index.php ``` ### Install ChromeiQL plug-in for Chrome + 1. Install from [here](https://chrome.google.com/webstore/detail/chromeiql/fkkiamalmpiidkljmicmjfbieiclmeij?hl=en) 2. Enter "http://localhost:8000" in the text field at the top and press the "Set Endpoint" button 3. Be sure to inspect the "Docs" flyout to get familiar with the dynamically-generated types ### Try mutation with valid input + ``` mutation { deleteAuthor(id: 1) { - valid - result + __valid + __result } } ``` ### Try mutation with invalid input + ``` mutation { deleteAuthor(id: 3) { - valid - result - suberrors { - id { - code - msg - } + __valid + __result + id { + __code + __msg } } } diff --git a/examples/01-basic-scalar-validation/index.php b/examples/01-basic-scalar-validation/index.php index 04ad385..44148b6 100644 --- a/examples/01-basic-scalar-validation/index.php +++ b/examples/01-basic-scalar-validation/index.php @@ -5,8 +5,8 @@ use GraphQL\GraphQL; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; -use GraphQL\Type\Definition\ValidatedFieldDefinition; use GraphQL\Type\Schema; +use GraphQlPhpValidationToolkit\Type\UserErrorType\ValidatedFieldDefinition; $authors = [ 1 => [ diff --git a/examples/02-custom-error-codes/README.md b/examples/02-custom-error-codes/README.md index 1051014..c9f3f7c 100644 --- a/examples/02-custom-error-codes/README.md +++ b/examples/02-custom-error-codes/README.md @@ -1,43 +1,45 @@ # Simple scalar validation -If you supply an `errorCodes` property on your `fields` or `args` definitions, then a custom, unique error code type will be created. + +If you supply an `errorCodes` property on your `fields` or `args` definitions, then a custom, unique error code type +will be created. ### Run locally + ``` php -S localhost:8000 ./index.php ``` ### Install ChromeiQL plug-in for Chrome + 1. Install from [here](https://chrome.google.com/webstore/detail/chromeiql/fkkiamalmpiidkljmicmjfbieiclmeij?hl=en) 2. Enter "http://localhost:8000" in the text field at the top and press the "Set Endpoint" button 3. Be sure to inspect the "Docs" flyout to get familiar with the dynamically-generated types ### Try mutation with valid input + ``` mutation { deleteAuthor(id: 1) { - valid - result - suberrors { - id { - code - msg - } + __valid + __result + id { + __code + __msg } } } ``` ### Try mutation with invalid input + ``` mutation { deleteAuthor(id: 3) { - valid - result - suberrors { - id { - code - msg - } + __valid + __result + id { + __code + __msg } } } diff --git a/examples/02-custom-error-codes/index.php b/examples/02-custom-error-codes/index.php index b3b138d..7549b89 100644 --- a/examples/02-custom-error-codes/index.php +++ b/examples/02-custom-error-codes/index.php @@ -6,12 +6,12 @@ use GraphQL\Type\Definition\Description; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; -use GraphQL\Type\Definition\ValidatedFieldDefinition; use GraphQL\Type\Schema; +use GraphQlPhpValidationToolkit\Type\UserErrorType\ValidatedFieldDefinition; -enum AuthorValidation { +enum AuthorValidation +{ #[Description(description: 'Author not found.')] - case UnknownAuthor; case AuthorAlreadyDeleted; } @@ -61,7 +61,7 @@ public function __construct() 'deleteAuthor' => new ValidatedFieldDefinition([ 'name' => 'deleteAuthor', 'typeSetter' => function (Type $type) use ($types) { - if(!isset($types[$type->name])) { + if (!isset($types[$type->name])) { $types[$type->name] = $type; } return $types[$type->name]; diff --git a/examples/03-input-object-validation/README.md b/examples/03-input-object-validation/README.md index d71aa02..efde6f0 100644 --- a/examples/03-input-object-validation/README.md +++ b/examples/03-input-object-validation/README.md @@ -1,19 +1,23 @@ -# Validation of InputObjects (or Objects) - -You can add `validate` properties to the fields of nested Objects or InputObjects (and those fields can themselves be of complex types with their own fields, and so on). This library will sniff them out and recursively build up a result type with a similarly nested structure. +# Validation of InputObjects (or Objects) +You can add `validate` properties to the fields of nested Objects or InputObjects (and those fields can themselves be of +complex types with their own fields, and so on). This library will sniff them out and recursively build up a result type +with a similarly nested structure. ### Run locally + ``` php -S localhost:8000 ./index.php ``` ### Install ChromeiQL plug-in for Chrome + 1. Install from [here](https://chrome.google.com/webstore/detail/chromeiql/fkkiamalmpiidkljmicmjfbieiclmeij?hl=en) 2. Enter "http://localhost:8000" in the text field at the top and press the "Set Endpoint" button 3. Be sure to inspect the "Docs" flyout to get familiar with the dynamically-generated types ### Try mutation with valid input + ``` mutation { updateAuthor( @@ -23,31 +27,28 @@ mutation { age: 47 } ) { - result { + __result { id name } - suberrors { - attributes { - code - msg - suberrors { - age { - code - msg - } - name { - code - msg - } + attributes { + __code + __msg + age { + __code + __msg + } + name { + __code + __msg } - } } } } ``` ### Try mutation with invalid input + ``` mutation { updateAuthor( @@ -57,24 +58,20 @@ mutation { age: -3 } ) { - result { + __result { id name } - suberrors { - attributes { - code - msg - suberrors { - age { - code - msg - } - name { - code - msg - } - } + attributes { + __code + __msg + age { + __code + __msg + } + name { + __code + __msg } } } diff --git a/examples/03-input-object-validation/index.php b/examples/03-input-object-validation/index.php index 6e7ef82..35160ac 100644 --- a/examples/03-input-object-validation/index.php +++ b/examples/03-input-object-validation/index.php @@ -6,19 +6,22 @@ use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; -use GraphQL\Type\Definition\ValidatedFieldDefinition; use GraphQL\Type\Schema; +use GraphQlPhpValidationToolkit\Type\UserErrorType\ValidatedFieldDefinition; -enum NameErrors { +enum NameErrors +{ case NameTooLong; case NameNotUnique; } -enum AgeErrors { +enum AgeErrors +{ case Negative; } -enum AuthorErrors { +enum AuthorErrors +{ case UnknownAuthor; case DeceasedAuthor; } @@ -115,7 +118,7 @@ public function __construct() 'type' => Type::id(), 'errorCodes' => AuthorErrors::class, 'validate' => function (string $authorId) use ($authors) { - if (! isset($authors[$authorId])) { + if (!isset($authors[$authorId])) { return [AuthorValidation::UnknownAuthor, 'We have no record of that author']; } diff --git a/examples/04-list-of-scalar-validation/README.md b/examples/04-list-of-scalar-validation/README.md index 839e9f3..99aae90 100644 --- a/examples/04-list-of-scalar-validation/README.md +++ b/examples/04-list-of-scalar-validation/README.md @@ -1,34 +1,34 @@ -# Validation of InputObjects (or Objects) - -You can add validate lists of things. You can specify a `validate` callback on the `ListOf` field itself, and also specify a `validateItem` callback to be applied to each item in the list. Any errors returned on the list items will each have an `index` property so you will know exactly which items were invalid. +# Validation of InputObjects (or Objects) +You can validate lists of things. You can specify a `validate` callback on the `ListOf` field itself, ... TODO fill this +in. Any errors returned on the list items will each have an `index` property so you will know exactly which items were +invalid. ### Run locally + ``` php -S localhost:8000 ./index.php ``` ### Install ChromeiQL plug-in for Chrome + 1. Install from [here](https://chrome.google.com/webstore/detail/chromeiql/fkkiamalmpiidkljmicmjfbieiclmeij?hl=en) 2. Enter "http://localhost:8000" in the text field at the top and press the "Set Endpoint" button 3. Be sure to inspect the "Docs" flyout to get familiar with the dynamically-generated types - ### Try mutation with valid input + ``` mutation { - savePhoneNumbers( - phoneNumbers: [ - "123-3456", - "867-5309" - ] - ) { - valid - suberrors { - phoneNumbers { - code - msg - path + savePhoneNumbers(phoneNumbers: ["123-3456", "867-5309"]) { + __valid + __code + __msg + phoneNumbers { + items { + __code + __msg + __path } } } @@ -36,20 +36,18 @@ mutation { ``` ### Try mutation with invalid input + ``` mutation { - savePhoneNumbers( - phoneNumbers: [ - "123-3456", - "xxx-3456" - ] - ) { - valid - suberrors { - phoneNumbers { - code - msg - path + savePhoneNumbers(phoneNumbers: ["123-3456", "xxx-5309"]) { + __valid + __code + __msg + phoneNumbers { + items { + __code + __msg + __path } } } diff --git a/examples/04-list-of-scalar-validation/index.php b/examples/04-list-of-scalar-validation/index.php index 959d722..5c4c2fa 100644 --- a/examples/04-list-of-scalar-validation/index.php +++ b/examples/04-list-of-scalar-validation/index.php @@ -5,8 +5,9 @@ use GraphQL\GraphQL; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; -use GraphQL\Type\Definition\ValidatedFieldDefinition; use GraphQL\Type\Schema; +use GraphQlPhpValidationToolkit\Type\UserErrorType\ValidatedFieldDefinition; +use GraphQlPhpValidationToolkit\Type\ValidatedStringType; try { $mutationType = new ObjectType([ @@ -21,16 +22,18 @@ } return 0; + }, 'args' => [ 'phoneNumbers' => [ - 'validate' => function ($phoneNumber) { - $res = preg_match('/^[0-9\-]+$/', $phoneNumber) === 1; - - return ! $res ? [1, 'That does not seem to be a valid phone number'] : 0; - }, 'type' => Type::listOf(Type::string()), - ], + 'items' => [ + 'validate' => function ($phoneNumber) { + $isValid = preg_match('/^[0-9\-]+$/', $phoneNumber) === 1; + return $isValid === 1 ? 0 : 1; + } + ], + ] ], 'resolve' => function ($value, $args) { // PhoneNumberProvider::setPhoneNumbers($args['phoneNumbers']); diff --git a/examples/05-list-of-input-object-validation/README.md b/examples/05-list-of-input-object-validation/README.md index 9cdb7d2..00cc2bf 100644 --- a/examples/05-list-of-input-object-validation/README.md +++ b/examples/05-list-of-input-object-validation/README.md @@ -1,22 +1,26 @@ -# Validation of InputObjects (or Objects) - -You can add validate lists of compound types, such as InputObject. You can specify a `validate` callback on the `ListOf` field to be applied to each item in the list, and if the compound object has any `validate` callbacks on its own fields, they will be called as well. +# Validation of InputObjects (or Objects) +You can add validate lists of compound types, such as InputObject. You can specify a `validate` callback on the `ListOf` +field to be applied to each item in the list, and if the compound object has any `validate` callbacks on its own fields, +they will be called as well. ### Run locally + ``` php -S localhost:8000 ./index.php ``` ### Install ChromeiQL plug-in for Chrome + 1. Install from [here](https://chrome.google.com/webstore/detail/chromeiql/fkkiamalmpiidkljmicmjfbieiclmeij?hl=en) 2. Enter "http://localhost:8000" in the text field at the top and press the "Set Endpoint" button 3. Be sure to inspect the "Docs" flyout to get familiar with the dynamically-generated types ### Try mutation with valid input + ``` mutation { - updateAuthors( + updateAuthors( authors: [{ id:1, firstName: "Stephen", @@ -27,27 +31,24 @@ mutation { lastName: "Clarke" }] ) { - valid - suberrors { - authors { - path - suberrors { - id { - code - msg - } - firstName { - code - msg - } - lastName { - code - msg - } + __valid + authors { + items { + id { + __code + __msg + } + firstName { + __code + __msg + } + lastName { + __code + __msg } } } - result { + __result { firstName lastName } @@ -56,11 +57,12 @@ mutation { ``` ### Try mutation with invalid input + ``` mutation { updateAuthors( authors: [{ - id:1, + id:-1, firstName: "Richard", lastName: "Matheson" },{ @@ -69,27 +71,25 @@ mutation { lastName: "Jones" }] ) { - valid - suberrors { - authors { - path - suberrors { - id { - code - msg - } - firstName { - code - msg - } - lastName { - code - msg - } + __valid + authors { + items { + __path + id { + __code + __msg + } + firstName { + __code + __msg + } + lastName { + __code + __msg } } } - result { + __result { firstName lastName } diff --git a/examples/05-list-of-input-object-validation/index.php b/examples/05-list-of-input-object-validation/index.php index 04e2e0f..977f564 100644 --- a/examples/05-list-of-input-object-validation/index.php +++ b/examples/05-list-of-input-object-validation/index.php @@ -6,7 +6,7 @@ use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; -use GraphQL\Type\Definition\ValidatedFieldDefinition; +use GraphQlPhpValidationToolkit\Type\UserErrorType\ValidatedFieldDefinition; use GraphQL\Type\Schema; class AuthorType extends ObjectType @@ -63,6 +63,8 @@ public function __construct() 'authors' => [ 'type' => Type::listOf(new InputObjectType([ 'name' => 'AuthorInput', + 'validate' => function ($args) { + }, 'fields' => [ 'id' => [ 'type' => Type::nonNull(Type::id()), diff --git a/src/Exception/NoValidatationFoundException.php b/src/Exception/NoValidatationFoundException.php new file mode 100644 index 0000000..2a7fdc9 --- /dev/null +++ b/src/Exception/NoValidatationFoundException.php @@ -0,0 +1,10 @@ +|null, - * fields?: array, - * validate?: null|callable(mixed $value): mixed, - * isRoot?: bool, - * typeSetter?: callable|null, - * } - * @phpstan-type Path array - * @phpstan-import-type ObjectConfig from ObjectType - * @phpstan-import-type ValidatedFieldConfig from ValidatedFieldDefinition - * @phpstan-import-type UnnamedFieldDefinitionConfig from FieldDefinition - */ -final class UserErrorsType extends ObjectType -{ - public const SUBERRORS_NAME = 'suberrors'; - protected const CODE_NAME = 'code'; - protected const MESSAGE_NAME = 'msg'; - - /** - * @phpstan-param UserErrorsConfig $config - * @phpstan-param Path $path - */ - public function __construct(array $config, array $path, bool $isParentList = false) - { - $finalFields = $config['fields'] ?? []; - $this->_addErrorCodes($config, $finalFields, $path); - - $type = $this->_resolveType($config['type']); - if ($type instanceof InputObjectType) { - $this->_buildInputObjectType($type, $config, $path, $finalFields, $isParentList); - } - - if ($isParentList) { - $this->_addPathField($finalFields); - } - - $pathEnd = end($path); - assert($pathEnd != false); - parent::__construct([ - 'name' => $this->_nameFromPath(\array_merge($path)) . \ucfirst('error'), - 'description' => 'User errors for ' . \ucfirst((string)$pathEnd), - 'fields' => $finalFields, - 'type' => $config['type'] - ]); - } - - /** - * @param Type|callable():Type $type - */ - protected function _resolveType(mixed $type): Type - { - if (\is_callable($type)) { - $type = $type(); - } - - if ($type instanceof WrappingType) { - $type = $type->getInnermostType(); - } - - return $type; - } - - /** - * @phpstan-param UserErrorsConfig $config - */ - public static function needSuberrors(array $config, bool $isParentList): bool - { - return ! empty($config['validate']) || ! empty($config['isRoot']) || $isParentList; - } - - /** - * @phpstan-param UserErrorsConfig $config - * @phpstan-param Path $path - * @return array - */ - protected function _buildInputObjectFields(InputObjectType $type, array $config, array $path): array - { - $fields = []; - foreach ($type->getFields() as $key => $field) { - $fieldConfig = $field->config; - $fieldType = $this->_resolveType($field->config['type']); - $newType = self::create( - [ - 'validate' => $fieldConfig['validate'] ?? null, - 'errorCodes' => $fieldConfig['errorCodes'] ?? null, - 'type' => $fieldType, - 'fields' => [], - 'typeSetter' => $config['typeSetter'] ?? null, - ], - \array_merge($path, [$key]), - $field->getType() instanceof ListOfType - ); - if (! empty($newType)) { - $fields[$key] = [ - 'description' => 'Error for ' . $key, - 'type' => $field->getType() instanceof ListOfType ? Type::listOf($newType) : $newType, - 'resolve' => static function ($value) use ($key) { - return $value[$key] ?? null; - }, - ]; - } - } - - return $fields; - } - - /** - * @param UserErrorsConfig $config - * @param Path $path - * @param array $finalFields - * @param bool $isParentList - * @return void - */ - protected function _buildInputObjectType(InputObjectType $type, array $config, array $path, array &$finalFields, bool $isParentList) - { - $createSubErrors = UserErrorsType::needSuberrors($config, $isParentList); - $fields = $this->_buildInputObjectFields($type, $config, $path); - if ($createSubErrors && \count($fields) > 0) { - /** - * suberrors property. - */ - $finalFields[static::SUBERRORS_NAME] = [ - 'type' => $this->_set(new ObjectType([ - 'name' => $this->_nameFromPath(\array_merge($path, ['fieldErrors'])), - 'description' => 'User Error', - 'fields' => $fields, - ]), $config), - 'description' => 'Validation errors for ' . \ucfirst((string)$path[\count($path) - 1]), - 'resolve' => static function (array $value) { - return $value[static::SUBERRORS_NAME] ?? null; - }, - ]; - } else { - $finalFields += $fields; - } - } - - /** - * @phpstan-param UserErrorsConfig $config - * @phpstan-param array $finalFields - * @phpstan-param Path $path - */ - protected function _addErrorCodes($config, &$finalFields, array $path): void - { - if (isset($config['errorCodes'])) { - if (! isset($config['validate'])) { - throw new \Exception('If you specify errorCodes, you must also provide a validate callback'); - } - - $type = new PhpEnumType($config['errorCodes']); - $type->description = "Error code"; - - if(!isset($config['typeSetter'])) { - $type->name = $this->_nameFromPath(\array_merge($path)) . 'ErrorCode'; - } - else { - $type->name = $type->name . 'ErrorCode'; - } - $type->description = "Error code"; - - /** code property */ - $finalFields[static::CODE_NAME] = [ - 'type' => $this->_set($type, $config), - 'description' => 'An error code', - 'resolve' => static function ($value) { - return $value['error'][0] ?? null; - }, - ]; - - /** - * msg property. - */ - $finalFields[static::MESSAGE_NAME] = [ - 'type' => Type::string(), - 'description' => 'A natural language description of the issue', - 'resolve' => static function ($value) { - return $value['error'][1] ?? null; - }, - ]; - } - } - - /** - * @phpstan-param array $finalFields - */ - protected function _addPathField(array &$finalFields): void - { - if (! empty($finalFields['code']) || ! empty($finalFields['suberrors'])) { - $finalFields['path'] = [ - 'type' => Type::listOf(Type::int()), - 'description' => 'A path describing this item\'s location in the nested array', - 'resolve' => static function ($value) { - return $value['path']; - }, - ]; - } - } - - /** - * @param mixed[] $config - * - * @return Type - */ - protected function _set(Type $type, array $config) - { - if (\is_callable($config['typeSetter'] ?? null)) { - return $config['typeSetter']($type); - } - - return $type; - } - - /** - * @param UserErrorsConfig $config - * @phpstan-param Path $path - * - * @return static|null - */ - public static function create(array $config, array $path, bool $isParentList = false, string $name = ''): ?self - { - if (\is_callable($config['validate'] ?? null)) { - $config['fields'][static::CODE_NAME] = $config['fields'][static::CODE_NAME] ?? static::_generateIntCodeType(); - $config['fields'][static::MESSAGE_NAME] = $config['fields'][static::MESSAGE_NAME] ?? static::_generateMessageType(); - } - - $userErrorType = new UserErrorsType($config, $path, $isParentList); - if (count($userErrorType->getFields()) > 0) { - $userErrorType->name = ! empty($name) ? $name : $userErrorType->name; - if (\is_callable($config['typeSetter'] ?? null)) { - $config['typeSetter']($userErrorType); - } - - return $userErrorType; - } - - if (\count($path) == 1) { - throw new \Exception("You must specify at least one 'validate' callback somewhere"); - } - - return null; - } - - /** - * @return UnnamedFieldDefinitionConfig - */ - protected static function _generateIntCodeType(): array - { - return [ - 'type' => Type::int(), - 'description' => 'A numeric error code. 0 on success, non-zero on failure.', - 'resolve' => static function ($value) { - $error = $value['error'] ?? null; - switch (\gettype($error)) { - case 'integer': - return $error; - } - - return $error[0] ?? null; - }, - ]; - } - - /** - * @return UnnamedFieldDefinitionConfig - */ - protected static function _generateMessageType(): array - { - return [ - 'type' => Type::string(), - 'description' => 'An error message.', - 'resolve' => static function ($value) { - $error = $value['error'] ?? null; - switch (\gettype($error)) { - case 'integer': - return ''; - } - - return $error[1] ?? null; - }, - ]; - } - - /** - * @param Path $path - */ - protected function _nameFromPath(array $path): string - { - return \implode('_', \array_map(static fn ($node) => ucfirst((string)$node), $path)); - } -} diff --git a/src/Type/Definition/ValidatedFieldDefinition.php b/src/Type/Definition/ValidatedFieldDefinition.php deleted file mode 100644 index 129f3f7..0000000 --- a/src/Type/Definition/ValidatedFieldDefinition.php +++ /dev/null @@ -1,258 +0,0 @@ -|callable(): bool|array, - * resultName?: string, - * args: array, - * resolve?: FieldResolver|null, - * validate?: callable(mixed $value): mixed, - * errorCodes?: class-string<\UnitEnum>|null, - * type: Type - * } - */ -class ValidatedFieldDefinition extends FieldDefinition -{ - /** @var callable */ - protected $typeSetter; - - protected string $validFieldName; - - protected string $resultFieldName; - - /** - * @phpstan-param ValidatedFieldConfig $config - */ - public function __construct(array $config) - { - $args = $config['args']; - $name = $config['name'] ?? \lcfirst($this->tryInferName()); - - $this->validFieldName = $config['validName'] ?? 'valid'; - $this->resultFieldName = $config['resultName'] ?? 'result'; - - parent::__construct([ - 'type' => fn () => static::_createUserErrorsType($name, $args, $config), - 'args' => $args, - 'name' => $name, - 'resolve' => function ($value, $args1, $context, $info) use ($config, $args) { - // validate inputs - $config['type'] = new InputObjectType([ - 'name' => '', - 'fields' => $args, - ]); - $config['isRoot'] = true; - - $errors = $this->_validate($config, $args1); - $result = $errors; - $result[$this->validFieldName] = empty($errors); - - if (! empty($result['valid'])) { - $result[$this->resultFieldName] = $config['resolve']($value, $args1, $context, $info); - } - - return $result; - }, - ]); - } - - /** - * @phpstan-param array $args - * @phpstan-param ValidatedFieldConfig $config - */ - protected function _createUserErrorsType(string $name, array $args, array $config): UserErrorsType - { - return UserErrorsType::create([ - 'errorCodes' => $config['errorCodes'] ?? null, - 'isRoot' => true, - 'fields' => [ - $this->resultFieldName => [ - 'type' => $config['type'], - 'description' => 'The payload, if any', - 'resolve' => static function ($value) { - return $value['result'] ?? null; - }, - ], - $this->validFieldName => [ - 'type' => Type::nonNull(Type::boolean()), - 'description' => 'Whether all validation passed. True for yes, false for no.', - 'resolve' => static function ($value) { - return $value['valid']; - }, - ], - ], - 'validate' => $config['validate'] ?? null, - 'type' => new InputObjectType([ - 'fields' => $args, - 'name' => '', - ]), - 'typeSetter' => $config['typeSetter'] ?? null, - ], [$name], false, \ucfirst($name) . 'Result'); - } - - /** - * @param mixed[] $arg - * @param mixed $value - * - * @return mixed[] - */ - protected function _validate(array $arg, $value, bool $isParentList = false): array - { - $res = []; - - if (\is_callable($arg['type'])) { - $arg['type'] = $arg['type'](); - } - - $type = $arg['type']; - switch ($type) { - case $type instanceof ListOfType: - $this->_validateListOfType($arg, $value, $res); - break; - - case $type instanceof NonNull: - $arg['type'] = $type->getWrappedType(); - $res = $this->_validate($arg, $value); - break; - - case $type instanceof InputObjectType: - $this->_validateInputObject($arg, $value, $res, $isParentList); - break; - - default: - if (\is_callable($arg['validate'] ?? null)) { - $res['error'] = $arg['validate']($value) ?? []; - } - } - - return \array_filter($res); - } - - /** - * @param array $config - * @param mixed[] $value - * @param array $res - * @param Array $path - */ - protected function _validateListOfType(array $config, array $value, array &$res, array $path=[0]): void - { - $validate = $config['validate'] ?? null; - $wrappedType = $config['type']->getWrappedType(); - foreach ($value as $idx => $subValue) { - $path[\count($path) - 1] = $idx; - if ($wrappedType instanceof ListOfType) { - $newPath = $path; - $newPath[] = 0; - $this->_validateListOfType(["type"=>$wrappedType, "validate" => $validate], $subValue, $res, $newPath ); - } else { - $err = $validate != null ? $validate($subValue): 0; - - if (empty($err)) { - $wrappedType = $config['type']->getInnermostType(); - $err = $this->_validate([ - 'type' => $wrappedType, - ], $subValue, $config['type'] instanceof ListOfType); - } - - if ($err) { - if (isset($err['suberrors'])) { - $err = $err; - } else { - $err = [ - 'error' => $err, - ]; - } - $err['path'] = $path; - $res[] = $err; - } - } - } - } - - /** - * @phpstan-param ValidatedFieldConfig $arg - * @param array $res - */ - protected function _validateInputObject(mixed $arg, mixed $value, array &$res, bool $isParentList): void - { - /** - * @phpstan-var InputObjectType - */ - $type = $arg['type']; - if (isset($arg['validate'])) { - $err = $arg['validate']($value) ?? []; - $res['error'] = $err; - } - - $this->_validateInputObjectFields($type, $arg, $value, $res, $isParentList); - } - - /** - * @phpstan-param InputObjectType $type - * @phpstan-param ValidatedFieldConfig $objectConfig - * @param array $res - */ - protected function _validateInputObjectFields(InputObjectType $type, array $objectConfig, mixed $value, array &$res, bool $isParentList = false): void - { - $createSubErrors = UserErrorsType::needSuberrors($objectConfig, $isParentList); - - $fields = $type->getFields(); - foreach ($fields as $key => $field) { - $error = null; - $config = $field->config; - - $isKeyPresent = array_key_exists($key, $value); - $isRequired = $config['required'] ?? false; - if(is_callable($isRequired)) { - $isRequired = $isRequired(); - } - if($isRequired && !isset($value[$key])) { - if ($isRequired === true) { - $error = ['error' => [1, "$key is required"]]; - } - else if (is_array($isRequired)) { - $error = ['error' => $isRequired]; - } - } - else if ($isKeyPresent) { - $error = $this->_validate($config, $value[$key] ?? null); - } - - if (!empty($error)) { - if ($createSubErrors) { - $res[UserErrorsType::SUBERRORS_NAME][$key] = $error; - } else { - $res[$key] = $error; - } - } - } - } - - /** - * @throws \ReflectionException - * - * @return mixed|string|string[]|null - */ - protected function tryInferName() - { - // If class is extended - infer name from className - // QueryType -> Type - // SomeOtherType -> SomeOther - $tmp = new \ReflectionClass($this); - $name = $tmp->getShortName(); - - return \preg_replace('~Type$~', '', $name); - } -} diff --git a/src/Type/UserErrorType/ErrorType.php b/src/Type/UserErrorType/ErrorType.php new file mode 100644 index 0000000..a34c0d2 --- /dev/null +++ b/src/Type/UserErrorType/ErrorType.php @@ -0,0 +1,271 @@ +|null, + * fields?: array, + * validate?: null|callable(mixed $value): mixed, + * isRoot?: bool, + * typeSetter?: callable|null, + * } + * @phpstan-type Path array + * @phpstan-import-type ObjectConfig from ObjectType + * @phpstan-import-type ValidatedFieldConfig from ValidatedFieldDefinition + * @phpstan-import-type UnnamedFieldDefinitionConfig from FieldDefinition + * @phpstan-import-type FieldDefinitionConfig from FieldDefinition + */ +abstract class ErrorType extends ObjectType +{ + protected const CODE_NAME = '__code'; + protected const MESSAGE_NAME = '__msg'; + + /** + * @param ValidatedFieldConfig $arg + * @param mixed $value + * @param array $res ; + */ + + abstract protected function _validate(array $arg, mixed $value, array &$res): void; + + + /** + * @phpstan-param UserErrorsConfig $config + * @phpstan-param Path $path + */ + protected function __construct(array $config, array $path) + { + $fields = $config['fields'] ?? []; + $this->_addCodeAndMessageFields($config, $fields, $path); + + $pathEnd = end($path); + assert($pathEnd != false); + + parent::__construct([ + 'name' => $this->_nameFromPath($path) . 'Error', + 'description' => 'User errors for ' . \ucfirst((string)$pathEnd), + 'fields' => $fields, + 'typeSetter' => $config['typeSetter'] ?? null, + ]); + } + + /** + * Factory method to create the appropriate type (InputObjectType, ListOfType, NonNull, or scalar). + * + * @phpstan-param UserErrorsConfig $config + * @phpstan-param Path $path + */ + public static function create(array $config, array $path): self + { + $resolvedType = self::_resolveType($config['type']); + + if ($resolvedType instanceof InputObjectType) { + $type = new InputObjectErrorType($config, $path); + } else if ($resolvedType instanceof ListOfType) { + $type = new ListOfErrorType($config, $path); + } else if ($resolvedType instanceof NonNull) { + $config['type'] = static::_resolveType($config['type'], true); + $type = static::create($config, $path); + } else if ($resolvedType instanceof StringType) { + $type = new StringErrorType($config, $path); + } else if ($resolvedType instanceof ScalarType) { + $type = new ScalarErrorType($config, $path); + } else { + throw new \Exception("Unknown type"); + } + return static::_set($type, $config); + } + + static protected function empty(mixed $value): bool + { + return !isset($value); + } + + + /** + * @param ValidatedFieldConfig $config + * @param mixed $value + * + * @return mixed[] + */ + public function validate(array $config, $value): array + { + $res = []; + + + if (static::empty($value) && static::isRequired($config['required'] ?? false)) { + if (is_array($config['required'])) { + $validationResult = static::_formatValidationResult($config['required']) + [ + static::CODE_NAME => 1, + static::MESSAGE_NAME => $config['name'] . " is required" + ]; + } else { + $validationResult = [static::CODE_NAME => 1, static::MESSAGE_NAME => $config['name'] . " is required"]; + } + return $validationResult; + } + + if (\is_callable($config['validate'] ?? null)) { + $result = static::_formatValidationResult($config['validate']($value)); + + if (isset($result) && $result[static::CODE_NAME] !== 0) { + $res = $result; + } + } + + if (\is_callable($config['type'])) { + $config['type'] = $config['type'](); + } + + $this->_validate($config, $value, $res); + return $res; + } + + /** + * @param bool|array{int|\UnitEnum, string} $requiredValue + * @return bool + */ + static function isRequired($requiredValue): bool + { + if (is_callable($requiredValue)) { + $requiredValue = $requiredValue(); + } + + if (is_bool($requiredValue)) { + return $requiredValue; + } + + return $requiredValue[0] !== 0; + } + + /** + * @param int|array{0: int|\UnitEnum, 1: string} $result + * @return array{0: int|\UnitEnum, 1: string} + * @throws \Exception + */ + protected static function _formatValidationResult(mixed $result): ?array + { + if (is_array($result) && count($result) === 2) { + [$code, $msg] = $result; + } elseif (is_int($result) || $result instanceof \UnitEnum) { + $code = $result; + $msg = ''; // Set a default message or leave as null + } else { + throw new \Exception("Invalid response from the validate callback"); + } + + if ($code === 0) { + return null; + } + + return [static::CODE_NAME => $code, static::MESSAGE_NAME => $msg]; + } + + protected static function isScalarType(Type $type): bool + { + return $type instanceof ScalarType; + } + + static protected function _resolveType(Type|callable $type, bool $resolveWrapped = false): Type + { + if (\is_callable($type)) { + $type = $type(); + } + + if ($resolveWrapped && $type instanceof WrappingType) { + $type = $type->getWrappedType(); + } + + return $type; + } + + /** + * @template T of Type + * @param T $type + * @param UserErrorsConfig $config + * @return T + */ + static protected function _set(Type $type, array $config): Type + { + if (\is_callable($config['typeSetter'] ?? null)) { + return $config['typeSetter']($type); + } + + return $type; + } + + /** + * @param UserErrorsConfig $config + * @param array $fields + * @param Path $path + * @throws \Exception + */ + protected function _addCodeAndMessageFields(array $config, array &$fields, array $path): void + { + if (isset($config['validate']) || !empty($config['required'])) { + if (isset($config['errorCodes'])) { + // error code. By default, this is an int, but if the user supplies the optional `errorCodes` + // enum property, then it takes that type + + if (!isset($config['validate']) && empty($config['required'])) { + throw new \Exception('If you specify errorCodes, you must also provide a \'validate\' callback, or mark the field as \'required\''); + } + $type = new PhpEnumType($config['errorCodes']); + if (!isset($config['typeSetter'])) { + $type->name = $this->_nameFromPath(\array_merge($path, [$type->name])); + } + + $fields[static::CODE_NAME] = [ + 'type' => static::_set($type, $config), + 'description' => 'An enumerated error code.', + ]; + } else { + $fields[static::CODE_NAME] = [ + 'type' => Type::int(), + 'description' => 'A numeric error code. 0 on success, non-zero on failure.', + ]; + } + + $fields[static::CODE_NAME]['resolve'] = static function ($error) { + return $error[static::CODE_NAME] ?? 0; + }; + + $fields[static::MESSAGE_NAME] = [ + 'type' => Type::string(), + 'description' => 'An error message.', + 'resolve' => static function ($error) { + return $error[static::MESSAGE_NAME] ?? ''; + }, + ]; + } else { + if (isset($config['errorCodes'])) { + if (!isset($config['validate'])) { + throw new \Exception('If you specify errorCodes, you must also provide a validate callback'); + } + } + + } + } + + /** + * @param Path $path + */ + protected function _nameFromPath(array $path): string + { + return implode('_', array_map('ucfirst', $path)); + } +} diff --git a/src/Type/UserErrorType/InputObjectErrorType.php b/src/Type/UserErrorType/InputObjectErrorType.php new file mode 100644 index 0000000..faece71 --- /dev/null +++ b/src/Type/UserErrorType/InputObjectErrorType.php @@ -0,0 +1,91 @@ +getErrorFields($config, $path); + $this->config['fields'] = array_merge($this->config['fields'], $errorFields); + } catch (NoValidatationFoundException $e) { + if (empty($config['validate'])) { + throw new NoValidatationFoundException($e); + } + } + } + + protected function _validate(array $arg, mixed $value, array &$res): void + { + /** + * @phpstan-var InputObjectErrorType + */ + $type = $arg['type']; + + $fields = $type->getFields(); + foreach ($fields as $key => $field) { + $config = $field->config; + $fieldErrorType = $this->config['fields'][$key]['type'] ?? null; + + // Handle validation logic for present keys + $validationResult = $fieldErrorType->validate($config, $value[$key] ?? null) + [static::CODE_NAME => 0, static::MESSAGE_NAME => ""]; + + if ($validationResult[static::CODE_NAME] !== 0) { + // Populate result array + $res[static::CODE_NAME] = 1; // not exposed for query, just needed so it doesn't get filtered higher-up in the tree + $res[$key] = $validationResult; + } + } + } + + /** + * @param UserErrorsConfig $config + * @param Path $path + * @return array + * @throws NoValidatationFoundException + */ + protected function getErrorFields($config, array $path): array + { + $type = $config['type']; + assert($type instanceof InputObjectType); + $fields = []; + foreach ($type->getFields() as $key => $field) { + $fieldConfig = $field->config; + try { + $newType = self::create(array_merge($fieldConfig, ['type' => $field->getType(), 'typeSetter' => $config['typeSetter'] ?? null]), array_merge($path, [$key])); + } catch (NoValidatationFoundException $e) { + // continue. we'll finish building all fields, and throw our own error at the end if we don't wind up with anything. + continue; + } + + $fields[$key] = [ + 'description' => 'Error for ' . $key, + 'type' => $newType, + ]; + } + + if (empty($fields) && !isset($this->config['validate'])) { + throw new NoValidatationFoundException(); + } + + return $fields; + } +} diff --git a/src/Type/UserErrorType/ListOfErrorType.php b/src/Type/UserErrorType/ListOfErrorType.php new file mode 100644 index 0000000..9834da2 --- /dev/null +++ b/src/Type/UserErrorType/ListOfErrorType.php @@ -0,0 +1,104 @@ +getInnermostType(); + try { + if (static::isScalarType($type)) { + $validate = $config[static::ITEMS_NAME]['validate'] ?? null; + $errorCodes = $config[static::ITEMS_NAME]['errorCodes'] ?? null; + } else { + if (isset($config[static::ITEMS_NAME])) { + throw new \Exception("'items' is only supported for scalar types"); + } + + $validate = $type->config['validate'] ?? null; + $errorCodes = $type->config['errorCodes'] ?? null; + } + + $errorType = static::create([ + 'type' => $type, + 'validate' => $validate, + 'errorCodes' => $errorCodes, + 'fields' => [ + static::PATH_NAME => [ + 'type' => Type::listOf(Type::int()), + 'description' => 'A path describing this item\'s location in the nested array', + 'resolve' => static function ($value) { + return $value[static::PATH_NAME]; + }, + ] + ], + ], [$this->name, $type->name]); + + $this->config['fields'][static::ITEMS_NAME] = [ + 'type' => Type::listOf($errorType), + 'description' => 'Validation errors for each ' . $type->name() . ' in the list', + 'resolve' => static function ($value) { + return $value[static::ITEMS_NAME] ?? []; + }, + ]; + } catch (NoValidatationFoundException $e) { + if (empty($config['required']) && !isset($config['validate']) && !isset($config[static::ITEMS_NAME]['validate'])) { + throw $e; + } + } + } + + static protected function empty(mixed $value): bool + { + return parent::empty($value) || count($value) === 0; + } + + protected function _validate(array $arg, mixed $value, array &$res): void + { + $this->_validateListOfType($arg, $value, $res, [0]); + } + + /** + * @param array $config + * @param mixed[] $value + * @param array $res + * @param Array $path + */ + protected function _validateListOfType(array $config, array $value, array &$res, array $path): void + { + $validate = $config['items']['validate'] ?? null; + $wrappedType = $config['type']->getWrappedType(); + $wrappedErrorType = $this->config['fields'][static::ITEMS_NAME]['type']->getWrappedType(); + foreach ($value as $idx => $subValue) { + $path[\count($path) - 1] = $idx; + if ($wrappedType instanceof ListOfErrorType) { + $newPath = $path; + $newPath[] = 0; + $this->_validateListOfType(["type" => $wrappedType, "validate" => $validate], $subValue, $res, $newPath); + } else { + $err = $wrappedErrorType->validate([ + 'type' => $wrappedType + ], $subValue); +// $err = static::_formatValidationResult($validate ? $validate($subValue) : 0); + if ($err) { + $err[static::PATH_NAME] = $path; + $res[static::ITEMS_NAME] ??= []; + $res[static::ITEMS_NAME][] = $err; + $res[static::CODE_NAME] = 1; // this doesn't get exposed to queries anywhere, but we need it to flag that an error happened, so it doesn't get filtered out + } + } + } + } +} \ No newline at end of file diff --git a/src/Type/UserErrorType/NonNullErrorType.php b/src/Type/UserErrorType/NonNullErrorType.php new file mode 100644 index 0000000..1940433 --- /dev/null +++ b/src/Type/UserErrorType/NonNullErrorType.php @@ -0,0 +1,16 @@ +|callable(): bool|array, + * resultName?: string, + * args: array, + * resolve?: FieldResolver|null, + * validate?: callable(mixed $value): mixed, + * errorCodes?: class-string<\UnitEnum>|null, + * type: Type + * } + */ +class ValidatedFieldDefinition extends FieldDefinition +{ + /** @var callable */ + protected $typeSetter; + + protected ErrorType $userErrorsType; + + protected string $validFieldName; + + protected string $resultFieldName; + + /** + * @phpstan-param ValidatedFieldConfig $config + */ + public function __construct(array $config) + { + $args = $config['args']; + $name = $config['name'] ?? \lcfirst($this->tryInferName()); + + $this->validFieldName = $config['validName'] ?? '__valid'; + $this->resultFieldName = $config['resultName'] ?? '__result'; + + + parent::__construct([ + 'type' => fn() => $this->userErrorsType = static::_createUserErrorsType($name, $args, $config), + 'args' => $args, + 'name' => $name, + 'resolve' => function ($value, $args1, $context, $info) use ($config, $args) { + // validate inputs + $config['type'] = new InputObjectType([ + 'name' => '', + 'fields' => $args, + ]); + $config['isRoot'] = true; + + $result = $errors = $this->userErrorsType->validate($config, $args1); + $result[$this->validFieldName] = empty($errors); + + if (!empty($result['valid'])) { + $result[$this->resultFieldName] = $config['resolve']($value, $args1, $context, $info); + } + + return $result; + }, + ]); + } + + /** + * @phpstan-param array $args + * @phpstan-param ValidatedFieldConfig $config + */ + protected function _createUserErrorsType(string $name, array $args, array $config): ErrorType + { + $userErrorType = ErrorType::create([ + 'errorCodes' => $config['errorCodes'] ?? null, + 'isRoot' => true, + 'fields' => [ + $this->resultFieldName => [ + 'type' => $config['type'], + 'description' => 'The payload, if any', + 'resolve' => static function ($value) { + return $value['result'] ?? null; + }, + ], + $this->validFieldName => [ + 'type' => Type::nonNull(Type::boolean()), + 'description' => 'Whether all validation passed. True for yes, false for no.', + 'resolve' => function ($value) { + return $value[$this->validFieldName]; + }, + ], + ], + 'validate' => $config['validate'] ?? null, + 'type' => new InputObjectType([ + 'fields' => $args, + 'name' => '', + ]), + 'typeSetter' => $config['typeSetter'] ?? null, + ], [$name]); + + $userErrorType->name = \ucfirst($name) . 'Result'; + return $userErrorType; + } + + /** + * @return mixed|string|string[]|null + * @throws \ReflectionException + * + */ + protected function tryInferName() + { + // If class is extended - infer name from className + // QueryType -> Type + // SomeOtherType -> SomeOther + $tmp = new \ReflectionClass($this); + $name = $tmp->getShortName(); + + return \preg_replace('~Type$~', '', $name); + } +} diff --git a/src/Type/ValidatedStringType.php b/src/Type/ValidatedStringType.php new file mode 100644 index 0000000..9e6bf2c --- /dev/null +++ b/src/Type/ValidatedStringType.php @@ -0,0 +1,12 @@ +expectExceptionMessage("You must provide at least one 'validate' callback or mark at least one field as 'required'."); + + ErrorType::create([ + 'type' => Type::id(), + ], ['upsertSku']); + } + + public function testIdWithValidationDoesNotThrow(): void + { + $this->expectNotToPerformAssertions(); + + ErrorType::create([ + 'type' => Type::id(), + 'validate' => static fn() => null + ], ['upsertSku']); + + } + + public function testStringThrows(): void + { + $this->expectExceptionMessage("You must provide at least one 'validate' callback or mark at least one field as 'required'."); + + ErrorType::create([ + 'type' => Type::string(), + ], ['upsertSku']); + } + + public function testStringWithValidationDoesNotThrow(): void + { + $this->expectNotToPerformAssertions(); + ErrorType::create([ + 'type' => Type::string(), + 'validate' => static fn() => null + ], ['upsertSku']); + } + + public function testIntThrows(): void + { + $this->expectExceptionMessage("You must provide at least one 'validate' callback or mark at least one field as 'required'."); + + ErrorType::create([ + 'type' => Type::int(), + ], ['upsertSku']); + } + + public function testIntWithValidationDoesNotThrow(): void + { + $this->expectNotToPerformAssertions(); + ErrorType::create([ + 'type' => Type::int(), + 'validate' => static fn() => null + ], ['upsertSku']); + } + + public function testBooleanThrows(): void + { + $this->expectExceptionMessage("You must provide at least one 'validate' callback or mark at least one field as 'required'."); + + ErrorType::create([ + 'type' => Type::boolean(), + ], ['upsertSku']); + } + + public function testBooleanWithValidationDoesNotThrow(): void + { + $this->expectNotToPerformAssertions(); + ErrorType::create([ + 'type' => Type::boolean(), + 'validate' => static fn() => null + ], ['upsertSku']); + } + + public function testFloatThrows(): void + { + $this->expectExceptionMessage("You must provide at least one 'validate' callback or mark at least one field as 'required'."); + + ErrorType::create([ + 'type' => Type::float(), + ], ['upsertSku']); + } + + public function testFloatWithValidationDoesNotThrow(): void + { + $this->expectNotToPerformAssertions(); + ErrorType::create([ + 'type' => Type::float(), + 'validate' => static fn() => null + ], ['upsertSku']); + } + + public function testInputObjectThrows(): void + { + $this->expectExceptionMessage("You must provide at least one 'validate' callback or mark at least one field as 'required'."); + ErrorType::create([ + 'type' => new InputObjectType([ + 'name' => 'updateBook', + 'fields' => [ + 'authorId' => [ + 'type' => Type::id(), + ], + 'publisherId' => [ + 'type' => Type::string(), + ] + ], + ]), + ], ['upsertSku']); + } + + public function testInputObjectWithValidationDoesNotThrow(): void + { + $this->expectNotToPerformAssertions(); + ErrorType::create([ + 'type' => new InputObjectType([ + 'name' => 'updateBook', + 'fields' => [ + 'authorId' => [ + 'type' => Type::id(), + ], + 'publisherId' => [ + 'type' => Type::string(), + ] + ], + ]), + 'validate' => static fn() => null + ], ['upsertSku']); + } + + public function testInputObjectWithValidationOnFieldDoesNotThrow(): void + { + $this->expectNotToPerformAssertions(); + ErrorType::create([ + 'type' => new InputObjectType([ + 'name' => 'updateBook', + 'fields' => [ + 'authorId' => [ + 'type' => Type::id(), + 'validate' => static fn() => null + ], + 'publisherId' => [ + 'type' => Type::string(), + ] + ], + ]), + 'validate' => static fn() => null + ], ['upsertSku']); + } + + public function testListOfFloatThrows(): void + { + $this->expectExceptionMessage("You must provide at least one 'validate' callback or mark at least one field as 'required'."); + + ErrorType::create([ + 'type' => Type::listOf(Type::float()), + ], ['upsertSku']); + } + + public function testListOfValidatedFloatDoesNotThrow(): void + { + $this->expectNotToPerformAssertions(); + ErrorType::create([ + 'type' => Type::listOf(Type::float()), + 'items' => ['validate' => static fn() => null] + ], ['upsertSku']); + } + + public function testListOfStringThrows(): void + { + $this->expectExceptionMessage("You must provide at least one 'validate' callback or mark at least one field as 'required'."); + + ErrorType::create([ + 'type' => Type::listOf(Type::string()), + ], ['upsertSku']); + } + + public function testListOfValidatedStringDoesNotThrow(): void + { + $this->expectNotToPerformAssertions(); + ErrorType::create([ + 'type' => Type::listOf(Type::string()), + 'items' => ['validate' => static fn() => null] + ], ['upsertSku']); + } + + public function testListOfIdThrows(): void + { + $this->expectExceptionMessage("You must provide at least one 'validate' callback or mark at least one field as 'required'."); + + ErrorType::create([ + 'type' => Type::string(), + ], ['upsertSku']); + } + + public function testListOfValidatedIdDoesNotThrow(): void + { + $this->expectNotToPerformAssertions(); + ErrorType::create([ + 'type' => Type::listOf(Type::id()), + 'items' => ['validate' => static fn() => null] + ], ['upsertSku']); + } + + public function testListOfInputObjectThrows(): void + { + $this->expectExceptionMessage("You must provide at least one 'validate' callback or mark at least one field as 'required'."); + ErrorType::create([ + 'type' => Type::listOf(new InputObjectType([ + 'name' => 'updateBook', + 'fields' => [ + 'authorId' => [ + 'type' => Type::id(), + ], + 'publisherId' => [ + 'type' => Type::string(), + ] + ], + ])), + ], ['upsertSku']); + } + + public function testItemValidationOnListOfInputObjectThrows(): void + { + $this->expectExceptionMessage("'items' is only supported for scalar types"); + ErrorType::create([ + 'items' => ['validate' => static fn() => null], + 'type' => Type::listOf(new InputObjectType([ + 'name' => 'updateBook', + 'fields' => [ + 'authorId' => [ + 'type' => Type::id(), + ], + 'publisherId' => [ + 'type' => Type::string(), + ] + ], + ])), + ], ['upsertSku']); + } +} diff --git a/tests/Type/ErrorCodeTypeGenerationTest.php b/tests/Type/ErrorCodeTypeGenerationTest.php deleted file mode 100644 index 6d4c6dc..0000000 --- a/tests/Type/ErrorCodeTypeGenerationTest.php +++ /dev/null @@ -1,138 +0,0 @@ - static function ($val) { - return $val ? 0 : 1; - }, - 'errorCodes' => UserValidation::class, - 'type' => new IDType(['name' => 'User']), - ], ['updateUser']); - - $generatedErrorType = $type->config['fields']['code']['type']; - - static::assertTrue( $generatedErrorType->name == 'UpdateUserErrorCode'); - - self::assertEquals( - SchemaPrinter::printType($generatedErrorType), - Utils::nowdoc(' - "Error code" - enum UpdateUserErrorCode { - UnknownUser - UserIsMinor - } - ') - ); - } - - public function testShortNameWhenTypeSetter(): void - { - $types = []; - new UserErrorsType([ - 'validate' => static function ($val) { - return $val ? 0 : 1; - }, - 'errorCodes' => UserValidation::class, - 'typeSetter' => static function ($type) use (&$types): Type { - if(!isset($types[$type->name])) { - $types[$type->name] = $type; - } - return $types[$type->name]; - }, - 'type' => new IDType(['name' => 'User']), - ], ['updateUser']); - - static::assertTrue(isset($types['UserValidationErrorCode'])); - - self::assertEquals( - SchemaPrinter::printType($types['UserValidationErrorCode']), - Utils::nowdoc(' - "Error code" - enum UserValidationErrorCode { - UnknownUser - UserIsMinor - } - ') - ); - } - - public function testFieldsWithNoErrorCodes(): void - { - $types = []; - $type = new UserErrorsType([ - 'type' => new InputObjectType([ - 'name' => 'bookInput', - 'fields' => [ - 'authorId' => [ - 'type' => Type::id(), - 'description' => 'An author Id', - ], - ], - ]), - ], ['updateBook']); - - self::assertEquals($types, []); - self::assertEquals(SchemaPrinter::printType($type), Utils::nowdoc(' - "User errors for UpdateBook" - type UpdateBookError - ')); - } - - public function testFieldsWithErrorCodes(): void - { - $types = []; - new UserErrorsType([ - 'type' => new InputObjectType([ - 'name' => 'bookInput', - 'fields' => [ - 'authorId' => [ - 'validate' => static function ($authorId) { - return $authorId ? 0 : 1; - }, - 'errorCodes' => AuthorValidation::class, - 'type' => Type::id(), - 'description' => 'An author Id', - ], - ], - ]), - 'typeSetter' => static function ($type) use (&$types): Type { - $types[$type->name] = $type; - return $types[$type->name]; - }, - ], ['updateBook']); - - self::assertCount(2, \array_keys($types)); - self::assertTrue(isset($types['AuthorValidationErrorCode'])); - self::assertEquals( - Utils::nowdoc(' - "Error code" - enum AuthorValidationErrorCode { - UnknownAuthor - } - '), - SchemaPrinter::printType($types['AuthorValidationErrorCode']), - ); - } -} diff --git a/tests/Type/ErrorType/CustomErrorCode.php b/tests/Type/ErrorType/CustomErrorCode.php new file mode 100644 index 0000000..575b0f6 --- /dev/null +++ b/tests/Type/ErrorType/CustomErrorCode.php @@ -0,0 +1,248 @@ +_checkSchema(ErrorType::create([ + 'validate' => static fn() => null, + 'type' => Type::id(), + 'errorCodes' => ColorError::class + ], ['palette']), ' + schema { + mutation: PaletteError + } + + "User errors for Palette" + type PaletteError { + "An enumerated error code." + __code: Palette_ColorError + + "An error message." + __msg: String + } + + enum Palette_ColorError { + invalidColor + badHue + } + + '); + } + + public function testCustomEnumOnListOfIdType(): void + { + $this->_checkSchema(ErrorType::create([ + 'type' => Type::listOf(Type::id()), + 'items' => [ + 'validate' => static fn() => null, + 'errorCodes' => ColorError::class + ], + ], ['palette']), ' + schema { + mutation: PaletteError + } + + "User errors for Palette" + type PaletteError { + "Validation errors for each ID in the list" + items: [PaletteError_IDError] + } + + "User errors for ID" + type PaletteError_IDError { + "A path describing this item\'s location in the nested array" + __path: [Int] + + "An enumerated error code." + __code: PaletteError_ID_ColorError + + "An error message." + __msg: String + } + + enum PaletteError_ID_ColorError { + invalidColor + badHue + } + + '); + } + + /** + * When there is no typesetter provided, we expect unique name for each error code enum + */ + public function testFieldsWithErrorCodesAndNoTypeSetter(): void + { + $this->_checkSchema( + ErrorType::create([ + 'type' => new InputObjectType([ + 'name' => 'updateBook', + 'fields' => [ + 'authorId' => [ + 'errorCodes' => PersonErrorCode::class, + 'type' => Type::id(), + 'validate' => static fn() => null, + ], + 'editorId' => [ + 'errorCodes' => PersonErrorCode::class, + 'type' => Type::id(), + 'validate' => static fn() => null, + ], + ], + ]), + ], ['updateBook']), ' + schema { + mutation: UpdateBookError + } + + "User errors for UpdateBook" + type UpdateBookError { + "Error for authorId" + authorId: UpdateBook_AuthorIdError + + "Error for editorId" + editorId: UpdateBook_EditorIdError + } + + "User errors for AuthorId" + type UpdateBook_AuthorIdError { + "An enumerated error code." + __code: UpdateBook_AuthorId_PersonErrorCode + + "An error message." + __msg: String + } + + enum UpdateBook_AuthorId_PersonErrorCode { + PersonNotFound + Retired + } + + "User errors for EditorId" + type UpdateBook_EditorIdError { + "An enumerated error code." + __code: UpdateBook_EditorId_PersonErrorCode + + "An error message." + __msg: String + } + + enum UpdateBook_EditorId_PersonErrorCode { + PersonNotFound + Retired + } + + '); + } + + /** + * When there is a typesetter provided, we expect a shared name for custom error code + */ + public function testFieldsWithErrorCodesAndTypeSetter(): void + { + $types = []; + + $this->_checkSchema( + ErrorType::create([ + 'typeSetter' => static function ($type) use (&$types): Type { + $types[$type->name] ??= $type; + return $types[$type->name]; + }, + + 'type' => new InputObjectType([ + 'name' => 'updateBook', + 'fields' => [ + 'authorId' => [ + 'errorCodes' => PersonErrorCode::class, + 'type' => Type::id(), + 'validate' => static fn() => null, + ], + 'editorId' => [ + 'errorCodes' => PersonErrorCode::class, + 'type' => Type::id(), + 'validate' => static fn() => null, + ], + ], + ]), + ], ['updateBook']), ' + schema { + mutation: UpdateBookError + } + + "User errors for UpdateBook" + type UpdateBookError { + "Error for authorId" + authorId: UpdateBook_AuthorIdError + + "Error for editorId" + editorId: UpdateBook_EditorIdError + } + + "User errors for AuthorId" + type UpdateBook_AuthorIdError { + "An enumerated error code." + __code: PersonErrorCode + + "An error message." + __msg: String + } + + enum PersonErrorCode { + PersonNotFound + Retired + } + + "User errors for EditorId" + type UpdateBook_EditorIdError { + "An enumerated error code." + __code: PersonErrorCode + + "An error message." + __msg: String + } + + '); + } + +// public function testStringTypeWithErrorCodesAndTypeSetter(): void +// { +// $types = []; +// +// $this->_checkSchema( +// ErrorType::create([ +// 'typeSetter' => static function ($type) use (&$types): Type { +// if (!isset($types[$type->name])) { +// $types[$type->name] = $type; +// } +// return $types[$type->name]; +// }, +// +// 'type' => new StringType([ +// 'validate' => static fn () => null, +// ]), +// ], ['upsertSku']), '' +// ); +// } +} diff --git a/tests/Type/ErrorType/InputObjectTest.php b/tests/Type/ErrorType/InputObjectTest.php new file mode 100644 index 0000000..93e1ae3 --- /dev/null +++ b/tests/Type/ErrorType/InputObjectTest.php @@ -0,0 +1,243 @@ +expectExceptionMessage('If you specify errorCodes, you must also provide a validate callback'); + + ErrorType::create([ + 'errorCodes' => PersonErrorCode::class, + 'type' => new InputObjectType([ + 'name' => 'updateBook', + 'fields' => [ + 'authorId' => [ + 'type' => Type::id(), + ], + ], + ]), + ], ['updateBook']); + } + + public function testValidateOnFieldsButNotOnSelf(): void + { + $this->_checkSchema( + ErrorType::create([ + 'type' => new InputObjectType([ + 'name' => 'book', + 'fields' => [ + 'title' => [ + 'type' => Type::string(), + 'validate' => static function () { + return 0; + }, + ], + 'authorId' => [ + 'validate' => static function (int $authorId): int { + return ($authorId > 0) ? 0 : 1; + }, + 'type' => Type::id(), + ], + ], + ]), + ], ['updateBook']), + ' + schema { + mutation: UpdateBookError + } + + "User errors for UpdateBook" + type UpdateBookError { + "Error for title" + title: UpdateBook_TitleError + + "Error for authorId" + authorId: UpdateBook_AuthorIdError + } + + "User errors for Title" + type UpdateBook_TitleError { + "A numeric error code. 0 on success, non-zero on failure." + __code: Int + + "An error message." + __msg: String + } + + "User errors for AuthorId" + type UpdateBook_AuthorIdError { + "A numeric error code. 0 on success, non-zero on failure." + __code: Int + + "An error message." + __msg: String + } + + ' + ); + } + + public function testValidateOnSelfButNotOnFields(): void + { + $this->_checkSchema( + ErrorType::create([ + 'validate' => static function () { + }, + 'type' => new InputObjectType([ + 'name' => 'book', + 'fields' => [ + 'title' => [ + 'type' => Type::string(), + ], + 'authorId' => [ + 'type' => Type::id(), + ], + ], + ]), + ], ['updateBook']), + ' + schema { + mutation: UpdateBookError + } + + "User errors for UpdateBook" + type UpdateBookError { + "A numeric error code. 0 on success, non-zero on failure." + __code: Int + + "An error message." + __msg: String + } + + ' + ); + } + + public function testValidateOnSelfAndOnFields(): void + { + $this->_checkSchema( + ErrorType::create([ + 'validate' => static function () { + }, + 'type' => new InputObjectType([ + 'name' => 'book', + 'fields' => [ + 'title' => [ + 'validate' => static function () { + }, + 'type' => Type::string(), + ], + 'authorId' => [ + 'validate' => static function () { + }, + 'type' => Type::id(), + ], + ], + ]), + ], ['updateBook']), + ' + schema { + mutation: UpdateBookError + } + + "User errors for UpdateBook" + type UpdateBookError { + "A numeric error code. 0 on success, non-zero on failure." + __code: Int + + "An error message." + __msg: String + + "Error for title" + title: UpdateBook_TitleError + + "Error for authorId" + authorId: UpdateBook_AuthorIdError + } + + "User errors for Title" + type UpdateBook_TitleError { + "A numeric error code. 0 on success, non-zero on failure." + __code: Int + + "An error message." + __msg: String + } + + "User errors for AuthorId" + type UpdateBook_AuthorIdError { + "A numeric error code. 0 on success, non-zero on failure." + __code: Int + + "An error message." + __msg: String + } + + ' + ); + } + + public function testValidateOnDeeplyNestedField(): void + { + $this->_checkSchema( + ErrorType::create([ + 'type' => new InputObjectType([ + 'name' => 'book', + 'fields' => [ + 'author' => [ + 'type' => new InputObjectType([ + 'name' => 'address', + 'fields' => [ + 'zip' => [ + 'validate' => static function () { + }, + 'type' => Type::string(), + ], + ], + ]), + ], + ], + ]), + ], ['updateBook']), + ' + schema { + mutation: UpdateBookError + } + + "User errors for UpdateBook" + type UpdateBookError { + "Error for author" + author: UpdateBook_AuthorError + } + + "User errors for Author" + type UpdateBook_AuthorError { + "Error for zip" + zip: UpdateBook_Author_ZipError + } + + "User errors for Zip" + type UpdateBook_Author_ZipError { + "A numeric error code. 0 on success, non-zero on failure." + __code: Int + + "An error message." + __msg: String + } + + ' + ); + } +} diff --git a/tests/Type/ErrorType/ListOf.php b/tests/Type/ErrorType/ListOf.php new file mode 100644 index 0000000..ed6df66 --- /dev/null +++ b/tests/Type/ErrorType/ListOf.php @@ -0,0 +1,181 @@ +expectExceptionMessage("You must specify at least one 'validate' callback somewhere in the tree."); + ErrorType::create([ + 'type' => Type::listOf(Type::id()), + ], ['upsertSku']); + } + + public function testCheckTypesOnListOfWithValidatedString(): void + { + $type = ErrorType::create([ + 'type' => Type::listOf(Type::string()), + 'items' => [ + 'validate' => static fn($str) => null + ] + ], ['upsertSku']); + + + $this->_checkSchema($type, ' + schema { + mutation: UpsertSkuError + } + + "User errors for UpsertSku" + type UpsertSkuError { + "Validation errors for each String in the list" + items: [UpsertSkuError_StringError] + } + + "User errors for String" + type UpsertSkuError_StringError { + "A path describing this item\'s location in the nested array" + path: [Int] + + "A numeric error code. 0 on success, non-zero on failure." + __code: Int + + "An error message." + __msg: String + } + + '); + } + + public function testCheckTypesOnListOfInputObjectWithValidation(): void + { + $type = ErrorType::create([ + 'type' => Type::listOf(new InputObjectType([ + 'name' => 'updateBook', + 'validate' => static fn($value) => null, + 'fields' => [ + 'authorId' => [ + 'type' => Type::id(), + 'validate' => static fn($value) => null + ], + ], + ])), + ], ['upsertSku']); + + $this->_checkSchema($type, ' + schema { + mutation: UpsertSkuError + } + + "User errors for UpsertSku" + type UpsertSkuError { + "Validation errors for each updateBook in the list" + items: [UpsertSkuError_UpdateBookError] + } + + "User errors for UpdateBook" + type UpsertSkuError_UpdateBookError { + "A path describing this item\'s location in the nested array" + path: [Int] + + "A numeric error code. 0 on success, non-zero on failure." + __code: Int + + "An error message." + __msg: String + + "Error for authorId" + authorId: UpsertSkuError_UpdateBook_AuthorIdError + } + + "User errors for AuthorId" + type UpsertSkuError_UpdateBook_AuthorIdError { + "A numeric error code. 0 on success, non-zero on failure." + __code: Int + + "An error message." + __msg: String + } + + '); + } + + public function testCheckTypesOnListOfListOfWithValidatedString(): void + { + $type = ErrorType::create([ + 'type' => Type::listOf(Type::listOf(Type::string())), + 'items' => [ + 'validate' => static fn($str) => null + ] + ], ['upsertSku']); + + $this->_checkSchema($type, ' + schema { + mutation: UpsertSkuError + } + + "User errors for UpsertSku" + type UpsertSkuError { + "Validation errors for each String in the list" + items: [UpsertSkuError_StringError] + } + + "User errors for String" + type UpsertSkuError_StringError { + "A path describing this item\'s location in the nested array" + path: [Int] + + "A numeric error code. 0 on success, non-zero on failure." + code: Int + + "An error message." + msg: String + } + + '); + } + + public function testCheckTypesOnListOfWithValidatedBoolean(): void + { + $type = ErrorType::create([ + 'type' => Type::listOf(Type::boolean()), + 'items' => [ + 'validate' => static fn($str) => null + ] + ], ['upsertSku']); + + $this->_checkSchema($type, ' + schema { + mutation: UpsertSkuError + } + + "User errors for UpsertSku" + type UpsertSkuError { + "Validation errors for each Boolean in the list" + items: [UpsertSkuError_BooleanError] + } + + "User errors for Boolean" + type UpsertSkuError_BooleanError { + "A path describing this item\'s location in the nested array" + path: [Int] + + "A numeric error code. 0 on success, non-zero on failure." + code: Int + + "An error message." + msg: String + } + + '); + } +} diff --git a/tests/Type/ErrorType/NonNull.php b/tests/Type/ErrorType/NonNull.php new file mode 100644 index 0000000..73344c9 --- /dev/null +++ b/tests/Type/ErrorType/NonNull.php @@ -0,0 +1,98 @@ +_checkSchema(ErrorType::create([ + 'type' => Type::nonNull(Type::string()), + 'validate' => static fn() => null + ], ['upsertSku']), ' + schema { + mutation: UpsertSkuError + } + + "User errors for UpsertSku" + type UpsertSkuError { + "A numeric error code. 0 on success, non-zero on failure." + __code: Int + + "An error message." + __msg: String + } + + '); + } + + public function testInputObjectWrappedType(): void + { + $this->_checkSchema(ErrorType::create([ + 'type' => Type::nonNull(new InputObjectType([ + 'name' => 'bookInput', + 'fields' => [ + 'firstName' => [ + 'type' => Type::string(), + 'description' => 'A first name', + 'validate' => static function ($firstName) { + if (strlen($firstName) > 100) { + return 1; + } + + return 0; + }, + ], + 'lastName' => [ + 'type' => Type::string(), + 'description' => 'A last name', + 'validate' => static function ($lastName) { + if (strlen($lastName) > 100) { + return 1; + } + + return 0; + }, + ], + ], + ])), + ], ['upsertSku']), ' + schema { + mutation: UpsertSkuError + } + + "User errors for UpsertSku" + type UpsertSkuError { + "Error for firstName" + firstName: UpsertSku_FirstNameError + + "Error for lastName" + lastName: UpsertSku_LastNameError + } + + "User errors for FirstName" + type UpsertSku_FirstNameError { + "A numeric error code. 0 on success, non-zero on failure." + __code: Int + + "An error message." + __msg: String + } + + "User errors for LastName" + type UpsertSku_LastNameError { + "A numeric error code. 0 on success, non-zero on failure." + __code: Int + + "An error message." + __msg: String + } + + '); + } +} diff --git a/tests/Type/ErrorType/ScalarTest.php b/tests/Type/ErrorType/ScalarTest.php new file mode 100644 index 0000000..a7c6f18 --- /dev/null +++ b/tests/Type/ErrorType/ScalarTest.php @@ -0,0 +1,77 @@ +_checkSchema(ErrorType::create([ + 'validate' => static fn() => null, + 'type' => Type::id(), + ], ['palette']), ' + schema { + mutation: PaletteError + } + + "User errors for Palette" + type PaletteError { + "A numeric error code. 0 on success, non-zero on failure." + __code: Int + + "An error message." + __msg: String + } + + '); + } + + public function testBoolean(): void + { + $this->_checkSchema(ErrorType::create([ + 'validate' => static fn() => null, + 'type' => Type::boolean(), + ], ['palette']), ' + schema { + mutation: PaletteError + } + + "User errors for Palette" + type PaletteError { + "A numeric error code. 0 on success, non-zero on failure." + __code: Int + + "An error message." + __msg: String + } + + '); + } + + public function testString(): void + { + $this->_checkSchema(ErrorType::create([ + 'validate' => static fn() => null, + 'type' => Type::string(), + ], ['palette']), ' + schema { + mutation: PaletteError + } + + "User errors for Palette" + type PaletteError { + "A numeric error code. 0 on success, non-zero on failure." + __code: Int + + "An error message." + __msg: String + } + + '); + } +} diff --git a/tests/Type/FieldDefinition.php b/tests/Type/TestBase.php similarity index 77% rename from tests/Type/FieldDefinition.php rename to tests/Type/TestBase.php index 30b831b..0f9f114 100644 --- a/tests/Type/FieldDefinition.php +++ b/tests/Type/TestBase.php @@ -1,34 +1,33 @@ 'Mutation', - 'fields' => static function () use ($field) { - return [ - $field->name => $field, - ]; - }, - ]); + $actual = SchemaPrinter::doPrint(new Schema(['mutation' => $field])); + self::assertEquals(Utils::nowdoc($expected), $actual); + } - $actual = SchemaPrinter::doPrint(new Schema(['mutation' => $mutation])); + protected function _checkType(Type $type, string $expected): void + { + $actual = SchemaPrinter::printType($type); self::assertEquals(Utils::nowdoc($expected), $actual); } + /** * @param array|null $args * @param array $expected @@ -63,7 +62,7 @@ protected function _checkValidation(ValidatedFieldDefinition $field, string $qry /** * @param array $expectedMap */ - protected function _checkTypes(UserErrorsType $field, array $expectedMap): void + protected function _checkTypes(ErrorType $field, array $expectedMap): void { $mutation = new ObjectType([ 'name' => 'Mutation', @@ -79,7 +78,7 @@ protected function _checkTypes(UserErrorsType $field, array $expectedMap): void $types = $schema->getTypeMap(); $types = array_filter($types, function ($type) { - return ! $type->isBuiltInType(); + return !$type->isBuiltInType(); }); $typeMap = array_map(function ($type) { @@ -88,11 +87,12 @@ protected function _checkTypes(UserErrorsType $field, array $expectedMap): void return Utils::toNowDoc(SchemaPrinter::printType($type), 8); }, $types); - if (! empty($this->outputPath)) { + if (!empty($this->outputPath)) { $lines = preg_split('/\\n/', Utils::varExport($typeMap, true)); assert($lines !== false); $numLines = \count($lines); for ($i = 0; $i < $numLines; ++$i) { + $lines[$i] = str_repeat(' ', 12) . $lines[$i]; } diff --git a/tests/Type/UserErrorsType/Basic.php b/tests/Type/UserErrorsType/Basic.php deleted file mode 100644 index a37d261..0000000 --- a/tests/Type/UserErrorsType/Basic.php +++ /dev/null @@ -1,154 +0,0 @@ - Type::id(), - ], ['upsertSku']); - - self::assertEquals(Utils::nowdoc(' - schema { - query: UpsertSkuError - } - - "User errors for UpsertSku" - type UpsertSkuError - - '), SchemaPrinter::doPrint(new Schema(['query' => $type]))); - } - - public function testRenameResultsField(): void - { - $this->_checkSchema( - new ValidatedFieldDefinition([ - 'type' => Type::boolean(), - 'name' => 'updateBook', - 'resultName' => '_result', - 'validate' => static function () {}, - 'args' => [], - 'resolve' => static function (array $data): bool { - return ! empty($data); - }, - ]), - ' - type Mutation { - updateBook: UpdateBookResult - } - - "User errors for UpdateBook" - type UpdateBookResult { - "The payload, if any" - _result: Boolean - - "Whether all validation passed. True for yes, false for no." - valid: Boolean! - - "A numeric error code. 0 on success, non-zero on failure." - code: Int - - "An error message." - msg: String - } - - ' - ); - } - - public function testRenameValidField(): void - { - $this->_checkSchema( - new ValidatedFieldDefinition([ - 'type' => Type::boolean(), - 'name' => 'updateBook', - 'validName' => '_valid', - 'validate' => static function () {}, - 'args' => [], - 'resolve' => static function (array $data): bool { - return ! empty($data); - }, - ]), - ' - type Mutation { - updateBook: UpdateBookResult - } - - "User errors for UpdateBook" - type UpdateBookResult { - "The payload, if any" - result: Boolean - - "Whether all validation passed. True for yes, false for no." - _valid: Boolean! - - "A numeric error code. 0 on success, non-zero on failure." - code: Int - - "An error message." - msg: String - } - - ' - ); - } - - public function testNoValidateCallbacks(): void - { - $this->expectExceptionMessage("You must specify at least one 'validate' callback somewhere"); - UserErrorsType::create([ - 'type' => new InputObjectType([ - 'name' => 'book', - 'fields' => [ - 'author' => [ - 'type' => new InputObjectType([ - 'name' => 'address', - 'fields' => [ - 'zip' => [ - 'type' => Type::string(), - ], - ], - ]), - ], - ], - ]), - ], ['updateBook']); - } - - public function testValidationWithNoErrorCodes(): void - { - $type = UserErrorsType::create([ - 'validate' => static function ($value) { - return $value ? 0 : 1; - }, - 'type' => Type::id(), - ], ['upsertSku']); - - self::assertEquals(Utils::nowdoc(' - schema { - query: UpsertSkuError - } - - "User errors for UpsertSku" - type UpsertSkuError { - "A numeric error code. 0 on success, non-zero on failure." - code: Int - - "An error message." - msg: String - } - - '), SchemaPrinter::doPrint(new Schema(['query' => $type]))); - } -} diff --git a/tests/Type/UserErrorsType/InputObjectTest.php b/tests/Type/UserErrorsType/InputObjectTest.php deleted file mode 100644 index fd40615..0000000 --- a/tests/Type/UserErrorsType/InputObjectTest.php +++ /dev/null @@ -1,229 +0,0 @@ -expectExceptionMessage('If you specify errorCodes, you must also provide a validate callback'); - - new UserErrorsType([ - 'errorCodes' => AuthorErrorTest::class, - 'type' => new InputObjectType([ - 'name' => 'updateBook', - 'fields' => [ - 'authorId' => [ - 'type' => Type::id(), - ], - ], - ]), - ], ['updateBook']); - } - - public function testValidateOnFieldsButNotOnSelf(): void - { - $this->_checkTypes( - UserErrorsType::create([ - 'type' => new InputObjectType([ - 'name' => 'book', - 'fields' => [ - 'title' => [ - 'type' => Type::string(), - 'validate' => static function () { return 0; }, - ], - 'authorId' => [ - 'validate' => static function (int $authorId): int { - return ($authorId > 0) ? 0 : 1; - }, - 'type' => Type::id(), - ], - ], - ]), - ], ['updateBook']), - [ - 'Mutation' => ' - type Mutation { - UpdateBookError: UpdateBookError - } - ', - 'UpdateBookError' => ' - type UpdateBookError { - "Error for title" - title: UpdateBook_TitleError - - "Error for authorId" - authorId: UpdateBook_AuthorIdError - } - ', - 'UpdateBook_TitleError' => ' - type UpdateBook_TitleError { - "A numeric error code. 0 on success, non-zero on failure." - code: Int - - "An error message." - msg: String - } - ', - 'UpdateBook_AuthorIdError' => ' - type UpdateBook_AuthorIdError { - "A numeric error code. 0 on success, non-zero on failure." - code: Int - - "An error message." - msg: String - } - ', - ] - ); - } - - public function testValidateOnSelfButNotOnFields(): void - { - $this->_checkTypes( - UserErrorsType::create([ - 'validate' => static function () {}, - 'type' => new InputObjectType([ - 'name' => 'book', - 'fields' => [ - 'title' => [ - 'type' => Type::string(), - ], - 'authorId' => [ - 'type' => Type::id(), - ], - ], - ]), - ], ['updateBook']), - [ - 'UpdateBookError' => ' - type UpdateBookError { - "A numeric error code. 0 on success, non-zero on failure." - code: Int - - "An error message." - msg: String - } - ', - ] - ); - } - - public function testValidateOnSelfAndOnFields(): void - { - $this->_checkTypes( - UserErrorsType::create([ - 'validate' => static function () {}, - 'type' => new InputObjectType([ - 'name' => 'book', - 'fields' => [ - 'title' => [ - 'validate' => static function () {}, - 'type' => Type::string(), - ], - 'authorId' => [ - 'validate' => static function () {}, - 'type' => Type::id(), - ], - ], - ]), - ], ['updateBook']), - [ - 'UpdateBookError' => ' - type UpdateBookError { - "A numeric error code. 0 on success, non-zero on failure." - code: Int - - "An error message." - msg: String - - "Validation errors for UpdateBook" - suberrors: UpdateBook_FieldErrors - } - ', - 'UpdateBook_FieldErrors' => ' - type UpdateBook_FieldErrors { - "Error for title" - title: UpdateBook_TitleError - - "Error for authorId" - authorId: UpdateBook_AuthorIdError - } - ', - 'UpdateBook_TitleError' => ' - type UpdateBook_TitleError { - "A numeric error code. 0 on success, non-zero on failure." - code: Int - - "An error message." - msg: String - } - ', - 'UpdateBook_AuthorIdError' => ' - type UpdateBook_AuthorIdError { - "A numeric error code. 0 on success, non-zero on failure." - code: Int - - "An error message." - msg: String - } - ', - ] - ); - } - - public function testValidateOnDeeplyNestedField(): void - { - $this->_checkTypes( - UserErrorsType::create([ - 'type' => new InputObjectType([ - 'name' => 'book', - 'fields' => [ - 'author' => [ - 'type' => new InputObjectType([ - 'name' => 'address', - 'fields' => [ - 'zip' => [ - 'validate' => static function () {}, - 'type' => Type::string(), - ], - ], - ]), - ], - ], - ]), - ], ['updateBook']), - [ - 'UpdateBookError' => ' - type UpdateBookError { - "Error for author" - author: UpdateBook_AuthorError - } - ', - 'UpdateBook_AuthorError' => ' - type UpdateBook_AuthorError { - "Error for zip" - zip: UpdateBook_Author_ZipError - } - ', - 'UpdateBook_Author_ZipError' => ' - type UpdateBook_Author_ZipError { - "A numeric error code. 0 on success, non-zero on failure." - code: Int - - "An error message." - msg: String - } - ', - ] - ); - } -} diff --git a/tests/Type/UserErrorsType/ListOf.php b/tests/Type/UserErrorsType/ListOf.php deleted file mode 100644 index 5714dc5..0000000 --- a/tests/Type/UserErrorsType/ListOf.php +++ /dev/null @@ -1,286 +0,0 @@ - Type::listOf(Type::id()), - ], ['upsertSku']); - - self::assertEquals(Utils::nowdoc(' - schema { - query: UpsertSkuError - } - - "User errors for UpsertSku" - type UpsertSkuError - - '), SchemaPrinter::doPrint(new Schema(['query' => $type]))); - } - - public function testListOfStringWithValidationOnSelf(): void - { - $this->_checkTypes( - UserErrorsType::create([ - 'type' => Type::listOf(Type::string()), - 'validate' => static function (string $phoneNumber) {}, - ], ['phoneNumber'], true), - [ - 'PhoneNumberError' => ' - type PhoneNumberError { - "A numeric error code. 0 on success, non-zero on failure." - code: Int - - "An error message." - msg: String - - "A path describing this item\'s location in the nested array" - path: [Int] - } - ', - ] - ); - } - - public function testListOfInputObjectWithValidationOnSelf(): void - { - $this->_checkTypes( - UserErrorsType::create( - [ - 'type' => Type::listOf(new InputObjectType([ - 'name' => 'Address', - 'fields' => [ - 'city' => [ - 'type' => Type::string(), - ], - 'zip' => [ - 'type' => Type::int(), - ], - ], - ])), - 'validate' => static function () {}, - ], - ['address'], - true - ), - [ - 'AddressError' => ' - type AddressError { - "A numeric error code. 0 on success, non-zero on failure." - code: Int - - "An error message." - msg: String - - "A path describing this item\'s location in the nested array" - path: [Int] - } - ', - ] - ); - } - - public function testListOfInputObjectWithValidationOnFields(): void - { - $this->_checkTypes( - UserErrorsType::create( - [ - 'type' => Type::listOf(new InputObjectType([ - 'name' => 'Address', - 'fields' => [ - 'city' => [ - 'type' => Type::string(), - 'validate' => static function () {}, - ], - 'zip' => [ - 'type' => Type::int(), - 'validate' => static function () {}, - ], - ], - ])), - ], - ['address'], - true - ), - [ - 'AddressError' => ' - type AddressError { - "Validation errors for Address" - suberrors: Address_FieldErrors - - "A path describing this item\'s location in the nested array" - path: [Int] - } - ', - 'Address_FieldErrors' => ' - type Address_FieldErrors { - "Error for city" - city: Address_CityError - - "Error for zip" - zip: Address_ZipError - } - ', - ] - ); - } - - public function testListOfInputObjectWithValidationOnSelfAndFields(): void - { - $this->_checkTypes( - UserErrorsType::create( - [ - 'validate' => static function () {}, - 'type' => Type::listOf(new InputObjectType([ - 'name' => 'Address', - 'fields' => [ - 'city' => [ - 'type' => Type::string(), - 'validate' => static function () {}, - ], - 'zip' => [ - 'type' => Type::int(), - 'validate' => static function () {}, - ], - ], - ])), - ], - ['address'], - true - ), - [ - 'AddressError' => ' - type AddressError { - "A numeric error code. 0 on success, non-zero on failure." - code: Int - - "An error message." - msg: String - - "Validation errors for Address" - suberrors: Address_FieldErrors - - "A path describing this item\'s location in the nested array" - path: [Int] - } - ', - 'Address_FieldErrors' => ' - type Address_FieldErrors { - "Error for city" - city: Address_CityError - - "Error for zip" - zip: Address_ZipError - } - ', - ] - ); - } - - public function testListOfListOfListOfScalarWithValidation(): void - { - $this->_checkTypes( - UserErrorsType::create( - [ - 'validate' => static function () {}, - 'type' => Type::listOf(Type::listOf(Type::listOf(Type::string()))), - ], - ['ids'], - true - ), - [ - 'IdsError' => ' - type IdsError { - "A numeric error code. 0 on success, non-zero on failure." - code: Int - - "An error message." - msg: String - - "A path describing this item\'s location in the nested array" - path: [Int] - } - ', - ] - ); - } - - public function testValidateOnDeeplyNestedField(): void - { - $this->_checkTypes( - UserErrorsType::create([ - 'type' => Type::listOf(new InputObjectType([ - 'name' => 'book', - 'fields' => [ - 'author' => [ - 'type' => Type::listOf(new InputObjectType([ - 'name' => 'address', - 'fields' => [ - 'zip' => [ - 'validate' => static function () {}, - 'type' => Type::listOf(Type::string()), - ], - ], - ])), - ], - ], - ])), - ], ['updateBook'], true), - [ - 'UpdateBookError' => ' - type UpdateBookError { - "Validation errors for UpdateBook" - suberrors: UpdateBook_FieldErrors - - "A path describing this item\'s location in the nested array" - path: [Int] - } - ', - 'UpdateBook_FieldErrors' => ' - type UpdateBook_FieldErrors { - "Error for author" - author: [UpdateBook_AuthorError] - } - ', - 'UpdateBook_AuthorError' => ' - type UpdateBook_AuthorError { - "Validation errors for Author" - suberrors: UpdateBook_Author_FieldErrors - - "A path describing this item\'s location in the nested array" - path: [Int] - } - ', - 'UpdateBook_Author_FieldErrors' => ' - type UpdateBook_Author_FieldErrors { - "Error for zip" - zip: [UpdateBook_Author_ZipError] - } - ', - 'UpdateBook_Author_ZipError' => ' - type UpdateBook_Author_ZipError { - "A numeric error code. 0 on success, non-zero on failure." - code: Int - - "An error message." - msg: String - - "A path describing this item\'s location in the nested array" - path: [Int] - } - ', - ] - ); - } -} diff --git a/tests/Type/UserErrorsType/NonNull.php b/tests/Type/UserErrorsType/NonNull.php deleted file mode 100644 index ca07604..0000000 --- a/tests/Type/UserErrorsType/NonNull.php +++ /dev/null @@ -1,188 +0,0 @@ -_checkSchema(new ValidatedFieldDefinition([ - 'type' => Type::boolean(), - 'name' => 'deleteAuthor', - 'args' => [ - 'authorId' => [ - 'type' => Type::nonNull(Type::string()), - 'validate' => static function (array $authorId) { - if (empty($authorId)) { - return [1, 'Invalid author id']; - } - - return 0; - }, - ], - ], - 'resolve' => static function (array $data): bool { - return ! empty($data); - }, - ]), ' - type Mutation { - deleteAuthor(authorId: String!): DeleteAuthorResult - } - - "User errors for DeleteAuthor" - type DeleteAuthorResult { - "The payload, if any" - result: Boolean - - "Whether all validation passed. True for yes, false for no." - valid: Boolean! - - "Validation errors for DeleteAuthor" - suberrors: DeleteAuthor_FieldErrors - } - - "User Error" - type DeleteAuthor_FieldErrors { - "Error for authorId" - authorId: DeleteAuthor_AuthorIdError - } - - "User errors for AuthorId" - type DeleteAuthor_AuthorIdError { - "A numeric error code. 0 on success, non-zero on failure." - code: Int - - "An error message." - msg: String - } - - '); - } - - public function testInputObjectWrappedType(): void - { - $this->_checkSchema(new ValidatedFieldDefinition([ - 'type' => Type::boolean(), - 'name' => 'updateAuthor', - 'args' => [ - 'author' => [ - 'type' => Type::nonNull(new InputObjectType([ - 'name' => 'bookInput', - 'fields' => [ - 'firstName' => [ - 'type' => Type::string(), - 'description' => 'A first name', - 'validate' => static function ($firstName) { - if (strlen($firstName) > 100) { - return 1; - } - - return 0; - }, - ], - 'lastName' => [ - 'type' => Type::string(), - 'description' => 'A last name', - 'validate' => static function ($lastName) { - if (strlen($lastName) > 100) { - return 1; - } - - return 0; - }, - ], - ], - ])), - 'validate' => static function (array $author) { - if (empty($author['firstName'] && empty($author['lastName']))) { - return [ - 1, - 'Please provide at least a first or a last name', - ]; - } - - return 0; - }, - ], - ], - 'resolve' => static function (array $data): bool { - return ! empty($data); - }, - ]), ' - type Mutation { - updateAuthor(author: bookInput!): UpdateAuthorResult - } - - input bookInput { - "A first name" - firstName: String - - "A last name" - lastName: String - } - - "User errors for UpdateAuthor" - type UpdateAuthorResult { - "The payload, if any" - result: Boolean - - "Whether all validation passed. True for yes, false for no." - valid: Boolean! - - "Validation errors for UpdateAuthor" - suberrors: UpdateAuthor_FieldErrors - } - - "User Error" - type UpdateAuthor_FieldErrors { - "Error for author" - author: UpdateAuthor_AuthorError - } - - "User errors for Author" - type UpdateAuthor_AuthorError { - "A numeric error code. 0 on success, non-zero on failure." - code: Int - - "An error message." - msg: String - - "Validation errors for Author" - suberrors: UpdateAuthor_Author_FieldErrors - } - - "User Error" - type UpdateAuthor_Author_FieldErrors { - "Error for firstName" - firstName: UpdateAuthor_Author_FirstNameError - - "Error for lastName" - lastName: UpdateAuthor_Author_LastNameError - } - - "User errors for FirstName" - type UpdateAuthor_Author_FirstNameError { - "A numeric error code. 0 on success, non-zero on failure." - code: Int - - "An error message." - msg: String - } - - "User errors for LastName" - type UpdateAuthor_Author_LastNameError { - "A numeric error code. 0 on success, non-zero on failure." - code: Int - - "An error message." - msg: String - } - - '); - } -} diff --git a/tests/Type/UserErrorsType/ScalarTest.php b/tests/Type/UserErrorsType/ScalarTest.php deleted file mode 100644 index c38f22b..0000000 --- a/tests/Type/UserErrorsType/ScalarTest.php +++ /dev/null @@ -1,67 +0,0 @@ - Type::id(), - ], ['upsertSku']); - - self::assertEquals(Utils::nowdoc(' - schema { - query: UpsertSkuError - } - - "User errors for UpsertSku" - type UpsertSkuError - - '), SchemaPrinter::doPrint(new Schema(['query' => $type]))); - } - - public function testWithValidation(): void - { - $type = new UserErrorsType([ - 'errorCodes' => ColorErrors::class, - 'validate' => static function ($value) { - return $value ? 0 : ColorErrors::invalidColor; - }, - 'type' => new IDType(['name' => 'Color']), - ], ['palette']); - - self::assertEquals(Utils::nowdoc(' - schema { - query: PaletteError - } - - "User errors for Palette" - type PaletteError { - "An error code" - code: PaletteErrorCode - - "A natural language description of the issue" - msg: String - } - - "Error code" - enum PaletteErrorCode { - invalidColor - } - - '), SchemaPrinter::doPrint(new Schema(['query' => $type]))); - } -} diff --git a/tests/Type/ValidatedFieldDefinition/BasicTest.php b/tests/Type/ValidatedFieldDefinition/BasicTest.php index c681027..b1c08ed 100644 --- a/tests/Type/ValidatedFieldDefinition/BasicTest.php +++ b/tests/Type/ValidatedFieldDefinition/BasicTest.php @@ -1,6 +1,6 @@ new ObjectType(['name' => 'Query', 'fields' => []]), + 'mutation' => new ObjectType([ + 'name' => 'Mutation', + 'fields' => function () { + return [ + 'updateBook' => new ValidatedFieldDefinition([ + 'name' => 'updateBook', + 'type' => Type::boolean(), + 'args' => [ + 'bookId' => [ + 'type' => Type::id(), + 'validate' => function ($bookId) { + return empty($bookId) ? 1 : 0; + }, + ], + ], + 'resolve' => static function ($value): bool { + return (bool)$value; + }, + ]), + ]; + }, + ]), + ]); + + $res = GraphQL::executeQuery( + $schema, + Utils::nowdoc(' + mutation UpdateBook( + $bookId:ID + ) { + updateBook (bookId: $bookId) { + __valid + bookId { + __code + __msg + } + __result + } + } + '), + [], + null, + ['bookId' => null] + ); + + static::assertEmpty($res->errors); + static::assertEquals($res->data['updateBook']['bookId']['__code'], 1); + } + + public function testEnumCodeType(): void + { + $schema = new Schema([ + 'query' => new ObjectType(['name' => 'Query', 'fields' => []]), + 'mutation' => new ObjectType([ + 'name' => 'Mutation', + 'fields' => function () { + return [ + 'updateBook' => new ValidatedFieldDefinition([ + 'name' => 'updateBook', + 'type' => Type::boolean(), + 'args' => [ + 'bookId' => [ + 'type' => Type::id(), + 'errorCodes' => BookError::class, + 'validate' => function ($bookId) { + return empty($bookId) ? BookError::required : 0; + }, + ], + ], + 'resolve' => static function ($value): bool { + return (bool)$value; + }, + ]), + ]; + }, + ]), + ]); + + $res = GraphQL::executeQuery( + $schema, + Utils::nowdoc(' + mutation UpdateBook( + $bookId:ID + ) { + updateBook (bookId: $bookId) { + bookId { + __code + __msg + } + __valid + __result + } + } + '), + [], + null, + ['bookId' => null] + ); + + static::assertEmpty($res->errors); + static::assertEquals($res->data['updateBook']['bookId']['__code'], 'required'); + } + + public function testIntCodeTypeAndMessage(): void + { + $schema = new Schema([ + 'query' => new ObjectType(['name' => 'Query', 'fields' => []]), + 'mutation' => new ObjectType([ + 'name' => 'Mutation', + 'fields' => function () { + return [ + 'updateBook' => new ValidatedFieldDefinition([ + 'name' => 'updateBook', + 'type' => Type::boolean(), + 'args' => [ + 'bookId' => [ + 'type' => Type::id(), + 'validate' => function ($bookId) { + return empty($bookId) ? [1, 'Invalid Book Id'] : 0; + }, + ], + ], + 'resolve' => static function ($value): bool { + return (bool)$value; + }, + ]), + ]; + }, + ]), + ]); + + $res = GraphQL::executeQuery( + $schema, + Utils::nowdoc(' + mutation UpdateBook( + $bookId:ID + ) { + updateBook (bookId: $bookId) { + __valid + bookId { + __code + __msg + } + __result + } + } + '), + [], + null, + ['bookId' => null] + ); + + static::assertEmpty($res->errors); + static::assertEquals(1, $res->data['updateBook']['bookId']['__code']); + static::assertEquals('Invalid Book Id', $res->data['updateBook']['bookId']['__msg']); + } + + public function testEnumCodeTypeAndMessage(): void + { + $schema = new Schema([ + 'query' => new ObjectType(['name' => 'Query', 'fields' => []]), + 'mutation' => new ObjectType([ + 'name' => 'Mutation', + 'fields' => function () { + return [ + 'updateBook' => new ValidatedFieldDefinition([ + 'name' => 'updateBook', + 'type' => Type::boolean(), + 'args' => [ + 'bookId' => [ + 'type' => Type::id(), + 'errorCodes' => BookError::class, + 'validate' => function ($bookId) { + return empty($bookId) ? [BookError::required, 'Invalid Book Id'] : 0; + }, + ], + ], + 'resolve' => static function ($value): bool { + return (bool)$value; + }, + ]), + ]; + }, + ]), + ]); + + $res = GraphQL::executeQuery( + $schema, + Utils::nowdoc(' + mutation UpdateBook( + $bookId:ID + ) { + updateBook (bookId: $bookId) { + __valid + bookId { + __code + __msg + } + __result + } + } + '), + [], + null, + ['bookId' => null] + ); + + static::assertEmpty($res->errors); + static::assertEquals('required', $res->data['updateBook']['bookId']['__code']); + static::assertEquals('Invalid Book Id', $res->data['updateBook']['bookId']['__msg']); + } +} diff --git a/tests/Type/ValidatedFieldDefinition/GeneratedCodeTypeTest.php b/tests/Type/ValidatedFieldDefinition/GeneratedCodeTypeTest.php deleted file mode 100644 index 1929327..0000000 --- a/tests/Type/ValidatedFieldDefinition/GeneratedCodeTypeTest.php +++ /dev/null @@ -1,125 +0,0 @@ - new ObjectType(['name' => 'Query', 'fields' => []]), - 'mutation' => new ObjectType([ - 'name' => 'Mutation', - 'fields' => function () { - return [ - 'updateBook' => new ValidatedFieldDefinition([ - 'name' => 'updateBook', - 'type' => Type::boolean(), - 'args' => [ - 'bookId' => [ - 'type' => Type::id(), - 'validate' => function ($bookId) { - return empty($bookId) ? 1 : 0; - }, - ], - ], - 'resolve' => static function ($value): bool { - return (bool) $value; - }, - ]), - ]; - }, - ]), - ]); - - $res = GraphQL::executeQuery( - $schema, - Utils::nowdoc(' - mutation UpdateBook( - $bookId:ID - ) { - updateBook (bookId: $bookId) { - valid - suberrors { - bookId { - code - msg - } - } - result - } - } - '), - [], - null, - ['bookId' => null] - ); - - static::assertEmpty($res->errors); - static::assertEquals($res->data['updateBook']['suberrors']['bookId']['code'], 1); - } - - public function testStringCodeType(): void - { - $schema = new Schema([ - 'query' => new ObjectType(['name' => 'Query', 'fields' => []]), - 'mutation' => new ObjectType([ - 'name' => 'Mutation', - 'fields' => function () { - return [ - 'updateBook' => new ValidatedFieldDefinition([ - 'name' => 'updateBook', - 'type' => Type::boolean(), - 'args' => [ - 'bookId' => [ - 'type' => Type::id(), - 'validate' => function ($bookId) { - return empty($bookId) ? [1, 'Invalid Book Id'] : 0; - }, - ], - ], - 'resolve' => static function ($value): bool { - return (bool) $value; - }, - ]), - ]; - }, - ]), - ]); - - $res = GraphQL::executeQuery( - $schema, - Utils::nowdoc(' - mutation UpdateBook( - $bookId:ID - ) { - updateBook (bookId: $bookId) { - valid - suberrors { - bookId { - code - msg - } - } - result - } - } - '), - [], - null, - ['bookId' => null] - ); - - static::assertEmpty($res->errors); - static::assertEquals(1, $res->data['updateBook']['suberrors']['bookId']['code']); - static::assertEquals('Invalid Book Id', $res->data['updateBook']['suberrors']['bookId']['msg']); - } -} diff --git a/tests/Type/ValidatedFieldDefinition/InputObjectValidation.php b/tests/Type/ValidatedFieldDefinition/InputObjectValidation.php index cc5f50f..fa4ed30 100644 --- a/tests/Type/ValidatedFieldDefinition/InputObjectValidation.php +++ b/tests/Type/ValidatedFieldDefinition/InputObjectValidation.php @@ -1,14 +1,15 @@ 'updateBook', 'type' => Type::boolean(), 'args' => [ - 'bookAttributes' => [ - 'type' => function () { // lazy load - return new InputObjectType([ - 'name' => 'BookAttributes', - 'fields' => [ - 'title' => [ - 'type' => Type::string(), - 'description' => 'Enter a book title, no more than 10 characters in length', - 'validate' => static function (string $title) { - if (\strlen($title) > 10) { - return [1, 'book title must be less than 10 chaacters']; - } - - return 0; - }, - ], - 'author' => [ - 'type' => Type::id(), - 'description' => 'Provide a valid author id', - 'validate' => function (string $authorId) { - if (! isset($this->data['people'][$authorId])) { - return [1, 'We have no record of that author']; - } + 'title' => [ + 'type' => Type::string(), + 'description' => 'Enter a book title, no more than 10 characters in length', + 'validate' => static function (string $title) { + if (\strlen($title) > 10) { + return [1, 'book title must be less than 10 characters']; + } - return 0; - }, - ], - ], - ]); + return 0; }, ], - ], - 'resolve' => static function ($value): bool { - return ! $value; - }, - ]), - Utils::nowdoc(' - mutation UpdateBook( - $bookAttributes: BookAttributes - ) { - updateBook ( - bookAttributes: $bookAttributes - ) { - valid - suberrors { - bookAttributes { - title { - code - msg - } - author { - code - msg - } + 'author' => [ + 'type' => Type::id(), + 'description' => 'Provide a valid author id', + 'validate' => function (string $authorId) { + if (!isset($this->data['people'][$authorId])) { + return [1, 'We have no record of that author']; } - } - result - } - } - '), - [ - 'bookAttributes' => [ - 'title' => 'The Catcher in the Rye', - 'author' => 4, - ], - ], - [ - 'valid' => false, - 'suberrors' => [ - 'bookAttributes' => [ - 'title' => [ - 'code' => 1, - 'msg' => 'book title must be less than 10 chaacters', - ], - 'author' => [ - 'code' => 1, - 'msg' => 'We have no record of that author', - ], - ], - ], - 'result' => null, - ] - ); - } - public function testInputObjectSuberrorsValidationOnSelf(): void - { - $this->_checkValidation( - new ValidatedFieldDefinition([ - 'name' => 'updateBook', - 'type' => Type::boolean(), - 'args' => [ - 'bookAttributes' => [ - 'validate' => static function ($atts) { return 0; }, - 'type' => new InputObjectType([ - 'name' => 'BookAttributes', - 'fields' => [ - 'title' => [ - 'type' => Type::string(), - 'description' => 'Enter a book title, no more than 10 characters in length', - 'validate' => static function (string $title) { - if (\strlen($title) > 10) { - return [1, 'book title must be less than 10 characters']; - } - - return 0; - }, - ], - 'author' => [ - 'type' => Type::id(), - 'description' => 'Provide a valid author id', - 'validate' => function (string $authorId) { - if (! isset($this->data['people'][$authorId])) { - return [1, 'We have no record of that author']; - } - - return 0; - }, - ], - ], - ]), ], ], 'resolve' => static function ($value): bool { - return ! $value; + return !$value; }, ]), Utils::nowdoc(' - mutation UpdateBook( - $bookAttributes: BookAttributes - ) { + mutation UpdateBook($title: String, $author: ID) { updateBook ( - bookAttributes: $bookAttributes + author: $author, title: $title ) { - valid - suberrors { - bookAttributes { - suberrors { - title { - code - msg - } - author { - code - msg - } - } - } + __valid + title { + __code + __msg + } + author { + __code + __msg } - result + __result } } '), [ - 'bookAttributes' => [ - 'title' => 'The Catcher in the Rye', - 'author' => 4, - ], + 'title' => 'The Catcher in the Rye', + 'author' => 4, ], [ - 'valid' => false, - 'suberrors' => [ - 'bookAttributes' => [ - 'suberrors' => [ - 'title' => [ - 'code' => 1, - 'msg' => 'book title must be less than 10 characters', - ], - 'author' => [ - 'code' => 1, - 'msg' => 'We have no record of that author', - ], - ], - ], + '__valid' => false, + 'title' => [ + '__code' => 1, + '__msg' => 'book title must be less than 10 characters', + ], + 'author' => [ + '__code' => 1, + '__msg' => 'We have no record of that author', ], - 'result' => null, + '__result' => null, ] ); } - public function testListOfInputObjectSuberrorsValidationOnChildField(): void + public function testInputObjectValidationOnSelfFail(): void { $this->_checkValidation( new ValidatedFieldDefinition([ 'name' => 'updateBook', 'type' => Type::boolean(), + 'validate' => static function ($info) { + if (!empty($info['title']) && empty($info['author'])) { + return [1, "If title is set, then author is required"]; + } + return 0; + }, 'args' => [ - 'bookAttributes' => [ - 'validate' => static function () { + 'title' => [ + 'type' => Type::string(), + 'description' => 'Enter a book title, no more than 10 characters in length', + 'validate' => static function (string $title) { + if (\strlen($title) > 10) { + return [1, 'book title must be less than 10 characters']; + } + + return 0; }, - 'type' => Type::listOf(new InputObjectType([ - 'name' => 'BookAttributes', - 'fields' => [ - 'title' => [ - 'type' => Type::string(), - 'description' => 'Enter a book title, no more than 10 characters in length', - 'validate' => static function (string $title) { - if (\strlen($title) > 10) { - return [1, 'book title must be less than 10 characters']; - } + ], + 'author' => [ + 'type' => Type::id(), + 'description' => 'Provide a valid author id', + 'validate' => function (string $authorId) { + if (!isset($this->data['people'][$authorId])) { + return [1, 'We have no record of that author']; + } - return 0; - }, - ], - ], - ])), + return 0; + }, ], ], 'resolve' => static function ($value): bool { - return ! $value; + return !$value; }, ]), Utils::nowdoc(' - mutation UpdateBook( - $bookAttributes: [BookAttributes] - ) { + mutation UpdateBook($title: String, $author: ID) { updateBook ( - bookAttributes: $bookAttributes + author: $author, title: $title ) { - valid - suberrors { - bookAttributes { - suberrors { - title { - code - msg - } - } - } + __valid + __code + __msg + title { + __code + __msg } - result + author { + __code + __msg + } + __result } } '), [ - 'bookAttributes' => [[ - 'title' => 'The Catcher in the Rye', - ]], + 'title' => 'The Catcher in the Rye', + 'author' => '', ], [ - 'valid' => false, - 'suberrors' => [ - 'bookAttributes' => [ - [ - 'suberrors' => [ - 'title' => [ - 'code' => 1, - 'msg' => 'book title must be less than 10 characters', - ], - ], - ], - ], + '__valid' => false, + '__code' => 1, + '__msg' => "If title is set, then author is required", + 'title' => [ + '__code' => 1, + '__msg' => 'book title must be less than 10 characters', + ], + 'author' => [ + '__code' => 1, + '__msg' => 'We have no record of that author', ], - 'result' => null, + '__result' => null, ] ); } diff --git a/tests/Type/ValidatedFieldDefinition/ListOfInputObjectValidation.php b/tests/Type/ValidatedFieldDefinition/ListOfInputObjectValidation.php index a2e8f08..4f135f4 100644 --- a/tests/Type/ValidatedFieldDefinition/ListOfInputObjectValidation.php +++ b/tests/Type/ValidatedFieldDefinition/ListOfInputObjectValidation.php @@ -1,17 +1,16 @@ schema = new Schema([ - 'query' => new ObjectType(['name' => 'Query', 'fields' => []]), - 'mutation' => new ObjectType([ - 'name' => 'Mutation', - 'fields' => function () { - return [ - 'updateBooks' => new ValidatedFieldDefinition([ - 'name' => 'updateBooks', - 'type' => Type::boolean(), - 'args' => [ - 'bookAttributes' => [ - 'type' => Type::listOf(new InputObjectType([ - 'name' => 'BookAttributes', - 'fields' => [ - 'title' => [ - 'type' => Type::string(), - 'description' => 'Enter a book title, no more than 10 characters in length', - 'validate' => static function (string $title) { - if (\strlen($title) > 10) { - return [1, 'book title must be less than 10 chaacters']; - } + $this->_checkValidation( + new ValidatedFieldDefinition([ + 'name' => 'updateBooks', + 'type' => Type::boolean(), + 'args' => [ + 'bookAttributes' => [ + 'type' => Type::listOf(new InputObjectType([ + 'name' => 'BookAttributes', + 'fields' => [ + 'title' => [ + 'type' => Type::string(), + 'description' => 'Enter a book title, no more than 10 characters in length', + 'validate' => static function (string $title) { + if (\strlen($title) > 5) { + return [1, 'book title must be less than 5 characters']; + } - return 0; - }, - ], - 'author' => [ - 'type' => Type::id(), - 'description' => 'Provide a valid author id', - 'validate' => function (string $authorId) { - if (! isset($this->data['people'][$authorId])) { - return [1, 'We have no record of that author']; - } + return 0; + }, + ], + 'author' => [ + 'type' => Type::id(), + 'description' => 'Provide a valid author id', + 'validate' => function (string $authorId) { + if (!isset($this->data['people'][$authorId])) { + return [1, 'We have no record of that author']; + } - return 0; - }, - ], - ], - ])), + return 0; + }, ], ], - 'resolve' => static function ($value): bool { - return (bool) $value; - }, - ]), - ]; + ])), + ], + ], + 'resolve' => static function ($value): bool { + return (bool)$value; }, ]), - ]); - } - - public function testValidationFail(): void - { - $res = GraphQL::executeQuery( - $this->schema, Utils::nowdoc(' mutation UpdateBooks( $bookAttributes: [BookAttributes] @@ -88,24 +72,20 @@ public function testValidationFail(): void updateBooks ( bookAttributes: $bookAttributes ) { - valid - result - suberrors { - bookAttributes { - suberrors { - title { - code - msg - } + __valid + __result + bookAttributes { + items { + title { + __code + __msg } - path + __path } } } } '), - [], - null, [ 'bookAttributes' => [ [ @@ -113,33 +93,30 @@ public function testValidationFail(): void 'author' => 3, ], [ - 'title' => '', - 'author' => '', + 'title' => 'Ubik', + 'author' => '-2', ], ], - ] - ); - - static::assertEmpty($res->errors); - - static::assertEquals( + ], [ - 'valid' => false, - 'result' => null, - 'suberrors' => [ - 'bookAttributes' => [ + '__valid' => false, + '__result' => null, + 'bookAttributes' => [ + 'items' => [ [ - 'suberrors' => [ - 'title' => null, + 'title' => [ + '__code' => 1, + '__msg' => 'book title must be less than 5 characters', ], - 'path' => [1], + '__path' => [0] ], - ], - ], - ], - $res->data['updateBooks'] + [ + 'title' => null, + '__path' => [1] + ], + ] + ] + ] ); - - static::assertFalse($res->data['updateBooks']['valid']); } } diff --git a/tests/Type/ValidatedFieldDefinition/ListOfScalarValidationTest.php b/tests/Type/ValidatedFieldDefinition/ListOfScalarValidationTest.php index b2185da..1fb11c2 100644 --- a/tests/Type/ValidatedFieldDefinition/ListOfScalarValidationTest.php +++ b/tests/Type/ValidatedFieldDefinition/ListOfScalarValidationTest.php @@ -1,228 +1,68 @@ schema = new Schema([ - 'query' => new ObjectType(['name' => 'Query', 'fields' => []]), - 'mutation' => new ObjectType([ - 'name' => 'Mutation', - 'fields' => static function () { - return [ - 'setPhoneNumbers' => new ValidatedFieldDefinition([ - 'name' => 'setPhoneNumbers', - 'type' => Type::boolean(), - 'validate' => static function (array $args) { - if (\count($args['phoneNumbers']) < 1) { - return [1, 'You must submit at least one list of numbers']; - } - - return 0; - }, - 'args' => [ - 'phoneNumbers' => [ - 'type' => Type::listOf(Type::listOf(Type::string())), - 'validate' => static function ($phoneNumber) { - $res = \preg_match('/^[0-9\-]+$/', $phoneNumber) === 1; - - return ! $res ? [1, 'That does not seem to be a valid phone number'] : 0; - }, - ], - ], - 'resolve' => static function (array $phoneNumbers): bool { - return ! empty($phoneNumbers); - }, - ]), - ]; - }, - ]), - ]); - } - - public function testItemsValidationOnWrappedTypeFail(): void - { - $res = GraphQL::executeQuery( - $this->schema, - Utils::nowdoc(' - mutation SetPhoneNumbers( - $phoneNumbers: [[String]] - ) { - setPhoneNumbers ( phoneNumbers: $phoneNumbers ) { - valid - suberrors { - phoneNumbers { - path - code - msg - } - } - result - } - } - '), - [], - null, - [ - 'phoneNumbers' => [ - [ - '123-4567', - 'xxx456-7890xxx', - '555-whoops', - ], - ], - ] - ); - - static::assertEquals( - [ - 'valid' => false, - 'suberrors' => [ + $this->_checkValidation( + new ValidatedFieldDefinition([ + 'name' => 'savePhoneNumbers', + 'type' => Type::boolean(), + 'args' => [ 'phoneNumbers' => [ - [ - 'path' => [ - 0, - 1, - ], - 'code' => 1, - 'msg' => 'That does not seem to be a valid phone number', - ], - [ - 'path' => [ - 0, - 2, - ], - 'code' => 1, - 'msg' => 'That does not seem to be a valid phone number', - ], + 'type' => Type::listOf(Type::string()), + 'description' => "Enter a list of names. We'll validate each one", + 'items' => ['validate' => static function ($value) { + return strlen($value) <= 7 ? 0 : 1; + }] ], ], - 'result' => null, - ], - $res->data['setPhoneNumbers'] - ); - - static::assertEmpty($res->errors); - static::assertFalse($res->data['setPhoneNumbers']['valid']); - } - - public function testItemsValidationOnSelfFail(): void - { - $res = GraphQL::executeQuery( - $this->schema, - Utils::nowdoc(' - mutation SetPhoneNumbers( - $phoneNumbers: [[String]] - ) { - setPhoneNumbers ( phoneNumbers: $phoneNumbers ) { - valid - code - msg - suberrors { - phoneNumbers { - code - msg - path - } - } - result - } - } - '), - [], - null, - [ - 'phoneNumbers' => [], - ] - ); - - static::assertEquals( - [ - 'valid' => false, - 'code' => 1, - 'msg' => 'You must submit at least one list of numbers', - 'suberrors' => null, - 'result' => null, - ], - $res->data['setPhoneNumbers'] - ); - - static::assertEmpty($res->errors); - static::assertFalse($res->data['setPhoneNumbers']['valid']); - } - - public function testListOfValidationFail(): void - { - $res = GraphQL::executeQuery( - $this->schema, + 'resolve' => static function ($value): bool { + return true; + }, + ]), Utils::nowdoc(' - mutation SetPhoneNumbers( - $phoneNumbers: [[String]] + mutation SavePhoneNumbers($phoneNumbers: [String]) { + savePhoneNumbers ( + phoneNumbers: $phoneNumbers ) { - setPhoneNumbers ( phoneNumbers: $phoneNumbers ) { - valid - suberrors { - phoneNumbers { - code - msg - path + __valid + phoneNumbers { + items { + __code + __msg + __path } } - result + __result } } '), - [], - null, [ 'phoneNumbers' => [ - [], - [ - '123-4567', - 'xxx-7890', - '321-1234', - ], - ], - ] - ); - - static::assertEmpty($res->errors); - static::assertEquals( - [ - 'valid' => false, - 'suberrors' => [ - 'phoneNumbers' => [ - [ - 'code' => 1, - 'msg' => 'That does not seem to be a valid phone number', - 'path' => [ - 0 => 1, - 1 => 1, - ], - ], - ], + '1', + '2', + '3' ], - 'result' => null, ], - $res->data['setPhoneNumbers'] + [ + '__valid' => true, + '__result' => null, + 'phoneNumbers' => null + ] ); - - static::assertFalse($res->data['setPhoneNumbers']['valid']); } } diff --git a/tests/Type/ValidatedFieldDefinition/NamelessDef.php b/tests/Type/ValidatedFieldDefinition/NamelessDef.php index 68d6f32..00943f7 100644 --- a/tests/Type/ValidatedFieldDefinition/NamelessDef.php +++ b/tests/Type/ValidatedFieldDefinition/NamelessDef.php @@ -1,8 +1,8 @@ 1] ); - static::assertTrue($res->data['updateBook']['valid']); + static::assertTrue($res->data['updateBook']['__valid']); } public function testNonNullScalarValidationFail(): void @@ -135,14 +131,12 @@ public function testNonNullScalarValidationFail(): void $bookId:ID! ) { updateBook (bookId: $bookId) { - valid - suberrors { - bookId { - code - msg - } + __valid + bookId { + __code + __msg } - result { + __result { title } } @@ -154,6 +148,6 @@ public function testNonNullScalarValidationFail(): void ); static::assertEmpty($res->errors); - static::assertFalse($res->data['updateBook']['valid']); + static::assertFalse($res->data['updateBook']['__valid']); } } diff --git a/tests/Type/ValidatedFieldDefinition/RequiredFieldsValidation.php b/tests/Type/ValidatedFieldDefinition/RequiredFieldsValidation.php index e8b5f64..6bccfe9 100644 --- a/tests/Type/ValidatedFieldDefinition/RequiredFieldsValidation.php +++ b/tests/Type/ValidatedFieldDefinition/RequiredFieldsValidation.php @@ -1,14 +1,19 @@ _checkValidation( new ValidatedFieldDefinition([ @@ -31,63 +36,39 @@ public function testInputObjectValidationOnFieldFail(): void return new InputObjectType([ 'name' => 'BookAttributes', 'fields' => [ - 'title' => [ - 'type' => Type::string(), - 'description' => 'Enter a book title, no more than 10 characters in length', - 'validate' => static function (string $title) { - if (\strlen($title) > 10) { - return [1, 'book title must be less than 10 characters']; - } - - return 0; - }, - ], + // basic required functionality 'foo' => [ 'type' => Type::string(), 'description' => 'Provide a foo', 'required' => true, - 'validate' => function (string $foo) { - if (strlen($foo) < 10) { - return [1, 'foo must be more than 10 characters']; - } - - return 0; - }, ], + + // custom required response (with [int, string]) 'bar' => [ 'type' => Type::string(), 'description' => 'Provide a bar', 'required' => [1, 'Oh, we absolutely must have a bar'], - 'validate' => function (string $foo) { - if (strlen($foo) < 10) { - return [1, 'bar must be more than 10 characters!']; - } - - return 0; - }, ], + + // required callback 'naz' => [ 'type' => Type::string(), 'description' => 'Provide a naz', - 'required' => static fn () => true, - 'validate' => function (string $naz) { - if (strlen($naz) < 10) { - return [1, 'naz must be more than 10 characters!']; - } + 'required' => static fn() => true, + ], - return 0; - } + // custom required response (with [enum, string]) + 'dingus' => [ + 'type' => Type::string(), + 'errorCodes' => DingusError::class, + 'description' => 'Provide a bar', + 'required' => [DingusError::dingusRequired, 'Make with the dingus'], ], - 'author' => [ - 'type' => Type::id(), - 'description' => 'Provide a valid author id', - 'validate' => function (string $authorId) { - if (! isset($this->data['people'][$authorId])) { - return [1, 'We have no record of that author']; - } - return 0; - }, + // list of scalar + 'gadgets' => [ + 'type' => Type::listOf(Type::string()), + 'required' => true, ], ], ]); @@ -95,7 +76,7 @@ public function testInputObjectValidationOnFieldFail(): void ], ], 'resolve' => static function ($value): bool { - return ! $value; + return !$value; }, ]), Utils::nowdoc(' @@ -105,73 +86,67 @@ public function testInputObjectValidationOnFieldFail(): void updateBook ( bookAttributes: $bookAttributes ) { - valid - suberrors { - bookAttributes { - title { - code - msg - } - author { - code - msg - } - foo { - code - msg - } - bar { - code - msg - } - naz { - code - msg - } + __valid + bookAttributes { + foo { + __code + __msg + } + bar { + __code + __msg + } + naz { + __code + __msg + } + dingus { + __code + __msg + } + gadgets { + __code + __msg } } - result + __result } } '), [ 'bookAttributes' => [ - 'title' => 'The Catcher in the Rye', - 'author' => 4, ], ], [ - 'valid' => false, - 'suberrors' => [ - 'bookAttributes' => [ - 'title' => [ - 'code' => 1, - 'msg' => 'book title must be less than 10 characters', - ], - 'foo' => [ - 'code' => 1, - 'msg' => 'foo is required', - ], - 'bar' => [ - 'code' => 1, - 'msg' => 'Oh, we absolutely must have a bar', - ], - 'author' => [ - 'code' => 1, - 'msg' => 'We have no record of that author', - ], - 'naz' => [ - 'code' => 1, - 'msg' => 'naz is required', - ], + '__valid' => false, + 'bookAttributes' => [ + 'foo' => [ + '__code' => 1, + '__msg' => 'foo is required', + ], + 'bar' => [ + '__code' => 1, + '__msg' => 'Oh, we absolutely must have a bar', + ], + 'naz' => [ + '__code' => 1, + '__msg' => 'naz is required', + ], + 'dingus' => [ + '__code' => 'dingusRequired', + '__msg' => 'Make with the dingus', + ], + 'gadgets' => [ + '__code' => 1, + '__msg' => 'gadgets is required', ], ], - 'result' => null, + '__result' => null, ] ); } - public function testInputObjectSuberrorsValidationOnSelf(): void + public function testRequiredFailByEmptyValue(): void { $this->_checkValidation( new ValidatedFieldDefinition([ @@ -179,40 +154,51 @@ public function testInputObjectSuberrorsValidationOnSelf(): void 'type' => Type::boolean(), 'args' => [ 'bookAttributes' => [ - 'validate' => static function ($atts) { - return 0; - }, - 'type' => new InputObjectType([ - 'name' => 'BookAttributes', - 'fields' => [ - 'title' => [ - 'type' => Type::string(), - 'description' => 'Enter a book title, no more than 10 characters in length', - 'validate' => static function (string $title) { - if (\strlen($title) > 10) { - return [1, 'book title must be less than 10 characters']; - } + 'type' => function () { // lazy load + return new InputObjectType([ + 'name' => 'BookAttributes', + 'fields' => [ + // basic required functionality + 'foo' => [ + 'type' => Type::string(), + 'description' => 'Provide a foo', + 'required' => true, + ], - return 0; - }, - ], - 'author' => [ - 'type' => Type::id(), - 'description' => 'Provide a valid author id', - 'validate' => function (string $authorId) { - if (! isset($this->data['people'][$authorId])) { - return [1, 'We have no record of that author']; - } + // custom required response (with [int, string]) + 'bar' => [ + 'type' => Type::string(), + 'description' => 'Provide a bar', + 'required' => [1, 'Oh, we absolutely must have a bar'], + ], + + // required callback + 'naz' => [ + 'type' => Type::string(), + 'description' => 'Provide a naz', + 'required' => static fn() => true, + ], + + // custom required response (with [enum, string]) + 'dingus' => [ + 'type' => Type::string(), + 'errorCodes' => DingusError::class, + 'description' => 'Provide a bar', + 'required' => [DingusError::dingusRequired, 'Make with the dingus'], + ], - return 0; - }, + // list of scalar + 'gadgets' => [ + 'type' => Type::listOf(Type::string()), + 'required' => true, + ], ], - ], - ]), + ]); + }, ], ], 'resolve' => static function ($value): bool { - return ! $value; + return !$value; }, ]), Utils::nowdoc(' @@ -222,126 +208,67 @@ public function testInputObjectSuberrorsValidationOnSelf(): void updateBook ( bookAttributes: $bookAttributes ) { - valid - suberrors { - bookAttributes { - suberrors { - title { - code - msg - } - author { - code - msg - } - } + __valid + bookAttributes { + foo { + __code + __msg + } + bar { + __code + __msg + } + naz { + __code + __msg + } + dingus { + __code + __msg + } + gadgets { + __code + __msg } } - result + __result } } '), [ 'bookAttributes' => [ - 'title' => 'The Catcher in the Rye', - 'author' => 4, + 'foo' => '', + 'bar' => null, + 'naz' => '', + 'dingus' => '', + 'gadgets' => [] ], ], [ - 'valid' => false, - 'suberrors' => [ - 'bookAttributes' => [ - 'suberrors' => [ - 'title' => [ - 'code' => 1, - 'msg' => 'book title must be less than 10 characters', - ], - 'author' => [ - 'code' => 1, - 'msg' => 'We have no record of that author', - ], - ], + '__valid' => false, + 'bookAttributes' => [ + 'foo' => [ + '__code' => 1, + '__msg' => 'foo is required', ], - ], - 'result' => null, - ] - ); - } - - public function testListOfInputObjectSuberrorsValidationOnChildField(): void - { - $this->_checkValidation( - new ValidatedFieldDefinition([ - 'name' => 'updateBook', - 'type' => Type::boolean(), - 'args' => [ - 'bookAttributes' => [ - 'validate' => static function () { - }, - 'type' => Type::listOf(new InputObjectType([ - 'name' => 'BookAttributes', - 'fields' => [ - 'title' => [ - 'type' => Type::string(), - 'description' => 'Enter a book title, no more than 10 characters in length', - 'validate' => static function (string $title) { - if (\strlen($title) > 10) { - return [1, 'book title must be less than 10 characters']; - } - - return 0; - }, - ], - ], - ])), + 'bar' => [ + '__code' => 1, + '__msg' => 'Oh, we absolutely must have a bar', ], - ], - 'resolve' => static function ($value): bool { - return ! $value; - }, - ]), - Utils::nowdoc(' - mutation UpdateBook( - $bookAttributes: [BookAttributes] - ) { - updateBook ( - bookAttributes: $bookAttributes - ) { - valid - suberrors { - bookAttributes { - suberrors { - title { - code - msg - } - } - } - } - result - } - } - '), - [ - 'bookAttributes' => [[ - 'title' => 'The Catcher in the Rye', - ]], - ], - [ - 'valid' => false, - 'suberrors' => [ - 'bookAttributes' => [ - [ - 'suberrors' => [ - 'title' => [ - 'code' => 1, - 'msg' => 'book title must be less than 10 characters', - ], - ], - ], + 'naz' => [ + '__code' => 1, + '__msg' => 'naz is required', + ], + 'dingus' => [ + '__code' => 'dingusRequired', + '__msg' => 'Make with the dingus', + ], + 'gadgets' => [ + '__code' => 1, + '__msg' => 'gadgets is required', ], ], - 'result' => null, + '__result' => null, ] ); } diff --git a/tests/Type/ValidatedFieldDefinition/ScalarValidationTest.php b/tests/Type/ValidatedFieldDefinition/ScalarValidationTest.php index 2c43664..4d8e15d 100644 --- a/tests/Type/ValidatedFieldDefinition/ScalarValidationTest.php +++ b/tests/Type/ValidatedFieldDefinition/ScalarValidationTest.php @@ -1,13 +1,13 @@ static function ($value): bool { - return (bool) $value; + return (bool)$value; }, ]), ]; @@ -108,14 +108,12 @@ public function testNullableScalarValidationOnNullValueSuccess(): void $bookId:ID ) { updateBook (bookId: $bookId) { - valid - suberrors { - bookId { - code - msg - } + __valid + bookId { + __code + __msg } - result { + __result { title } } @@ -127,6 +125,6 @@ public function testNullableScalarValidationOnNullValueSuccess(): void ); static::assertEmpty($res->errors); - static::assertFalse($res->data['updateBook']['valid']); + static::assertFalse($res->data['updateBook']['__valid']); } } diff --git a/tests/Utils.php b/tests/Utils.php index bdd2e71..f031603 100644 --- a/tests/Utils.php +++ b/tests/Utils.php @@ -1,6 +1,6 @@