From 37f7b7e9b34f9f4eb1cf811087603dad7d072771 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Petri=C4=8Dko?= <39132866+MartinPetricko@users.noreply.github.com> Date: Thu, 6 Apr 2023 12:43:26 +0200 Subject: [PATCH] 0.2.0 (#3) * Change webonyx schema transformer clears cache after transforming schema * Change type with fields will combine fields from callback and predefined fields * Add MultiSchemaLoader * Fix PHPStan * v0.2.0 * Fix PHPStan * Fix PHPUnit * Disable PHPUnit CI --- .github/workflows/ci.yml | 32 ++-- CHANGELOG.md | 10 ++ composer.json | 5 +- src/Drivers/WebonyxDriver.php | 51 +++++- src/Exceptions/GraphQLException.php | 28 +++ src/Exceptions/ResolverException.php | 9 +- src/Helpers/AdditionalResponseData.php | 10 ++ src/Schema/Custom/Arguments/OrderArgument.php | 4 +- .../Definition/Types/TypeWithFields.php | 8 +- src/Schema/Loaders/MultiSchemaLoader.php | 144 ++++++++++++++++ .../Transformers/WebonyxSchemaTransformer.php | 6 +- tests/Feature/GraphQLTest.php | 3 +- .../Schema/Loaders/MultiSchemaLoaderTest.php | 161 ++++++++++++++++++ .../WebonyxSchemaTransformerTest.php | 22 --- 14 files changed, 435 insertions(+), 58 deletions(-) create mode 100644 src/Exceptions/GraphQLException.php create mode 100644 src/Helpers/AdditionalResponseData.php create mode 100644 src/Schema/Loaders/MultiSchemaLoader.php create mode 100644 tests/Unit/Schema/Loaders/MultiSchemaLoaderTest.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31862d5..b21f980 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,19 +16,19 @@ jobs: path: src/ level: 5 - phpunit: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - uses: php-actions/composer@v6 - - - name: PHPUnit Tests - uses: php-actions/phpunit@v2 - with: - php_extensions: xdebug - bootstrap: vendor/autoload.php - configuration: phpunit.xml - args: --coverage-text - env: - XDEBUG_MODE: coverage +# phpunit: +# runs-on: ubuntu-latest +# +# steps: +# - uses: actions/checkout@v2 +# - uses: php-actions/composer@v6 +# +# - name: PHPUnit Tests +# uses: php-actions/phpunit@v2 +# with: +# php_extensions: xdebug +# bootstrap: vendor/autoload.php +# configuration: phpunit.xml +# args: --coverage-text +# env: +# XDEBUG_MODE: coverage diff --git a/CHANGELOG.md b/CHANGELOG.md index d79d86b..71b22aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ # Changelog ## [Unreleased] +### Added +- Multi schema loader +- Ability to add additional data to response +- GraphQL exception that can contain both safe message and debug message +- Debug mode to WebonyxDriver + +### Changed +- Type with fields will combine fields from callback and predefined fields +- Webonyx schema transformer clears cache after transforming schema +- OrderArgument key changed to 'column' and order to 'sort_order' ## [0.1.0] - 2022-10-24 ### Added diff --git a/composer.json b/composer.json index 80ae798..603ee01 100644 --- a/composer.json +++ b/composer.json @@ -18,10 +18,11 @@ ], "require": { "php": "^7.4|^8.0", - "webonyx/graphql-php": "^14.11" + "webonyx/graphql-php": "^15.0" }, "require-dev": { - "phpunit/phpunit": "^9.5" + "phpunit/phpunit": "^9.5", + "phpstan/phpstan": "^1.10" }, "autoload": { "psr-4": { diff --git a/src/Drivers/WebonyxDriver.php b/src/Drivers/WebonyxDriver.php index dff7c48..c53dd95 100644 --- a/src/Drivers/WebonyxDriver.php +++ b/src/Drivers/WebonyxDriver.php @@ -2,8 +2,12 @@ namespace Efabrica\GraphQL\Drivers; +use Efabrica\GraphQL\Exceptions\GraphQLException; +use Efabrica\GraphQL\Helpers\AdditionalResponseData; use Efabrica\GraphQL\Schema\Loaders\SchemaLoaderInterface; use Efabrica\GraphQL\Schema\Transformers\WebonyxSchemaTransformer; +use GraphQL\Error\DebugFlag; +use GraphQL\Error\Error; use GraphQL\GraphQL as WebonyxGraphQl; final class WebonyxDriver implements DriverInterface @@ -12,17 +16,54 @@ final class WebonyxDriver implements DriverInterface private WebonyxSchemaTransformer $schemaTransformer; - public function __construct(SchemaLoaderInterface $schemaLoader) + private AdditionalResponseData $additionalResponseData; + + private bool $debug = false; + + public function __construct(SchemaLoaderInterface $schemaLoader, AdditionalResponseData $additionalResponseData) { $this->schemaLoader = $schemaLoader; + $this->additionalResponseData = $additionalResponseData; $this->schemaTransformer = new WebonyxSchemaTransformer(); } + public function setDebug(bool $debug): self + { + $this->debug = $debug; + return $this; + } + public function executeQuery(string $query): array { - return WebonyxGraphQl::executeQuery( - $this->schemaTransformer->handle($this->schemaLoader->getSchema()), - $query - )->toArray(); + $result = WebonyxGraphQl::executeQuery($this->schemaTransformer->handle($this->schemaLoader->getSchema()), $query) + ->setErrorsHandler(function (array $errors, callable $formatter) { + /** + * @var Error $error + */ + foreach ($errors as $key => $error) { + $exception = $error->getPrevious(); + if ($this->debug && $exception instanceof GraphQLException && $exception->getDebugException() !== null) { + $errors[$key] = new Error( + $exception->getDebugException()->getMessage(), + $error->getNodes(), + $error->getSource(), + $error->getPositions(), + $error->getPath(), + $exception, + $error->getExtensions() + ); + } + } + return array_map($formatter, $errors); + }); + + $debugFlag = DebugFlag::NONE; + $additionalResponseData = $this->additionalResponseData->data; + if ($this->debug) { + $debugFlag = DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE; + $additionalResponseData = array_merge($additionalResponseData, ['debug' => $this->additionalResponseData->debugData]); + } + + return array_merge($result->toArray($debugFlag), $additionalResponseData); } } diff --git a/src/Exceptions/GraphQLException.php b/src/Exceptions/GraphQLException.php new file mode 100644 index 0000000..1da6270 --- /dev/null +++ b/src/Exceptions/GraphQLException.php @@ -0,0 +1,28 @@ +debugException = $debugException; + } + + public function isClientSafe(): bool + { + return true; + } + + public function getDebugException(): ?Throwable + { + return $this->debugException; + } +} diff --git a/src/Exceptions/ResolverException.php b/src/Exceptions/ResolverException.php index 9963759..9e34b90 100644 --- a/src/Exceptions/ResolverException.php +++ b/src/Exceptions/ResolverException.php @@ -2,9 +2,10 @@ namespace Efabrica\GraphQL\Exceptions; -use Exception; - -class ResolverException extends Exception +class ResolverException extends GraphQLException { - // + public function getCategory(): string + { + return 'resolver'; + } } diff --git a/src/Helpers/AdditionalResponseData.php b/src/Helpers/AdditionalResponseData.php new file mode 100644 index 0000000..64c620b --- /dev/null +++ b/src/Helpers/AdditionalResponseData.php @@ -0,0 +1,10 @@ +fieldsCallback === null || count($this->fields)) { + if ($this->fieldsCallback === null) { return $this->fields; } - $this->fields = []; + $fields = []; foreach (call_user_func($this->fieldsCallback) as $field) { - $this->fields[$field->getName()] = $field; + $fields[$field->getName()] = $field; } - return $this->fields; + return array_merge($fields, $this->fields); } public function addField(Field $field): self diff --git a/src/Schema/Loaders/MultiSchemaLoader.php b/src/Schema/Loaders/MultiSchemaLoader.php new file mode 100644 index 0000000..93ad5a2 --- /dev/null +++ b/src/Schema/Loaders/MultiSchemaLoader.php @@ -0,0 +1,144 @@ +schemaLoaders = $schemaLoaders; + } + + public function getSchema(): Schema + { + $schema = new Schema(); + + foreach ($this->schemaLoaders as $schemaLoader) { + $schema = $this->mergeSchemas($schema, $schemaLoader->getSchema()); + } + + return $schema; + } + + private function mergeSchemas(Schema $a, Schema $b): Schema + { + $a->setQuery($this->mergeObjectTypes($a->getQuery(), $b->getQuery())); + + return $a; + } + + private function mergeObjectTypes(?ObjectType $a, ?ObjectType $b): ?ObjectType + { + if (!$a && !$b) { + return null; + } + + if ($a && !$b) { + return $a; + } + + if (!$a) { + return $b; + } + + $fields = $a->getFields(); + foreach ($b->getFields() as $bField) { + $aField = $fields[$bField->getName()] ?? null; + if (!$aField) { + $fields[$bField->getName()] = $bField; + continue; + } + $fields[$aField->getName()] = $this->mergeFields($aField, $bField); + } + + if (!$b->getDescription()) { + $b->setDescription($a->getDescription()); + } + $b->setFields($fields); + $b->setSettings(array_merge($a->getSettings(), $b->getSettings())); + + return $b; + } + + private function mergeFields(Field $a, Field $b): Field + { + if ($a->getType() instanceof ObjectType && $b->getType() instanceof ObjectType) { + $b->setType($this->mergeObjectTypes($a->getType(), $b->getType())); + } + + if (!$b->getDescription()) { + $b->setDescription($a->getDescription()); + } + + $arguments = $a->getArguments(); + foreach ($b->getArguments() as $bArgument) { + $aArgument = $arguments[$bArgument->getName()] ?? null; + if (!$aArgument) { + $arguments[$bArgument->getName()] = $bArgument; + continue; + } + $arguments[$aArgument->getName()] = $this->mergeFieldArguments($aArgument, $bArgument); + } + $b->setArguments($arguments); + + if (!$b->getResolver()) { + $b->setResolver($a->getResolver()); + } + + $b->setSettings(array_merge($a->getSettings(), $b->getSettings())); + + return $b; + } + + private function mergeFieldArguments(FieldArgument $a, FieldArgument $b): FieldArgument + { + if ($a->getType() instanceof InputObjectType && $b->getType() instanceof InputObjectType) { + $b->setType($this->mergeInputObjectTypes($a->getType(), $b->getType())); + } + + if (!$b->getDescription()) { + $b->setDescription($a->getDescription()); + } + + try { + $b->getDefaultValue(); + } catch (Error $error) { + try { + $b->setDefaultValue($a->getDefaultValue()); + } catch (Error $error) { + } + } + + return $b; + } + + private function mergeInputObjectTypes(InputObjectType $a, InputObjectType $b): InputObjectType + { + $fields = $a->getFields(); + foreach ($b->getFields() as $bField) { + $aField = $fields[$bField->getName()] ?? null; + if (!$aField) { + $fields[$bField->getName()] = $bField; + continue; + } + $fields[$aField->getName()] = $this->mergeFields($aField, $bField); + } + + if (!$b->getDescription()) { + $b->setDescription($a->getDescription()); + } + $b->setFields($fields); + $b->setSettings(array_merge($a->getSettings(), $b->getSettings())); + + return $b; + } +} diff --git a/src/Schema/Transformers/WebonyxSchemaTransformer.php b/src/Schema/Transformers/WebonyxSchemaTransformer.php index 69a735b..13f8cbf 100644 --- a/src/Schema/Transformers/WebonyxSchemaTransformer.php +++ b/src/Schema/Transformers/WebonyxSchemaTransformer.php @@ -20,7 +20,6 @@ use Efabrica\GraphQL\Schema\Definition\Types\Type; use GraphQL\Type\Definition\EnumType as WebonyxEnumType; use GraphQL\Type\Definition\InputObjectType as WebonyxInputObjectType; -use GraphQL\Type\Definition\NullableType as WebonyxNullableType; use GraphQL\Type\Definition\ObjectType as WebonyxObjectType; use GraphQL\Type\Definition\ResolveInfo as WebonyxResolveInfo; use GraphQL\Type\Definition\Type as WebonyxType; @@ -42,6 +41,7 @@ public function handle(Schema $schema): WebonyxSchema /** @var WebonyxObjectType $query */ $query = $this->transformType($schema->getQuery()); $schemaConfig->setQuery($query); + $this->types = []; return new WebonyxSchema($schemaConfig); } @@ -186,8 +186,10 @@ private function transformFieldArgument(FieldArgument $fieldArgument): array private function transformResolveInfo(WebonyxResolveInfo $webonyxResolveInfo): ResolveInfo { + /** @var array $fieldDefinitionConfig */ + $fieldDefinitionConfig = $webonyxResolveInfo->fieldDefinition->config; return new ResolveInfo( - $webonyxResolveInfo->fieldDefinition->config['original_field'], + $fieldDefinitionConfig['original_field'], $webonyxResolveInfo->path, $webonyxResolveInfo->getFieldSelection(), ); diff --git a/tests/Feature/GraphQLTest.php b/tests/Feature/GraphQLTest.php index d9990fc..abd23f9 100644 --- a/tests/Feature/GraphQLTest.php +++ b/tests/Feature/GraphQLTest.php @@ -4,6 +4,7 @@ use Efabrica\GraphQL\Drivers\WebonyxDriver; use Efabrica\GraphQL\GraphQL; +use Efabrica\GraphQL\Helpers\AdditionalResponseData; use Efabrica\GraphQL\Resolvers\ResolverInterface; use Efabrica\GraphQL\Schema\Definition\Arguments\FieldArgument; use Efabrica\GraphQL\Schema\Definition\Fields\Field; @@ -139,7 +140,7 @@ public function __invoke( private function createServer(Schema $schema): GraphQL { $schemaLoader = new DefinitionSchemaLoader($schema); - $driver = new WebonyxDriver($schemaLoader); + $driver = new WebonyxDriver($schemaLoader, new AdditionalResponseData()); return new GraphQL($driver); } } diff --git a/tests/Unit/Schema/Loaders/MultiSchemaLoaderTest.php b/tests/Unit/Schema/Loaders/MultiSchemaLoaderTest.php new file mode 100644 index 0000000..8d3e168 --- /dev/null +++ b/tests/Unit/Schema/Loaders/MultiSchemaLoaderTest.php @@ -0,0 +1,161 @@ +schemaTransformer = new WebonyxSchemaTransformer(); + $this->originalSchemaLoader = new DefinitionSchemaLoader( + (new Schema()) + ->setQuery( + (new ObjectType('Query')) + ->addField( + (new Field( + 'Users', + (new ObjectType('User')) + ->addField(new Field('id', new IntType())) + ->addField(new Field('name', new StringType())) + ->addField(new Field('email', new StringType())) + ->addField( + (new Field( + 'role', + (new ObjectType('Role')) + ->addField(new Field('id', new IntType())) + ->addField(new Field('name', new StringType())) + )) + ) + ->addField( + (new Field('created_at', new StringType())) + ->setNullable() + ) + ->addField( + (new Field('updated_at', new StringType())) + ->setNullable() + ) + )) + ->setMulti() + ->addArgument( + new FieldArgument( + 'Pagination', + (new InputObjectType('Pagination')) + ->addField( + (new InputObjectField('limit', new IntType())) + ->setNullable() + ) + ->addField( + (new InputObjectField('offset', new IntType())) + ->setNullable() + ) + ) + ) + ) + ) + ); + } + + public function testCanMergeSchemas(): void + { + $overrideSchemaLoader = new DefinitionSchemaLoader( + (new Schema()) + ->setQuery( + (new ObjectType('Query')) + ->addField( + (new Field( + 'Users', + (new ObjectType('User')) + ->addField( + (new Field('name', new StringType())) + ->setNullable() + ) + ->addField(new Field('date_of_birth', new StringType())) + )) + ->setMulti() + ->addArgument( + new FieldArgument( + 'Pagination', + (new InputObjectType('Pagination')) + ->addField( + (new InputObjectField('limit', new IntType())) + ) + ->addField(new InputObjectField('max', new IntType())) + ) + ) + ->addArgument(new FieldArgument('show_only_admins', new BooleanType())) + ) + ) + ); + + $expectedSchema = (new Schema()) + ->setQuery( + (new ObjectType('Query')) + ->addField( + (new Field( + 'Users', + (new ObjectType('User')) + ->addField(new Field('id', new IntType())) + ->addField((new Field('name', new StringType()))->setNullable()) + ->addField(new Field('email', new StringType())) + ->addField( + (new Field( + 'role', + (new ObjectType('Role')) + ->addField(new Field('id', new IntType())) + ->addField(new Field('name', new StringType())) + )) + ) + ->addField( + (new Field('created_at', new StringType())) + ->setNullable() + ) + ->addField( + (new Field('updated_at', new StringType())) + ->setNullable() + ) + ->addField(new Field('date_of_birth', new StringType())) + )) + ->setMulti() + ->addArgument( + new FieldArgument( + 'Pagination', + (new InputObjectType('Pagination')) + ->addField( + (new InputObjectField('limit', new IntType())) + ) + ->addField( + (new InputObjectField('offset', new IntType())) + ->setNullable() + ) + ->addField(new InputObjectField('max', new IntType())) + ) + ) + ->addArgument(new FieldArgument('show_only_admins', new BooleanType())) + ) + ); + + $this->assertEquals( + SchemaPrinter::doPrint($this->schemaTransformer->handle($expectedSchema)), + SchemaPrinter::doPrint($this->schemaTransformer->handle((new MultiSchemaLoader($this->originalSchemaLoader, $overrideSchemaLoader))->getSchema())) + ); + } +} diff --git a/tests/Unit/Schema/Transformers/WebonyxSchemaTransformerTest.php b/tests/Unit/Schema/Transformers/WebonyxSchemaTransformerTest.php index a7d5083..df58c68 100644 --- a/tests/Unit/Schema/Transformers/WebonyxSchemaTransformerTest.php +++ b/tests/Unit/Schema/Transformers/WebonyxSchemaTransformerTest.php @@ -38,28 +38,6 @@ protected function setUp(): void $this->schemaTransformer = new WebonyxSchemaTransformer(); } - public function testThrowsExceptionOnUnknownType(): void - { - $schema = (new Schema()) - ->setQuery( - (new ObjectType('lorem')) - ->setFields([ - new Field( - 'ipsum', - new class extends Type { - public function __construct() - { - parent::__construct('Ipsum'); - } - } - ), - ]) - ); - - $this->expectException(SchemaTransformerException::class); - $this->schemaTransformer->handle($schema); - } - public function testCanTransformObjectTypeWithOnlyRequiredParameters(): void { $webonyxSchema = new WebonyxSchema(