From f9703f5fa8aada359206bbfae665407161c69828 Mon Sep 17 00:00:00 2001 From: Alan Poulain Date: Wed, 8 Sep 2021 15:59:53 +0200 Subject: [PATCH] feat(graphql): use denormalization groups to allow creation of relation in mutation --- features/graphql/mutation.feature | 19 +- .../Bundle/Resources/config/graphql.xml | 1 + .../Serializer/AbstractItemNormalizer.php | 3 + src/GraphQl/Type/FieldsBuilder.php | 115 +++++------ src/GraphQl/Type/FieldsBuilderInterface.php | 12 +- src/GraphQl/Type/SchemaBuilder.php | 12 +- src/GraphQl/Type/TypeBuilder.php | 30 +-- src/GraphQl/Type/TypeBuilderInterface.php | 3 +- src/GraphQl/Type/TypeConverter.php | 49 ++++- src/GraphQl/Type/TypeConverterInterface.php | 3 +- .../TestBundle/Document/RelatedDummy.php | 2 +- .../TestBundle/Entity/RelatedDummy.php | 2 +- .../TestBundle/GraphQl/Type/TypeConverter.php | 5 +- .../Factory/CollectionResolverFactoryTest.php | 8 +- .../Resolver/Stage/SerializeStageTest.php | 3 +- tests/GraphQl/Type/FieldsBuilderTest.php | 192 +++++++++++------- tests/GraphQl/Type/SchemaBuilderTest.php | 35 +++- tests/GraphQl/Type/TypeBuilderTest.php | 109 ++++++---- tests/GraphQl/Type/TypeConverterTest.php | 57 +++++- 19 files changed, 411 insertions(+), 249 deletions(-) diff --git a/features/graphql/mutation.feature b/features/graphql/mutation.feature index 2ac8de7b7da..0bb5caf44eb 100644 --- a/features/graphql/mutation.feature +++ b/features/graphql/mutation.feature @@ -425,7 +425,7 @@ Feature: GraphQL mutation support And the JSON node "data.createFoo.foo.name" should be equal to "Created without mutation id" And the JSON node "data.createFoo.foo.bar" should be equal to "works" - Scenario: Create an item with a subresource + Scenario: Create an item with a relation to an existing resource Given there are 1 dummy objects with relatedDummy When I send the following GraphQL request: """ @@ -666,15 +666,26 @@ Feature: GraphQL mutation support Scenario: Use serialization groups with relations Given there is 1 dummy object with relatedDummy and its thirdLevel And there is a RelatedDummy with 2 friends + And there is a dummy object with a fourth level relation When I send the following GraphQL request: """ mutation { - updateRelatedDummy(input: {id: "/related_dummies/2", symfony: "laravel", embeddedDummy: "{}", thirdLevel: "/third_levels/1"}) { + updateRelatedDummy(input: { + id: "/related_dummies/2", + symfony: "laravel", + thirdLevel: { + fourthLevel: "/fourth_levels/1" + } + }) { relatedDummy { id symfony thirdLevel { id + fourthLevel { + id + __typename + } __typename } relatedToDummyFriend { @@ -694,8 +705,10 @@ Feature: GraphQL mutation support And the header "Content-Type" should be equal to "application/json" And the JSON node "data.updateRelatedDummy.relatedDummy.id" should be equal to "/related_dummies/2" And the JSON node "data.updateRelatedDummy.relatedDummy.symfony" should be equal to "laravel" - And the JSON node "data.updateRelatedDummy.relatedDummy.thirdLevel.id" should be equal to "/third_levels/1" + And the JSON node "data.updateRelatedDummy.relatedDummy.thirdLevel.id" should be equal to "/third_levels/3" And the JSON node "data.updateRelatedDummy.relatedDummy.thirdLevel.__typename" should be equal to "updateThirdLevelNestedPayload" + And the JSON node "data.updateRelatedDummy.relatedDummy.thirdLevel.fourthLevel.id" should be equal to "/fourth_levels/1" + And the JSON node "data.updateRelatedDummy.relatedDummy.thirdLevel.fourthLevel.__typename" should be equal to "updateFourthLevelNestedPayload" And the JSON node "data.updateRelatedDummy.relatedDummy.relatedToDummyFriend.__typename" should be equal to "updateRelatedToDummyFriendNestedPayloadConnection" And the JSON node "data.updateRelatedDummy.relatedDummy.relatedToDummyFriend.edges[0].node.name" should be equal to "Relation-1" And the JSON node "data.updateRelatedDummy.relatedDummy.relatedToDummyFriend.edges[1].node.name" should be equal to "Relation-2" diff --git a/src/Core/Bridge/Symfony/Bundle/Resources/config/graphql.xml b/src/Core/Bridge/Symfony/Bundle/Resources/config/graphql.xml index e612cbae965..c4494439e01 100644 --- a/src/Core/Bridge/Symfony/Bundle/Resources/config/graphql.xml +++ b/src/Core/Bridge/Symfony/Bundle/Resources/config/graphql.xml @@ -130,6 +130,7 @@ + diff --git a/src/Core/Serializer/AbstractItemNormalizer.php b/src/Core/Serializer/AbstractItemNormalizer.php index df7ab523192..5d06aecf1b2 100644 --- a/src/Core/Serializer/AbstractItemNormalizer.php +++ b/src/Core/Serializer/AbstractItemNormalizer.php @@ -864,6 +864,9 @@ private function createAttributeValue($attribute, $value, $format = null, array $resourceClass = $this->resourceClassResolver->getResourceClass(null, $className); $childContext = $this->createChildContext($context, $attribute, $format); $childContext['resource_class'] = $resourceClass; + if ($this->resourceMetadataFactory instanceof ResourceMetadataCollectionFactoryInterface) { + $childContext['operation'] = $this->resourceMetadataFactory->create($resourceClass)->getOperation(); + } return $this->denormalizeRelation($attribute, $propertyMetadata, $resourceClass, $value, $format, $childContext); } diff --git a/src/GraphQl/Type/FieldsBuilder.php b/src/GraphQl/Type/FieldsBuilder.php index 720fa116d79..f7e392a57bd 100644 --- a/src/GraphQl/Type/FieldsBuilder.php +++ b/src/GraphQl/Type/FieldsBuilder.php @@ -94,15 +94,12 @@ public function getNodeQueryFields(): array /** * {@inheritdoc} */ - public function getItemQueryFields(string $resourceClass, Operation $operation, string $queryName, array $configuration): array + public function getItemQueryFields(string $resourceClass, Operation $operation, array $configuration): array { - $shortName = $operation->getShortName(); - $fieldName = lcfirst('item_query' === $queryName ? $shortName : $queryName.$shortName); - $description = $operation->getDescription(); - $deprecationReason = $operation->getDeprecationReason(); + $fieldName = lcfirst('item_query' === $operation->getName() ? $operation->getShortName() : $operation->getName().$operation->getShortName()); - if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $description, $deprecationReason, new Type(Type::BUILTIN_TYPE_OBJECT, true, $resourceClass), $resourceClass, false, $queryName)) { - $args = $this->resolveResourceArgs($configuration['args'] ?? [], $queryName, $shortName); + if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $operation->getDescription(), $operation->getDeprecationReason(), new Type(Type::BUILTIN_TYPE_OBJECT, true, $resourceClass), $resourceClass, false, $operation)) { + $args = $this->resolveResourceArgs($configuration['args'] ?? [], $operation); $configuration['args'] = $args ?: $configuration['args'] ?? ['id' => ['type' => GraphQLType::nonNull(GraphQLType::id())]]; return [$fieldName => array_merge($fieldConfiguration, $configuration)]; @@ -114,15 +111,12 @@ public function getItemQueryFields(string $resourceClass, Operation $operation, /** * {@inheritdoc} */ - public function getCollectionQueryFields(string $resourceClass, Operation $operation, string $queryName, array $configuration): array + public function getCollectionQueryFields(string $resourceClass, Operation $operation, array $configuration): array { - $shortName = $operation->getShortName(); - $fieldName = lcfirst('collection_query' === $queryName ? $shortName : $queryName.$shortName); - $description = $operation->getDescription(); - $deprecationReason = $operation->getDeprecationReason(); + $fieldName = lcfirst('collection_query' === $operation->getName() ? $operation->getShortName() : $operation->getName().$operation->getShortName()); - if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $description, $deprecationReason, new Type(Type::BUILTIN_TYPE_OBJECT, false, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, $resourceClass)), $resourceClass, false, $queryName)) { - $args = $this->resolveResourceArgs($configuration['args'] ?? [], $queryName, $shortName); + if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $operation->getDescription(), $operation->getDeprecationReason(), new Type(Type::BUILTIN_TYPE_OBJECT, false, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, $resourceClass)), $resourceClass, false, $operation)) { + $args = $this->resolveResourceArgs($configuration['args'] ?? [], $operation); $configuration['args'] = $args ?: $configuration['args'] ?? $fieldConfiguration['args']; return [Inflector::pluralize($fieldName) => array_merge($fieldConfiguration, $configuration)]; @@ -134,19 +128,17 @@ public function getCollectionQueryFields(string $resourceClass, Operation $opera /** * {@inheritdoc} */ - public function getMutationFields(string $resourceClass, Operation $operation, string $mutationName): array + public function getMutationFields(string $resourceClass, Operation $operation): array { $mutationFields = []; - $shortName = $operation->getShortName(); $resourceType = new Type(Type::BUILTIN_TYPE_OBJECT, true, $resourceClass); - $description = $operation->getDescription() ?? ucfirst("{$mutationName}s a $shortName."); - $deprecationReason = $operation->getDeprecationReason(); + $description = $operation->getDescription() ?? ucfirst("{$operation->getName()}s a {$operation->getShortName()}."); - if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $description, $deprecationReason, $resourceType, $resourceClass, false, $mutationName)) { - $fieldConfiguration['args'] += ['input' => $this->getResourceFieldConfiguration(null, null, $deprecationReason, $resourceType, $resourceClass, true, $mutationName)]; + if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $description, $operation->getDeprecationReason(), $resourceType, $resourceClass, false, $operation)) { + $fieldConfiguration['args'] += ['input' => $this->getResourceFieldConfiguration(null, null, $operation->getDeprecationReason(), $resourceType, $resourceClass, true, $operation)]; } - $mutationFields[$mutationName.$shortName] = $fieldConfiguration ?? []; + $mutationFields[$operation->getName().$operation->getShortName()] = $fieldConfiguration ?? []; return $mutationFields; } @@ -154,22 +146,21 @@ public function getMutationFields(string $resourceClass, Operation $operation, s /** * {@inheritdoc} */ - public function getSubscriptionFields(string $resourceClass, Operation $operation, string $subscriptionName): array + public function getSubscriptionFields(string $resourceClass, Operation $operation): array { $subscriptionFields = []; - $shortName = $operation->getShortName(); $resourceType = new Type(Type::BUILTIN_TYPE_OBJECT, true, $resourceClass); - $description = $operation->getDescription() ?? sprintf('Subscribes to the action event of a %s.', $shortName); - $deprecationReason = $operation->getDeprecationReason(); + $description = $operation->getDescription() ?? sprintf('Subscribes to the action event of a %s.', $operation->getShortName()); - if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $description, $deprecationReason, $resourceType, $resourceClass, false, $subscriptionName)) { - $fieldConfiguration['args'] += ['input' => $this->getResourceFieldConfiguration(null, null, $deprecationReason, $resourceType, $resourceClass, true, $subscriptionName)]; + if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $description, $operation->getDeprecationReason(), $resourceType, $resourceClass, false, $operation)) { + $fieldConfiguration['args'] += ['input' => $this->getResourceFieldConfiguration(null, null, $operation->getDeprecationReason(), $resourceType, $resourceClass, true, $operation)]; } if (!$fieldConfiguration) { return []; } + $subscriptionName = $operation->getName(); // TODO: 3.0 change this if ('update_subscription' === $subscriptionName) { $subscriptionName = 'update'; @@ -183,10 +174,11 @@ public function getSubscriptionFields(string $resourceClass, Operation $operatio /** * {@inheritdoc} */ - public function getResourceObjectTypeFields(?string $resourceClass, Operation $operation, bool $input, string $operationName, int $depth = 0, ?array $ioMetadata = null): array + public function getResourceObjectTypeFields(?string $resourceClass, Operation $operation, bool $input, int $depth = 0, ?array $ioMetadata = null): array { $fields = []; $idField = ['type' => GraphQLType::nonNull(GraphQLType::id())]; + $optionalIdField = ['type' => GraphQLType::id()]; $clientMutationId = GraphQLType::string(); $clientSubscriptionId = GraphQLType::string(); @@ -205,7 +197,7 @@ public function getResourceObjectTypeFields(?string $resourceClass, Operation $o ]; } - if ('delete' === $operationName) { + if ('delete' === $operation->getName()) { $fields = [ 'id' => $idField, ]; @@ -217,9 +209,12 @@ public function getResourceObjectTypeFields(?string $resourceClass, Operation $o return $fields; } - if (!$input || 'create' !== $operationName) { + if (!$input || 'create' !== $operation->getName()) { $fields['id'] = $idField; } + if ($input && $depth >= 1) { + $fields['id'] = $optionalIdField; + } ++$depth; // increment the depth for the call to getResourceFieldConfiguration. @@ -239,7 +234,7 @@ public function getResourceObjectTypeFields(?string $resourceClass, Operation $o continue; } - if ($fieldConfiguration = $this->getResourceFieldConfiguration($property, $propertyMetadata->getDescription(), $propertyMetadata->getDeprecationReason(), $propertyType, $resourceClass, $input, $operationName, $depth, null !== $propertyMetadata->getSecurity())) { + if ($fieldConfiguration = $this->getResourceFieldConfiguration($property, $propertyMetadata->getDescription(), $propertyMetadata->getDeprecationReason(), $propertyType, $resourceClass, $input, $operation, $depth, null !== $propertyMetadata->getSecurity())) { $fields['id' === $property ? '_id' : $this->normalizePropertyName($property, $resourceClass)] = $fieldConfiguration; } } @@ -255,11 +250,11 @@ public function getResourceObjectTypeFields(?string $resourceClass, Operation $o /** * {@inheritdoc} */ - public function resolveResourceArgs(array $args, string $operationName, string $shortName): array + public function resolveResourceArgs(array $args, Operation $operation): array { foreach ($args as $id => $arg) { if (!isset($arg['type'])) { - throw new \InvalidArgumentException(sprintf('The argument "%s" of the custom operation "%s" in %s needs a "type" option.', $id, $operationName, $shortName)); + throw new \InvalidArgumentException(sprintf('The argument "%s" of the custom operation "%s" in %s needs a "type" option.', $id, $operation->getName(), $operation->getShortName())); } $args[$id]['type'] = $this->typeConverter->resolveType($arg['type']); @@ -273,7 +268,7 @@ public function resolveResourceArgs(array $args, string $operationName, string $ * * @see http://webonyx.github.io/graphql-php/type-system/object-types/ */ - private function getResourceFieldConfiguration(?string $property, ?string $fieldDescription, ?string $deprecationReason, Type $type, string $rootResource, bool $input, string $operationName, int $depth = 0, bool $forceNullable = false): ?array + private function getResourceFieldConfiguration(?string $property, ?string $fieldDescription, ?string $deprecationReason, Type $type, string $rootResource, bool $input, Operation $rootOperation, int $depth = 0, bool $forceNullable = false): ?array { try { if ( @@ -285,7 +280,7 @@ private function getResourceFieldConfiguration(?string $property, ?string $field $resourceClass = $type->getClassName(); } - if (null === $graphqlType = $this->convertType($type, $input, $operationName, $resourceClass ?? '', $rootResource, $property, $depth, $forceNullable)) { + if (null === $graphqlType = $this->convertType($type, $input, $rootOperation, $resourceClass ?? '', $rootResource, $property, $depth, $forceNullable)) { return null; } @@ -295,24 +290,24 @@ private function getResourceFieldConfiguration(?string $property, ?string $field $resourceClass = ''; } + // Check mercure attribute if it's a subscription at the root level. + if ($rootOperation instanceof Subscription && null === $property && !$rootOperation->getMercure()) { + return null; + } + $resourceMetadataCollection = $operation = null; if (!empty($resourceClass)) { $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); try { - $operation = $resourceMetadataCollection->getGraphQlOperation($operationName); + $operation = $resourceMetadataCollection->getGraphQlOperation($rootOperation->getName()); } catch (OperationNotFoundException $e) { } } - // Check mercure attribute if it's a subscription at the root level. - if ($operation instanceof Subscription && null === $property && !$operation->getMercure()) { - return null; - } - $args = []; - if (!$input && !$operation instanceof Mutation && !$operation instanceof Subscription && !$isStandardGraphqlType && $this->typeBuilder->isCollection($type)) { - if ($this->pagination->isGraphQlEnabled($resourceClass, $operationName)) { - $args = $this->getGraphQlPaginationArgs($resourceClass, $operationName); + if (!$input && !$rootOperation instanceof Mutation && !$rootOperation instanceof Subscription && !$isStandardGraphqlType && $this->typeBuilder->isCollection($type)) { + if ($this->pagination->isGraphQlEnabled($resourceClass, $rootOperation->getName())) { + $args = $this->getGraphQlPaginationArgs($resourceClass, $rootOperation->getName()); } // Look for the collection operation if it exists @@ -323,17 +318,17 @@ private function getResourceFieldConfiguration(?string $property, ?string $field } } - $args = $this->getFilterArgs($args, $resourceClass, $operation, $rootResource, $property, $operationName, $depth); + $args = $this->getFilterArgs($args, $resourceClass, $operation, $rootResource, $rootOperation, $property, $depth); } if ($isStandardGraphqlType || $input) { $resolve = null; - } elseif (($operation instanceof Mutation || $operation instanceof Subscription) && $depth <= 0) { - $resolve = $operation instanceof Mutation ? ($this->itemMutationResolverFactory)($resourceClass, $rootResource, $operationName) : ($this->itemSubscriptionResolverFactory)($resourceClass, $rootResource, $operationName); + } elseif (($rootOperation instanceof Mutation || $rootOperation instanceof Subscription) && $depth <= 0) { + $resolve = $rootOperation instanceof Mutation ? ($this->itemMutationResolverFactory)($resourceClass, $rootResource, $rootOperation->getName()) : ($this->itemSubscriptionResolverFactory)($resourceClass, $rootResource, $rootOperation->getName()); } elseif ($this->typeBuilder->isCollection($type)) { - $resolve = ($this->collectionResolverFactory)($resourceClass, $rootResource, $operationName); + $resolve = ($this->collectionResolverFactory)($resourceClass, $rootResource, $rootOperation->getName()); } else { - $resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $operationName); + $resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $rootOperation->getName()); } return [ @@ -397,7 +392,7 @@ private function getGraphQlPaginationArgs(string $resourceClass, string $queryNa /** * @param Operation|ApiOperation|null $operation */ - private function getFilterArgs(array $args, ?string $resourceClass, $operation, string $rootResource, ?string $property, string $operationName, int $depth): array + private function getFilterArgs(array $args, ?string $resourceClass, $operation, string $rootResource, Operation $rootOperation, ?string $property, int $depth): array { if (null === $operation || null === $resourceClass) { return $args; @@ -411,7 +406,7 @@ private function getFilterArgs(array $args, ?string $resourceClass, $operation, foreach ($this->filterLocator->get($filterId)->getDescription($resourceClass) as $key => $value) { $nullable = isset($value['required']) ? !$value['required'] : true; $filterType = \in_array($value['type'], Type::$builtinTypes, true) ? new Type($value['type'], $nullable) : new Type('object', $nullable, $value['type']); - $graphqlFilterType = $this->convertType($filterType, false, $operationName, $resourceClass, $rootResource, $property, $depth); + $graphqlFilterType = $this->convertType($filterType, false, $rootOperation, $resourceClass, $rootResource, $property, $depth); if ('[]' === substr($key, -2)) { $graphqlFilterType = GraphQLType::listOf($graphqlFilterType); @@ -502,9 +497,9 @@ private function convertFilterArgsToTypes(array $args): array * * @throws InvalidTypeException */ - private function convertType(Type $type, bool $input, string $operationName, string $resourceClass, string $rootResource, ?string $property, int $depth, bool $forceNullable = false) + private function convertType(Type $type, bool $input, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth, bool $forceNullable = false) { - $graphqlType = $this->typeConverter->convertType($type, $input, $operationName, $resourceClass, $rootResource, $property, $depth); + $graphqlType = $this->typeConverter->convertType($type, $input, $rootOperation, $resourceClass, $rootResource, $property, $depth); if (null === $graphqlType) { throw new InvalidTypeException(sprintf('The type "%s" is not supported.', $type->getBuiltinType())); @@ -519,20 +514,10 @@ private function convertType(Type $type, bool $input, string $operationName, str } if ($this->typeBuilder->isCollection($type)) { - return $this->pagination->isGraphQlEnabled($resourceClass, $operationName) && !$input ? $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $operationName) : GraphQLType::listOf($graphqlType); - } - - $operation = null; - $resourceClass = '' === $resourceClass ? $rootResource : $resourceClass; - if ($resourceClass) { - $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); - try { - $operation = $resourceMetadataCollection->getGraphQlOperation($operationName); - } catch (OperationNotFoundException $e) { - } + return $this->pagination->isGraphQlEnabled($resourceClass, $rootOperation->getName()) && !$input ? $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $rootOperation->getName()) : GraphQLType::listOf($graphqlType); } - return $forceNullable || !$graphqlType instanceof NullableType || $type->isNullable() || ($operation instanceof Mutation && 'update' === $operationName) + return $forceNullable || !$graphqlType instanceof NullableType || $type->isNullable() || ($rootOperation instanceof Mutation && 'update' === $rootOperation->getName()) ? $graphqlType : GraphQLType::nonNull($graphqlType); } diff --git a/src/GraphQl/Type/FieldsBuilderInterface.php b/src/GraphQl/Type/FieldsBuilderInterface.php index 988e5f4e731..1361b388fe3 100644 --- a/src/GraphQl/Type/FieldsBuilderInterface.php +++ b/src/GraphQl/Type/FieldsBuilderInterface.php @@ -32,30 +32,30 @@ public function getNodeQueryFields(): array; /** * Gets the item query fields of the schema. */ - public function getItemQueryFields(string $resourceClass, Operation $operation, string $queryName, array $configuration): array; + public function getItemQueryFields(string $resourceClass, Operation $operation, array $configuration): array; /** * Gets the collection query fields of the schema. */ - public function getCollectionQueryFields(string $resourceClass, Operation $operation, string $queryName, array $configuration): array; + public function getCollectionQueryFields(string $resourceClass, Operation $operation, array $configuration): array; /** * Gets the mutation fields of the schema. */ - public function getMutationFields(string $resourceClass, Operation $operation, string $mutationName): array; + public function getMutationFields(string $resourceClass, Operation $operation): array; /** * Gets the subscription fields of the schema. */ - public function getSubscriptionFields(string $resourceClass, Operation $operation, string $subscriptionName): array; + public function getSubscriptionFields(string $resourceClass, Operation $operation): array; /** * Gets the fields of the type of the given resource. */ - public function getResourceObjectTypeFields(?string $resourceClass, Operation $operation, bool $input, string $operationName, int $depth = 0, ?array $ioMetadata = null): array; + public function getResourceObjectTypeFields(?string $resourceClass, Operation $operation, bool $input, int $depth = 0, ?array $ioMetadata = null): array; /** * Resolve the args of a resource by resolving its types. */ - public function resolveResourceArgs(array $args, string $operationName, string $shortName): array; + public function resolveResourceArgs(array $args, Operation $operation): array; } diff --git a/src/GraphQl/Type/SchemaBuilder.php b/src/GraphQl/Type/SchemaBuilder.php index 607e4e29e32..d7067f676ec 100644 --- a/src/GraphQl/Type/SchemaBuilder.php +++ b/src/GraphQl/Type/SchemaBuilder.php @@ -66,35 +66,35 @@ public function getSchema(): Schema //TODO: 3.0 remove these if ('item_query' === $operationName) { - $queryFields += $this->fieldsBuilder->getItemQueryFields($resourceClass, $operation, $operationName, $configuration); + $queryFields += $this->fieldsBuilder->getItemQueryFields($resourceClass, $operation, $configuration); continue; } if ('collection_query' === $operationName) { - $queryFields += $this->fieldsBuilder->getCollectionQueryFields($resourceClass, $operation, $operationName, $configuration); + $queryFields += $this->fieldsBuilder->getCollectionQueryFields($resourceClass, $operation, $configuration); continue; } if ($operation instanceof Query && !$operation->isCollection()) { - $queryFields += $this->fieldsBuilder->getItemQueryFields($resourceClass, $operation, $operationName, $configuration); + $queryFields += $this->fieldsBuilder->getItemQueryFields($resourceClass, $operation, $configuration); continue; } if ($operation instanceof Query && $operation->isCollection()) { - $queryFields += $this->fieldsBuilder->getCollectionQueryFields($resourceClass, $operation, $operationName, $configuration); + $queryFields += $this->fieldsBuilder->getCollectionQueryFields($resourceClass, $operation, $configuration); continue; } if ($operation instanceof Subscription && $operation->getMercure()) { - $subscriptionFields += $this->fieldsBuilder->getSubscriptionFields($resourceClass, $operation, $operationName); + $subscriptionFields += $this->fieldsBuilder->getSubscriptionFields($resourceClass, $operation); continue; } - $mutationFields += $this->fieldsBuilder->getMutationFields($resourceClass, $operation, $operationName); + $mutationFields += $this->fieldsBuilder->getMutationFields($resourceClass, $operation); } } } diff --git a/src/GraphQl/Type/TypeBuilder.php b/src/GraphQl/Type/TypeBuilder.php index a47c9dff003..68bc15b736c 100644 --- a/src/GraphQl/Type/TypeBuilder.php +++ b/src/GraphQl/Type/TypeBuilder.php @@ -54,18 +54,10 @@ public function __construct(TypesContainerInterface $typesContainer, callable $d /** * {@inheritdoc} */ - public function getResourceObjectType(?string $resourceClass, ResourceMetadataCollection $resourceMetadataCollection, string $operationName, bool $input, bool $wrapped = false, int $depth = 0): GraphQLType + public function getResourceObjectType(?string $resourceClass, ResourceMetadataCollection $resourceMetadataCollection, Operation $operation, bool $input, bool $wrapped = false, int $depth = 0): GraphQLType { - try { - $operation = $resourceMetadataCollection->getGraphQlOperation($operationName); - } catch (OperationNotFoundException $e) { - $operation = (new Query()) - ->withResource($resourceMetadataCollection[0]) - ->withName($operationName) - ->withCollection('collection_query' === $operationName); - } - $shortName = $operation->getShortName(); + $operationName = $operation->getName(); if ($operation instanceof Mutation) { $shortName = $operationName.ucfirst($shortName); @@ -76,6 +68,9 @@ public function getResourceObjectType(?string $resourceClass, ResourceMetadataCo } if ($input) { + if ($depth > 0) { + $shortName .= 'Nested'; + } $shortName .= 'Input'; } elseif ($operation instanceof Mutation || $operation instanceof Subscription) { if ($depth > 0) { @@ -141,8 +136,17 @@ public function getResourceObjectType(?string $resourceClass, ResourceMetadataCo $wrappedOperationName = $operation instanceof Query ? $operationName : 'item_query'; } + try { + $wrappedOperation = $resourceMetadataCollection->getGraphQlOperation($wrappedOperationName); + } catch (OperationNotFoundException $e) { + $wrappedOperation = (new Query()) + ->withResource($resourceMetadataCollection[0]) + ->withName($wrappedOperationName) + ->withCollection('collection_query' === $wrappedOperationName); + } + $fields = [ - lcfirst($operation->getShortName()) => $this->getResourceObjectType($resourceClass, $resourceMetadataCollection, $wrappedOperationName, $input, true, $depth), + lcfirst($wrappedOperation->getShortName()) => $this->getResourceObjectType($resourceClass, $resourceMetadataCollection, $wrappedOperation, $input, true, $depth), ]; if ($operation instanceof Subscription) { @@ -158,10 +162,10 @@ public function getResourceObjectType(?string $resourceClass, ResourceMetadataCo } $fieldsBuilder = $this->fieldsBuilderLocator->get('api_platform.graphql.fields_builder'); - $fields = $fieldsBuilder->getResourceObjectTypeFields($resourceClass, $operation, $input, $operationName, $depth, $ioMetadata); + $fields = $fieldsBuilder->getResourceObjectTypeFields($resourceClass, $operation, $input, $depth, $ioMetadata); if ($input && $operation instanceof Mutation && null !== $mutationArgs = $operation->getArgs()) { - return $fieldsBuilder->resolveResourceArgs($mutationArgs, $operationName, $operation->getShortName()) + ['clientMutationId' => $fields['clientMutationId']]; + return $fieldsBuilder->resolveResourceArgs($mutationArgs, $operation) + ['clientMutationId' => $fields['clientMutationId']]; } return $fields; diff --git a/src/GraphQl/Type/TypeBuilderInterface.php b/src/GraphQl/Type/TypeBuilderInterface.php index 21e7a285688..6ee17ec1861 100644 --- a/src/GraphQl/Type/TypeBuilderInterface.php +++ b/src/GraphQl/Type/TypeBuilderInterface.php @@ -13,6 +13,7 @@ namespace ApiPlatform\GraphQl\Type; +use ApiPlatform\Metadata\GraphQl\Operation; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\NonNull; @@ -34,7 +35,7 @@ interface TypeBuilderInterface * * @return ObjectType|NonNull the object type, possibly wrapped by NonNull */ - public function getResourceObjectType(?string $resourceClass, ResourceMetadataCollection $resourceMetadataCollection, string $operationName, bool $input, bool $wrapped = false, int $depth = 0): GraphQLType; + public function getResourceObjectType(?string $resourceClass, ResourceMetadataCollection $resourceMetadataCollection, Operation $operation, bool $input, bool $wrapped = false, int $depth = 0): GraphQLType; /** * Get the interface type of a node. diff --git a/src/GraphQl/Type/TypeConverter.php b/src/GraphQl/Type/TypeConverter.php index 0e68c1c1903..66d3d069ea1 100644 --- a/src/GraphQl/Type/TypeConverter.php +++ b/src/GraphQl/Type/TypeConverter.php @@ -14,7 +14,11 @@ namespace ApiPlatform\GraphQl\Type; use ApiPlatform\Exception\InvalidArgumentException; +use ApiPlatform\Exception\OperationNotFoundException; use ApiPlatform\Exception\ResourceClassNotFoundException; +use ApiPlatform\Metadata\GraphQl\Operation; +use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use GraphQL\Error\SyntaxError; use GraphQL\Language\AST\ListTypeNode; @@ -38,18 +42,24 @@ final class TypeConverter implements TypeConverterInterface private $typeBuilder; private $typesContainer; private $resourceMetadataCollectionFactory; + private $propertyMetadataFactory; - public function __construct(TypeBuilderInterface $typeBuilder, TypesContainerInterface $typesContainer, ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory) + public function __construct(TypeBuilderInterface $typeBuilder, TypesContainerInterface $typesContainer, ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory = null) { $this->typeBuilder = $typeBuilder; $this->typesContainer = $typesContainer; $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory; + $this->propertyMetadataFactory = $propertyMetadataFactory; + + if (null === $this->propertyMetadataFactory) { + @trigger_error(sprintf('Not injecting %s in the TypeConverter is deprecated since 2.7 and will not be supported in 3.0.', PropertyMetadataFactoryInterface::class), \E_USER_DEPRECATED); + } } /** * {@inheritdoc} */ - public function convertType(Type $type, bool $input, string $operationName, string $resourceClass, string $rootResource, ?string $property, int $depth) + public function convertType(Type $type, bool $input, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth) { switch ($type->getBuiltinType()) { case Type::BUILTIN_TYPE_BOOL: @@ -62,21 +72,17 @@ public function convertType(Type $type, bool $input, string $operationName, stri return GraphQLType::string(); case Type::BUILTIN_TYPE_ARRAY: case Type::BUILTIN_TYPE_ITERABLE: - if ($resourceType = $this->getResourceType($type, $input, $operationName, $depth)) { + if ($resourceType = $this->getResourceType($type, $input, $rootOperation, $rootResource, $property, $depth)) { return $resourceType; } return 'Iterable'; case Type::BUILTIN_TYPE_OBJECT: - if ($input && $depth > 0) { - return GraphQLType::string(); - } - if (is_a($type->getClassName(), \DateTimeInterface::class, true)) { return GraphQLType::string(); } - return $this->getResourceType($type, $input, $operationName, $depth); + return $this->getResourceType($type, $input, $rootOperation, $rootResource, $property, $depth); default: return null; } @@ -100,7 +106,7 @@ public function resolveType(string $type): ?GraphQLType throw new InvalidArgumentException(sprintf('The type "%s" was not resolved.', $type)); } - private function getResourceType(Type $type, bool $input, string $operationName, int $depth): ?GraphQLType + private function getResourceType(Type $type, bool $input, Operation $rootOperation, string $rootResource, ?string $property, int $depth): ?GraphQLType { if ( $this->typeBuilder->isCollection($type) && @@ -122,7 +128,6 @@ private function getResourceType(Type $type, bool $input, string $operationName, } $hasGraphQl = false; - $operation = null; foreach ($resourceMetadataCollection as $resourceMetadata) { if (null !== $resourceMetadata->getGraphQlOperations()) { $hasGraphQl = true; @@ -138,7 +143,29 @@ private function getResourceType(Type $type, bool $input, string $operationName, return null; } - return $this->typeBuilder->getResourceObjectType($resourceClass, $resourceMetadataCollection, $operationName, $input, false, $depth); + $propertyMetadata = null; + if ($property && $this->propertyMetadataFactory) { + $context = [ + 'normalization_groups' => $rootOperation->getNormalizationContext()['groups'] ?? null, + 'denormalization_groups' => $rootOperation->getDenormalizationContext()['groups'] ?? null, + ]; + $propertyMetadata = $this->propertyMetadataFactory->create($rootResource, $property, $context); + } + + if ($input && $depth > 0 && (!$propertyMetadata || !$propertyMetadata->isWritableLink())) { + return GraphQLType::string(); + } + + try { + $operation = $resourceMetadataCollection->getGraphQlOperation($rootOperation->getName()); + } catch (OperationNotFoundException $e) { + $operation = (new Query()) + ->withResource($resourceMetadataCollection[0]) + ->withName($rootOperation->getName()) + ->withCollection('collection_query' === $rootOperation->getName()); + } + + return $this->typeBuilder->getResourceObjectType($resourceClass, $resourceMetadataCollection, $operation, $input, false, $depth); } private function resolveAstTypeNode(TypeNode $astTypeNode, string $fromType): ?GraphQLType diff --git a/src/GraphQl/Type/TypeConverterInterface.php b/src/GraphQl/Type/TypeConverterInterface.php index 5af5f286a42..3206a3163dc 100644 --- a/src/GraphQl/Type/TypeConverterInterface.php +++ b/src/GraphQl/Type/TypeConverterInterface.php @@ -13,6 +13,7 @@ namespace ApiPlatform\GraphQl\Type; +use ApiPlatform\Metadata\GraphQl\Operation; use GraphQL\Type\Definition\Type as GraphQLType; use Symfony\Component\PropertyInfo\Type; @@ -31,7 +32,7 @@ interface TypeConverterInterface * * @return string|GraphQLType|null */ - public function convertType(Type $type, bool $input, string $operationName, string $resourceClass, string $rootResource, ?string $property, int $depth); + public function convertType(Type $type, bool $input, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth); /** * Resolves a type written with the GraphQL type system to its object representation. diff --git a/tests/Fixtures/TestBundle/Document/RelatedDummy.php b/tests/Fixtures/TestBundle/Document/RelatedDummy.php index eddac4c06b8..4c25449c3f6 100644 --- a/tests/Fixtures/TestBundle/Document/RelatedDummy.php +++ b/tests/Fixtures/TestBundle/Document/RelatedDummy.php @@ -27,7 +27,7 @@ * @author Kévin Dunglas * @author Alexandre Delplace * - * @ApiResource(graphql={"update"={"normalization_context"={"groups"={"chicago", "fakemanytomany"}}}}, iri="https://schema.org/Product", attributes={"normalization_context"={"groups"={"friends"}}, "filters"={"related_dummy.mongodb.friends"}}) + * @ApiResource(graphql={"update"={"normalization_context"={"groups"={"chicago", "fakemanytomany"}}, "denormalization_context"={"groups"={"friends"}}}}, iri="https://schema.org/Product", attributes={"normalization_context"={"groups"={"friends"}}, "filters"={"related_dummy.mongodb.friends"}}) * @ODM\Document */ class RelatedDummy extends ParentDummy diff --git a/tests/Fixtures/TestBundle/Entity/RelatedDummy.php b/tests/Fixtures/TestBundle/Entity/RelatedDummy.php index 8328ab9f52f..932c4fd5ee2 100644 --- a/tests/Fixtures/TestBundle/Entity/RelatedDummy.php +++ b/tests/Fixtures/TestBundle/Entity/RelatedDummy.php @@ -26,7 +26,7 @@ * * @author Kévin Dunglas * - * @ApiResource(graphql={"update"={"normalization_context"={"groups"={"chicago", "fakemanytomany"}}}}, iri="https://schema.org/Product", attributes={"normalization_context"={"groups"={"friends"}}, "filters"={"related_dummy.friends", "related_dummy.complex_sub_query"}}) + * @ApiResource(graphql={"update"={"normalization_context"={"groups"={"chicago", "fakemanytomany"}}, "denormalization_context"={"groups"={"friends"}}}}, iri="https://schema.org/Product", attributes={"normalization_context"={"groups"={"friends"}}, "filters"={"related_dummy.friends", "related_dummy.complex_sub_query"}}) * @ORM\Entity */ class RelatedDummy extends ParentDummy diff --git a/tests/Fixtures/TestBundle/GraphQl/Type/TypeConverter.php b/tests/Fixtures/TestBundle/GraphQl/Type/TypeConverter.php index 0b650b07f7f..58d473a24c4 100644 --- a/tests/Fixtures/TestBundle/GraphQl/Type/TypeConverter.php +++ b/tests/Fixtures/TestBundle/GraphQl/Type/TypeConverter.php @@ -14,6 +14,7 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\GraphQl\Type; use ApiPlatform\GraphQl\Type\TypeConverterInterface; +use ApiPlatform\Metadata\GraphQl\Operation; use ApiPlatform\Tests\Fixtures\TestBundle\Document\Dummy as DummyDocument; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; use GraphQL\Type\Definition\Type as GraphQLType; @@ -36,7 +37,7 @@ public function __construct(TypeConverterInterface $defaultTypeConverter) /** * {@inheritdoc} */ - public function convertType(Type $type, bool $input, string $operationName, string $resourceClass, string $rootResource, ?string $property, int $depth) + public function convertType(Type $type, bool $input, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth) { if ('dummyDate' === $property && \in_array($rootResource, [Dummy::class, DummyDocument::class], true) @@ -46,7 +47,7 @@ public function convertType(Type $type, bool $input, string $operationName, stri return 'DateTime'; } - return $this->defaultTypeConverter->convertType($type, $input, $operationName, $resourceClass, $rootResource, $property, $depth); + return $this->defaultTypeConverter->convertType($type, $input, $rootOperation, $resourceClass, $rootResource, $property, $depth); } /** diff --git a/tests/GraphQl/Resolver/Factory/CollectionResolverFactoryTest.php b/tests/GraphQl/Resolver/Factory/CollectionResolverFactoryTest.php index 2d6568c6fa1..a40e2e222bf 100644 --- a/tests/GraphQl/Resolver/Factory/CollectionResolverFactoryTest.php +++ b/tests/GraphQl/Resolver/Factory/CollectionResolverFactoryTest.php @@ -20,7 +20,7 @@ use ApiPlatform\GraphQl\Resolver\Stage\SecurityStageInterface; use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use GraphQL\Type\Definition\ResolveInfo; @@ -92,7 +92,7 @@ public function testResolve(): void $readStageCollection = [new \stdClass()]; $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operationName, $resolverContext)->shouldBeCalled()->willReturn($readStageCollection); - $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operationName => (new Query())->withCollection(true)])])); + $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operationName => new QueryCollection()])])); $this->securityStageProphecy->__invoke($resourceClass, $operationName, $resolverContext + [ 'extra_variables' => [ @@ -162,7 +162,7 @@ public function testResolveNullSource(): void $readStageCollection = [new \stdClass()]; $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operationName, $resolverContext)->shouldBeCalled()->willReturn($readStageCollection); - $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operationName => (new Query())->withCollection(true)])])); + $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operationName => new QueryCollection()])])); $this->securityStageProphecy->__invoke($resourceClass, $operationName, $resolverContext + [ 'extra_variables' => [ @@ -238,7 +238,7 @@ public function testResolveCustom(): void $readStageCollection = [new \stdClass()]; $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operationName, $resolverContext)->shouldBeCalled()->willReturn($readStageCollection); - $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operationName => (new Query())->withCollection(true)->withResolver('query_resolver_id')])])); + $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operationName => (new QueryCollection())->withResolver('query_resolver_id')])])); $customCollection = [new \stdClass()]; $customCollection[0]->field = 'foo'; diff --git a/tests/GraphQl/Resolver/Stage/SerializeStageTest.php b/tests/GraphQl/Resolver/Stage/SerializeStageTest.php index 49d7e6e2a90..75844fd421d 100644 --- a/tests/GraphQl/Resolver/Stage/SerializeStageTest.php +++ b/tests/GraphQl/Resolver/Stage/SerializeStageTest.php @@ -23,6 +23,7 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GraphQl\Mutation; use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\GraphQl\Subscription; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; @@ -134,7 +135,7 @@ public function testApplyCollectionWithPagination(iterable $collection, array $a 'args' => $args, 'info' => $this->prophesize(ResolveInfo::class)->reveal(), ]; - $resourceMetadata = new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operationName => (new Query())->withShortName('shortName')->withCollection(true)])]); + $resourceMetadata = new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operationName => (new QueryCollection())->withShortName('shortName')])]); $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn($resourceMetadata); $normalizationContext = ['normalization' => true]; diff --git a/tests/GraphQl/Type/FieldsBuilderTest.php b/tests/GraphQl/Type/FieldsBuilderTest.php index 15694c34d46..a63b69a8bb1 100644 --- a/tests/GraphQl/Type/FieldsBuilderTest.php +++ b/tests/GraphQl/Type/FieldsBuilderTest.php @@ -26,6 +26,7 @@ use ApiPlatform\Metadata\GraphQl\Mutation; use ApiPlatform\Metadata\GraphQl\Operation; use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\GraphQl\Subscription; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; @@ -140,15 +141,17 @@ public function testGetNodeQueryFields(): void /** * @dataProvider itemQueryFieldsProvider */ - public function testGetItemQueryFields(string $resourceClass, Operation $operation, string $queryName, array $configuration, ?GraphQLType $graphqlType, ?callable $resolver, array $expectedQueryFields): void + public function testGetItemQueryFields(string $resourceClass, Operation $operation, array $configuration, ?GraphQLType $graphqlType, ?callable $resolver, array $expectedQueryFields): void { - $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, $queryName, $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); + $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, Argument::that(static function (Operation $arg) use ($operation) : bool { + return $arg->getName() === $operation->getName(); + }), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); $this->typeConverterProphecy->resolveType(Argument::type('string'))->willReturn(GraphQLType::string()); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); - $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$queryName => $operation])])); - $this->itemResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $queryName)->willReturn($resolver); + $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); + $this->itemResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation->getName())->willReturn($resolver); - $queryFields = $this->fieldsBuilder->getItemQueryFields($resourceClass, $operation, $queryName, $configuration); + $queryFields = $this->fieldsBuilder->getItemQueryFields($resourceClass, $operation, $configuration); $this->assertEquals($expectedQueryFields, $queryFields); } @@ -156,8 +159,8 @@ public function testGetItemQueryFields(string $resourceClass, Operation $operati public function itemQueryFieldsProvider(): array { return [ - 'no resource field configuration' => ['resourceClass', (new Query()), 'action', [], null, null, []], - 'nominal standard type case with deprecation reason and description' => ['resourceClass', (new Query())->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), 'action', [], GraphQLType::string(), null, + 'no resource field configuration' => ['resourceClass', (new Query())->withName('action'), [], null, null, []], + 'nominal standard type case with deprecation reason and description' => ['resourceClass', (new Query())->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), [], GraphQLType::string(), null, [ 'actionShortName' => [ 'type' => GraphQLType::string(), @@ -170,7 +173,7 @@ public function itemQueryFieldsProvider(): array ], ], ], - 'nominal item case' => ['resourceClass', (new Query())->withShortName('ShortName'), 'action', [], $graphqlType = new ObjectType(['name' => 'item']), $resolver = function () { + 'nominal item case' => ['resourceClass', (new Query())->withName('action')->withShortName('ShortName'), [], $graphqlType = new ObjectType(['name' => 'item']), $resolver = function () { }, [ 'actionShortName' => [ @@ -185,7 +188,7 @@ public function itemQueryFieldsProvider(): array ], ], 'empty overridden args and add fields' => [ - 'resourceClass', (new Query())->withShortName('ShortName'), 'item_query', ['args' => [], 'name' => 'customActionName'], GraphQLType::string(), null, + 'resourceClass', (new Query())->withShortName('ShortName'), ['args' => [], 'name' => 'customActionName'], GraphQLType::string(), null, [ 'shortName' => [ 'type' => GraphQLType::string(), @@ -198,7 +201,7 @@ public function itemQueryFieldsProvider(): array ], ], 'override args with custom ones' => [ - 'resourceClass', (new Query())->withShortName('ShortName'), 'item_query', ['args' => ['customArg' => ['type' => 'a type']]], GraphQLType::string(), null, + 'resourceClass', (new Query())->withShortName('ShortName'), ['args' => ['customArg' => ['type' => 'a type']]], GraphQLType::string(), null, [ 'shortName' => [ 'type' => GraphQLType::string(), @@ -219,14 +222,16 @@ public function itemQueryFieldsProvider(): array /** * @dataProvider collectionQueryFieldsProvider */ - public function testGetCollectionQueryFields(string $resourceClass, Operation $operation, string $queryName, array $configuration, ?GraphQLType $graphqlType, ?callable $resolver, array $expectedQueryFields): void + public function testGetCollectionQueryFields(string $resourceClass, Operation $operation, array $configuration, ?GraphQLType $graphqlType, ?callable $resolver, array $expectedQueryFields): void { - $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, $queryName, $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); + $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, Argument::that(static function (Operation $arg) use ($operation) : bool { + return $arg->getName() === $operation->getName(); + }), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); $this->typeConverterProphecy->resolveType(Argument::type('string'))->willReturn(GraphQLType::string()); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(true); - $this->typeBuilderProphecy->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $queryName)->willReturn($graphqlType); - $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$queryName => $operation])])); - $this->collectionResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $queryName)->willReturn($resolver); + $this->typeBuilderProphecy->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $operation->getName())->willReturn($graphqlType); + $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); + $this->collectionResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation->getName())->willReturn($resolver); $this->filterLocatorProphecy->has('my_filter')->willReturn(true); $filterProphecy = $this->prophesize(FilterInterface::class); $filterProphecy->getDescription($resourceClass)->willReturn([ @@ -241,7 +246,7 @@ public function testGetCollectionQueryFields(string $resourceClass, Operation $o $this->typesContainerProphecy->set('ShortNameFilter_dateField', Argument::type(ListOfType::class)); $this->typesContainerProphecy->set('ShortNameFilter_parent__child', Argument::type(ListOfType::class)); - $queryFields = $this->fieldsBuilder->getCollectionQueryFields($resourceClass, $operation, $queryName, $configuration); + $queryFields = $this->fieldsBuilder->getCollectionQueryFields($resourceClass, $operation, $configuration); $this->assertEquals($expectedQueryFields, $queryFields); } @@ -249,8 +254,8 @@ public function testGetCollectionQueryFields(string $resourceClass, Operation $o public function collectionQueryFieldsProvider(): array { return [ - 'no resource field configuration' => ['resourceClass', new Query(), 'action', [], null, null, []], - 'nominal collection case with deprecation reason and description' => ['resourceClass', (new Query())->withCollection(true)->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), 'action', [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function () { + 'no resource field configuration' => ['resourceClass', (new QueryCollection())->withName('action'), [], null, null, []], + 'nominal collection case with deprecation reason and description' => ['resourceClass', (new QueryCollection())->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function () { }, [ 'actionShortNames' => [ @@ -279,7 +284,7 @@ public function collectionQueryFieldsProvider(): array ], ], ], - 'collection with filters' => ['resourceClass', (new Query())->withShortName('ShortName')->withCollection(true)->withFilters(['my_filter']), 'action', [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function () { + 'collection with filters' => ['resourceClass', (new QueryCollection())->withName('action')->withShortName('ShortName')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function () { }, [ 'actionShortNames' => [ @@ -313,7 +318,7 @@ public function collectionQueryFieldsProvider(): array ], ], 'collection empty overridden args and add fields' => [ - 'resourceClass', (new Query())->withCollection(true)->withShortName('ShortName')->withArgs([]), 'action', ['args' => [], 'name' => 'customActionName'], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function () { + 'resourceClass', (new QueryCollection())->withName('action')->withShortName('ShortName')->withArgs([]), ['args' => [], 'name' => 'customActionName'], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function () { }, [ 'actionShortNames' => [ @@ -327,7 +332,7 @@ public function collectionQueryFieldsProvider(): array ], ], 'collection override args with custom ones' => [ - 'resourceClass', (new Query())->withCollection(true)->withShortName('ShortName'), 'action', ['args' => ['customArg' => ['type' => 'a type']]], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function () { + 'resourceClass', (new QueryCollection())->withName('action')->withShortName('ShortName'), ['args' => ['customArg' => ['type' => 'a type']]], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function () { }, [ 'actionShortNames' => [ @@ -343,7 +348,7 @@ public function collectionQueryFieldsProvider(): array ], ], ], - 'collection with page-based pagination enabled' => ['resourceClass', (new Query())->withCollection(true)->withShortName('ShortName')->withPaginationType('page')->withFilters(['my_filter']), 'action', [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function () { + 'collection with page-based pagination enabled' => ['resourceClass', (new QueryCollection())->withName('action')->withShortName('ShortName')->withPaginationType('page')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function () { }, [ 'actionShortNames' => [ @@ -370,16 +375,20 @@ public function collectionQueryFieldsProvider(): array /** * @dataProvider mutationFieldsProvider */ - public function testGetMutationFields(string $resourceClass, Operation $operation, string $mutationName, GraphQLType $graphqlType, GraphQLType $inputGraphqlType, ?callable $mutationResolver, array $expectedMutationFields): void + public function testGetMutationFields(string $resourceClass, Operation $operation, GraphQLType $graphqlType, GraphQLType $inputGraphqlType, ?callable $mutationResolver, array $expectedMutationFields): void { - $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, $mutationName, $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); - $this->typeConverterProphecy->convertType(Argument::type(Type::class), true, $mutationName, $resourceClass, $resourceClass, null, 0)->willReturn($inputGraphqlType); + $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, Argument::that(static function (Operation $arg) use ($operation) : bool { + return $arg->getName() === $operation->getName(); + }), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); + $this->typeConverterProphecy->convertType(Argument::type(Type::class), true, Argument::that(static function (Operation $arg) use ($operation) : bool { + return $arg->getName() === $operation->getName(); + }), $resourceClass, $resourceClass, null, 0)->willReturn($inputGraphqlType); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); - $this->typeBuilderProphecy->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $mutationName)->willReturn($graphqlType); - $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$mutationName => $operation])])); - $this->itemMutationResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $mutationName)->willReturn($mutationResolver); + $this->typeBuilderProphecy->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $operation->getName())->willReturn($graphqlType); + $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); + $this->itemMutationResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation->getName())->willReturn($mutationResolver); - $mutationFields = $this->fieldsBuilder->getMutationFields($resourceClass, $operation, $mutationName); + $mutationFields = $this->fieldsBuilder->getMutationFields($resourceClass, $operation); $this->assertEquals($expectedMutationFields, $mutationFields); } @@ -387,7 +396,7 @@ public function testGetMutationFields(string $resourceClass, Operation $operatio public function mutationFieldsProvider(): array { return [ - 'nominal case with deprecation reason' => ['resourceClass', (new Mutation())->withShortName('ShortName')->withDeprecationReason('not useful'), 'action', $graphqlType = new ObjectType(['name' => 'mutation']), $inputGraphqlType = new ObjectType(['name' => 'input']), $mutationResolver = function () { + 'nominal case with deprecation reason' => ['resourceClass', (new Mutation())->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'mutation']), $inputGraphqlType = new ObjectType(['name' => 'input']), $mutationResolver = function () { }, [ 'actionShortName' => [ @@ -407,7 +416,7 @@ public function mutationFieldsProvider(): array ], ], ], - 'custom description' => ['resourceClass', (new Mutation())->withShortName('ShortName')->withDescription('Custom description.'), 'action', $graphqlType = new ObjectType(['name' => 'mutation']), $inputGraphqlType = new ObjectType(['name' => 'input']), $mutationResolver = function () { + 'custom description' => ['resourceClass', (new Mutation())->withName('action')->withShortName('ShortName')->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'mutation']), $inputGraphqlType = new ObjectType(['name' => 'input']), $mutationResolver = function () { }, [ 'actionShortName' => [ @@ -433,16 +442,20 @@ public function mutationFieldsProvider(): array /** * @dataProvider subscriptionFieldsProvider */ - public function testGetSubscriptionFields(string $resourceClass, Operation $operation, string $subscriptionName, GraphQLType $graphqlType, GraphQLType $inputGraphqlType, ?callable $subscriptionResolver, array $expectedSubscriptionFields): void + public function testGetSubscriptionFields(string $resourceClass, Operation $operation, GraphQLType $graphqlType, GraphQLType $inputGraphqlType, ?callable $subscriptionResolver, array $expectedSubscriptionFields): void { - $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, $subscriptionName, $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); - $this->typeConverterProphecy->convertType(Argument::type(Type::class), true, $subscriptionName, $resourceClass, $resourceClass, null, 0)->willReturn($inputGraphqlType); + $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, Argument::that(static function (Operation $arg) use ($operation) : bool { + return $arg->getName() === $operation->getName(); + }), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); + $this->typeConverterProphecy->convertType(Argument::type(Type::class), true, Argument::that(static function (Operation $arg) use ($operation) : bool { + return $arg->getName() === $operation->getName(); + }), $resourceClass, $resourceClass, null, 0)->willReturn($inputGraphqlType); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); - $this->typeBuilderProphecy->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $subscriptionName)->willReturn($graphqlType); - $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$subscriptionName => $operation])])); - $this->itemSubscriptionResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $subscriptionName)->willReturn($subscriptionResolver); + $this->typeBuilderProphecy->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $operation->getName())->willReturn($graphqlType); + $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); + $this->itemSubscriptionResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation->getName())->willReturn($subscriptionResolver); - $subscriptionFields = $this->fieldsBuilder->getSubscriptionFields($resourceClass, $operation, $subscriptionName); + $subscriptionFields = $this->fieldsBuilder->getSubscriptionFields($resourceClass, $operation); $this->assertEquals($expectedSubscriptionFields, $subscriptionFields); } @@ -450,9 +463,9 @@ public function testGetSubscriptionFields(string $resourceClass, Operation $oper public function subscriptionFieldsProvider(): array { return [ - 'mercure not enabled' => ['resourceClass', (new Subscription())->withShortName('ShortName'), 'action', new ObjectType(['name' => 'subscription']), new ObjectType(['name' => 'input']), null, [], + 'mercure not enabled' => ['resourceClass', (new Subscription())->withName('action')->withShortName('ShortName'), new ObjectType(['name' => 'subscription']), new ObjectType(['name' => 'input']), null, [], ], - 'nominal case with deprecation reason' => ['resourceClass', (new Subscription())->withShortName('ShortName')->withMercure(true)->withDeprecationReason('not useful'), 'action', $graphqlType = new ObjectType(['name' => 'subscription']), $inputGraphqlType = new ObjectType(['name' => 'input']), $subscriptionResolver = function () { + 'nominal case with deprecation reason' => ['resourceClass', (new Subscription())->withName('action')->withShortName('ShortName')->withMercure(true)->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'subscription']), $inputGraphqlType = new ObjectType(['name' => 'input']), $subscriptionResolver = function () { }, [ 'actionShortNameSubscribe' => [ @@ -472,7 +485,7 @@ public function subscriptionFieldsProvider(): array ], ], ], - 'custom description' => ['resourceClass', (new Subscription())->withShortName('ShortName')->withMercure(true)->withDescription('Custom description.'), 'action', $graphqlType = new ObjectType(['name' => 'subscription']), $inputGraphqlType = new ObjectType(['name' => 'input']), $subscriptionResolver = function () { + 'custom description' => ['resourceClass', (new Subscription())->withName('action')->withShortName('ShortName')->withMercure(true)->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'subscription']), $inputGraphqlType = new ObjectType(['name' => 'input']), $subscriptionResolver = function () { }, [ 'actionShortNameSubscribe' => [ @@ -498,40 +511,46 @@ public function subscriptionFieldsProvider(): array /** * @dataProvider resourceObjectTypeFieldsProvider */ - public function testGetResourceObjectTypeFields(string $resourceClass, Operation $operation, array $properties, bool $input, ?string $queryName, ?string $mutationName, ?string $subscriptionName, ?array $ioMetadata, array $expectedResourceObjectTypeFields, ?AdvancedNameConverterInterface $advancedNameConverter = null): void + public function testGetResourceObjectTypeFields(string $resourceClass, Operation $operation, array $properties, bool $input, int $depth, ?array $ioMetadata, array $expectedResourceObjectTypeFields, ?AdvancedNameConverterInterface $advancedNameConverter = null): void { $this->propertyNameCollectionFactoryProphecy->create($resourceClass)->willReturn(new PropertyNameCollection(array_keys($properties))); foreach ($properties as $propertyName => $propertyMetadata) { $this->propertyMetadataFactoryProphecy->create($resourceClass, $propertyName, ['normalization_groups' => null, 'denormalization_groups' => null])->willReturn($propertyMetadata); - $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_NULL), Argument::type('bool'), $queryName, '', $resourceClass, $propertyName, 1)->willReturn(null); - $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_CALLABLE), Argument::type('bool'), $queryName, '', $resourceClass, $propertyName, 1)->willReturn('NotRegisteredType'); - $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), $queryName, '', $resourceClass, $propertyName, 1)->willReturn(GraphQLType::string()); - $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), $mutationName, '', $resourceClass, $propertyName, 1)->willReturn(GraphQLType::string()); + $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_NULL), Argument::type('bool'), Argument::that(static function (Operation $arg) use ($operation) : bool { + return $arg->getName() === $operation->getName(); + }), '', $resourceClass, $propertyName, $depth + 1)->willReturn(null); + $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_CALLABLE), Argument::type('bool'), Argument::that(static function (Operation $arg) use ($operation) : bool { + return $arg->getName() === $operation->getName(); + }), '', $resourceClass, $propertyName, $depth + 1)->willReturn('NotRegisteredType'); + $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static function (Operation $arg) use ($operation) : bool { + return $arg->getName() === $operation->getName(); + }), '', $resourceClass, $propertyName, $depth + 1)->willReturn(GraphQLType::string()); if ('propertyObject' === $propertyName) { - $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), $mutationName, 'objectClass', $resourceClass, $propertyName, 1)->willReturn(new ObjectType(['name' => 'objectType'])); + $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static function (Operation $arg) use ($operation) : bool { + return $arg->getName() === $operation->getName(); + }), 'objectClass', $resourceClass, $propertyName, $depth + 1)->willReturn(new ObjectType(['name' => 'objectType'])); $this->resourceMetadataCollectionFactoryProphecy->create('objectClass')->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations(['item_query' => new Query()])])); - $this->itemResolverFactoryProphecy->__invoke('objectClass', $resourceClass, $queryName ?? $mutationName ?? $subscriptionName)->willReturn(static function () { + $this->itemResolverFactoryProphecy->__invoke('objectClass', $resourceClass, $operation->getName())->willReturn(static function () { }); } - $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), $subscriptionName, '', $resourceClass, $propertyName, 1)->willReturn(GraphQLType::string()); - $this->typeConverterProphecy->convertType(Argument::type(Type::class), true, $mutationName, 'subresourceClass', $propertyName, 1)->willReturn(GraphQLType::string()); - $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)), Argument::type('bool'), $queryName ?? $mutationName ?? $subscriptionName, '', $resourceClass, $propertyName, 1)->willReturn(GraphQLType::nonNull(GraphQLType::listOf(GraphQLType::nonNull(GraphQLType::string())))); + $this->typeConverterProphecy->convertType(Argument::type(Type::class), true, Argument::that(static function (Operation $arg) use ($operation) : bool { + return $arg->getName() === $operation->getName(); + }), 'subresourceClass', $propertyName, $depth + 1)->willReturn(GraphQLType::string()); + $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)), Argument::type('bool'), Argument::that(static function (Operation $arg) use ($operation) : bool { + return $arg->getName() === $operation->getName(); + }), '', $resourceClass, $propertyName, $depth + 1)->willReturn(GraphQLType::nonNull(GraphQLType::listOf(GraphQLType::nonNull(GraphQLType::string())))); } $this->typesContainerProphecy->has('NotRegisteredType')->willReturn(false); $this->typesContainerProphecy->all()->willReturn([]); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); - $operation = $queryName ? new Query() : new Mutation(); - if ($subscriptionName) { - $operation = new Subscription(); - } - $this->resourceMetadataCollectionFactoryProphecy->create('resourceClass')->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$queryName ?? $mutationName ?? $subscriptionName => $operation])])); + $this->resourceMetadataCollectionFactoryProphecy->create('resourceClass')->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); $this->resourceMetadataCollectionFactoryProphecy->create('subresourceClass')->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations(['item_query' => new Query()])])); $fieldsBuilder = $this->fieldsBuilder; if ($advancedNameConverter) { $fieldsBuilder = $this->buildFieldsBuilder($advancedNameConverter); } - $resourceObjectTypeFields = $fieldsBuilder->getResourceObjectTypeFields($resourceClass, $operation, $input, $queryName ?? $mutationName ?? $subscriptionName, 0, $ioMetadata); + $resourceObjectTypeFields = $fieldsBuilder->getResourceObjectTypeFields($resourceClass, $operation, $input, $depth, $ioMetadata); $this->assertEquals($expectedResourceObjectTypeFields, $resourceObjectTypeFields); } @@ -549,7 +568,7 @@ public function resourceObjectTypeFieldsProvider(): array 'propertyNotReadable' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(false), 'nameConverted' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withReadable(true)->withWritable(false), ], - false, 'item_query', null, null, null, + false, 0, null, [ 'id' => [ 'type' => GraphQLType::nonNull(GraphQLType::id()), @@ -574,7 +593,7 @@ public function resourceObjectTypeFieldsProvider(): array [ 'field' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withReadable(true)->withWritable(false), ], - false, 'item_query', null, null, null, + false, 0, null, [ 'id' => [ 'type' => GraphQLType::nonNull(GraphQLType::id()), @@ -594,7 +613,7 @@ public function resourceObjectTypeFieldsProvider(): array 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(false), ], - true, 'item_query', null, null, null, + true, 0, null, [ 'id' => [ 'type' => GraphQLType::nonNull(GraphQLType::id()), @@ -614,7 +633,7 @@ public function resourceObjectTypeFieldsProvider(): array new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)), ])->withReadable(true)->withWritable(false), ], - false, 'item_query', null, null, null, + false, 0, null, [ 'id' => [ 'type' => GraphQLType::nonNull(GraphQLType::id()), @@ -628,14 +647,14 @@ public function resourceObjectTypeFieldsProvider(): array ], ], ], - 'mutation non input' => ['resourceClass', new Mutation(), + 'mutation non input' => ['resourceClass', (new Mutation())->withName('mutation'), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), 'propertyReadable' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(true)->withWritable(true), 'propertyObject' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL, false, 'objectClass')])->withReadable(true)->withWritable(true), ], - false, null, 'mutation', null, null, + false, 0, null, [ 'id' => [ 'type' => GraphQLType::nonNull(GraphQLType::id()), @@ -657,14 +676,14 @@ public function resourceObjectTypeFieldsProvider(): array ], ], ], - 'mutation input' => ['resourceClass', new Mutation(), + 'mutation input' => ['resourceClass', (new Mutation())->withName('mutation'), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withDescription('propertyBool description')->withReadable(false)->withWritable(true)->withDeprecationReason('not useful'), 'propertySubresource' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), 'id' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)])->withReadable(false)->withWritable(true), ], - true, null, 'mutation', null, null, + true, 0, null, [ 'id' => [ 'type' => GraphQLType::nonNull(GraphQLType::id()), @@ -693,11 +712,30 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'delete mutation input' => ['resourceClass', new Mutation(), + 'mutation nested input' => ['resourceClass', (new Mutation())->withName('mutation'), + [ + 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), + ], + true, 1, null, + [ + 'id' => [ + 'type' => GraphQLType::id(), + ], + 'propertyBool' => [ + 'type' => GraphQLType::nonNull(GraphQLType::string()), + 'description' => null, + 'args' => [], + 'resolve' => null, + 'deprecationReason' => null, + ], + 'clientMutationId' => GraphQLType::string(), + ], + ], + 'delete mutation input' => ['resourceClass', (new Mutation())->withName('delete'), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], - true, null, 'delete', null, null, + true, 0, null, [ 'id' => [ 'type' => GraphQLType::nonNull(GraphQLType::id()), @@ -705,11 +743,11 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'create mutation input' => ['resourceClass', new Mutation(), + 'create mutation input' => ['resourceClass', (new Mutation())->withName('create'), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], - true, null, 'create', null, null, + true, 0, null, [ 'propertyBool' => [ 'type' => GraphQLType::nonNull(GraphQLType::string()), @@ -721,11 +759,11 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'update mutation input' => ['resourceClass', new Mutation(), + 'update mutation input' => ['resourceClass', (new Mutation())->withName('update'), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], - true, null, 'update', null, null, + true, 0, null, [ 'id' => [ 'type' => GraphQLType::nonNull(GraphQLType::id()), @@ -746,7 +784,7 @@ public function resourceObjectTypeFieldsProvider(): array 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), 'propertyReadable' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(true)->withWritable(true), ], - false, null, null, 'subscription', null, + false, 0, null, [ 'id' => [ 'type' => GraphQLType::nonNull(GraphQLType::id()), @@ -767,7 +805,7 @@ public function resourceObjectTypeFieldsProvider(): array 'propertySubresource' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), 'id' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)])->withReadable(false)->withWritable(true), ], - true, null, null, 'subscription', null, + true, 0, null, [ 'id' => [ 'type' => GraphQLType::nonNull(GraphQLType::id()), @@ -779,13 +817,13 @@ public function resourceObjectTypeFieldsProvider(): array [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], - false, null, 'update', null, ['class' => null], [], + false, 0, ['class' => null], [], ], 'null io metadata input' => ['resourceClass', new Query(), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], - true, null, 'update', null, ['class' => null], + true, 0, ['class' => null], [ 'clientMutationId' => GraphQLType::string(), ], @@ -795,7 +833,7 @@ public function resourceObjectTypeFieldsProvider(): array 'propertyInvalidType' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_NULL)])->withReadable(true)->withWritable(false), 'propertyNotRegisteredType' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_CALLABLE)])->withReadable(true)->withWritable(false), ], - false, 'item_query', null, null, null, + false, 0, null, [ 'id' => [ 'type' => GraphQLType::nonNull(GraphQLType::id()), @@ -816,7 +854,7 @@ public function testResolveResourceArgs(array $args, array $expectedResolvedArgs $this->typeConverterProphecy->resolveType(Argument::type('string'))->willReturn(GraphQLType::string()); - $args = $this->fieldsBuilder->resolveResourceArgs($args, 'operation', 'shortName'); + $args = $this->fieldsBuilder->resolveResourceArgs($args, (new Operation())->withName('operation')->withShortName('shortName')); $this->assertSame($expectedResolvedArgs, $args); } diff --git a/tests/GraphQl/Type/SchemaBuilderTest.php b/tests/GraphQl/Type/SchemaBuilderTest.php index 5ad82af77ad..e771bc18be4 100644 --- a/tests/GraphQl/Type/SchemaBuilderTest.php +++ b/tests/GraphQl/Type/SchemaBuilderTest.php @@ -24,6 +24,7 @@ use ApiPlatform\Metadata\GraphQl\Mutation; use ApiPlatform\Metadata\GraphQl\Operation; use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\GraphQl\Subscription; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; @@ -85,12 +86,24 @@ public function testGetSchema(string $resourceClass, ResourceMetadataCollection $typeFoo->name = 'Foo'; $this->typesContainerProphecy->get('Foo')->willReturn(GraphQLType::listOf($typeFoo)); $this->fieldsBuilderProphecy->getNodeQueryFields()->shouldBeCalled()->willReturn(['node_fields']); - $this->fieldsBuilderProphecy->getItemQueryFields($resourceClass, Argument::type(Operation::class), 'item_query', [])->willReturn(['query' => ['query_fields']]); - $this->fieldsBuilderProphecy->getCollectionQueryFields($resourceClass, Argument::type(Operation::class), 'collection_query', [])->willReturn(['query' => ['query_fields']]); - $this->fieldsBuilderProphecy->getItemQueryFields($resourceClass, Argument::type(Operation::class), 'custom_item_query', [])->willReturn(['custom_item_query' => ['custom_item_query_fields']]); - $this->fieldsBuilderProphecy->getCollectionQueryFields($resourceClass, Argument::type(Operation::class), 'custom_collection_query', [])->willReturn(['custom_collection_query' => ['custom_collection_query_fields']]); - $this->fieldsBuilderProphecy->getMutationFields($resourceClass, Argument::type(Operation::class), 'mutation')->willReturn(['mutation' => ['mutation_fields']]); - $this->fieldsBuilderProphecy->getSubscriptionFields($resourceClass, Argument::type(Operation::class), 'update')->willReturn(['subscription' => ['subscription_fields']]); + $this->fieldsBuilderProphecy->getItemQueryFields($resourceClass, Argument::that(static function (Operation $arg): bool { + return $arg->getName() === 'item_query'; + }), [])->willReturn(['query' => ['query_fields']]); + $this->fieldsBuilderProphecy->getCollectionQueryFields($resourceClass, Argument::that(static function (Operation $arg): bool { + return $arg->getName() === 'collection_query'; + }), [])->willReturn(['query' => ['query_fields']]); + $this->fieldsBuilderProphecy->getItemQueryFields($resourceClass, Argument::that(static function (Operation $arg): bool { + return $arg->getName() === 'custom_item_query'; + }), [])->willReturn(['custom_item_query' => ['custom_item_query_fields']]); + $this->fieldsBuilderProphecy->getCollectionQueryFields($resourceClass, Argument::that(static function (Operation $arg): bool { + return $arg->getName() === 'custom_collection_query'; + }), [])->willReturn(['custom_collection_query' => ['custom_collection_query_fields']]); + $this->fieldsBuilderProphecy->getMutationFields($resourceClass, Argument::that(static function (Operation $arg): bool { + return $arg->getName() === 'mutation'; + }))->willReturn(['mutation' => ['mutation_fields']]); + $this->fieldsBuilderProphecy->getSubscriptionFields($resourceClass, Argument::that(static function (Operation $arg): bool { + return $arg->getName() === 'update'; + }))->willReturn(['subscription' => ['subscription_fields']]); $this->resourceNameCollectionFactoryProphecy->create()->willReturn(new ResourceNameCollection([$resourceClass])); $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn($resourceMetadata); @@ -123,7 +136,7 @@ public function schemaProvider(): array ], ]), null, null, ], - 'collection query' => [$resourceClass = 'resourceClass', new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations(['collection_query' => (new Query())->withCollection(true)])]), + 'collection query' => [$resourceClass = 'resourceClass', new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations(['collection_query' => new QueryCollection()])]), new ObjectType([ 'name' => 'Query', 'fields' => [ @@ -132,7 +145,7 @@ public function schemaProvider(): array ], ]), null, null, ], - 'custom item query' => [$resourceClass = 'resourceClass', new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations(['custom_item_query' => (new Query())->withResolver('item_query_resolver')])]), + 'custom item query' => [$resourceClass = 'resourceClass', new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations(['custom_item_query' => (new Query())->withName('custom_item_query')->withResolver('item_query_resolver')])]), new ObjectType([ 'name' => 'Query', 'fields' => [ @@ -141,7 +154,7 @@ public function schemaProvider(): array ], ]), null, null, ], - 'custom collection query' => [$resourceClass = 'resourceClass', new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations(['custom_collection_query' => (new Query())->withCollection(true)->withResolver('collection_query_resolver')])]), + 'custom collection query' => [$resourceClass = 'resourceClass', new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations(['custom_collection_query' => (new QueryCollection())->withName('custom_collection_query')->withResolver('collection_query_resolver')])]), new ObjectType([ 'name' => 'Query', 'fields' => [ @@ -150,7 +163,7 @@ public function schemaProvider(): array ], ]), null, null, ], - 'mutation' => [$resourceClass = 'resourceClass', new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations(['mutation' => new Mutation()])]), + 'mutation' => [$resourceClass = 'resourceClass', new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations(['mutation' => (new Mutation())->withName('mutation')])]), new ObjectType([ 'name' => 'Query', 'fields' => [ @@ -165,7 +178,7 @@ public function schemaProvider(): array ]), null, ], - 'subscription' => [$resourceClass = 'resourceClass', new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations(['update' => (new Subscription())->withMercure(true)])]), + 'subscription' => [$resourceClass = 'resourceClass', new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations(['update' => (new Subscription())->withName('update')->withMercure(true)])]), new ObjectType([ 'name' => 'Query', 'fields' => [ diff --git a/tests/GraphQl/Type/TypeBuilderTest.php b/tests/GraphQl/Type/TypeBuilderTest.php index f4576bcf879..19b0feaaaaa 100644 --- a/tests/GraphQl/Type/TypeBuilderTest.php +++ b/tests/GraphQl/Type/TypeBuilderTest.php @@ -21,7 +21,9 @@ use ApiPlatform\GraphQl\Type\TypesContainerInterface; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GraphQl\Mutation; +use ApiPlatform\Metadata\GraphQl\Operation; use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\GraphQl\Subscription; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; @@ -81,14 +83,15 @@ protected function setUp(): void public function testGetResourceObjectType(): void { - $resourceMetadata = new ResourceMetadataCollection('resourceClass', [(new ApiResource())->withGraphQlOperations(['item_query' => (new Query())->withShortName('shortName')->withDescription('description')])]); + $resourceMetadataCollection = new ResourceMetadataCollection('resourceClass', []); $this->typesContainerProphecy->has('shortName')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('shortName', Argument::type(ObjectType::class))->shouldBeCalled(); $this->typesContainerProphecy->has('Node')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('Node', Argument::type(InterfaceType::class))->shouldBeCalled(); + $operation = (new Query())->withShortName('shortName')->withDescription('description'); /** @var ObjectType $resourceObjectType */ - $resourceObjectType = $this->typeBuilder->getResourceObjectType('resourceClass', $resourceMetadata, 'item_query', false); + $resourceObjectType = $this->typeBuilder->getResourceObjectType('resourceClass', $resourceMetadataCollection, $operation, false); $this->assertSame('shortName', $resourceObjectType->name); $this->assertSame('description', $resourceObjectType->description); $this->assertSame($this->defaultFieldResolver, $resourceObjectType->resolveFieldFn); @@ -96,21 +99,22 @@ public function testGetResourceObjectType(): void $this->assertArrayHasKey('fields', $resourceObjectType->config); $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderInterface::class); - $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $resourceMetadata->getGraphQlOperation('item_query'), false, 'item_query', 0, null)->shouldBeCalled(); + $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, false, 0, null)->shouldBeCalled(); $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); $resourceObjectType->config['fields'](); } public function testGetResourceObjectTypeOutputClass(): void { - $resourceMetadata = new ResourceMetadataCollection('resourceClass', [(new ApiResource())->withGraphQlOperations(['item_query' => (new Query())->withShortName('shortName')->withDescription('description')->withOutput(['class' => 'outputClass'])])]); + $resourceMetadata = new ResourceMetadataCollection('resourceClass', []); $this->typesContainerProphecy->has('shortName')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('shortName', Argument::type(ObjectType::class))->shouldBeCalled(); $this->typesContainerProphecy->has('Node')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('Node', Argument::type(InterfaceType::class))->shouldBeCalled(); + $operation = (new Query())->withShortName('shortName')->withDescription('description')->withOutput(['class' => 'outputClass']); /** @var ObjectType $resourceObjectType */ - $resourceObjectType = $this->typeBuilder->getResourceObjectType('resourceClass', $resourceMetadata, 'item_query', false); + $resourceObjectType = $this->typeBuilder->getResourceObjectType('resourceClass', $resourceMetadata, $operation, false); $this->assertSame('shortName', $resourceObjectType->name); $this->assertSame('description', $resourceObjectType->description); $this->assertSame($this->defaultFieldResolver, $resourceObjectType->resolveFieldFn); @@ -118,7 +122,7 @@ public function testGetResourceObjectTypeOutputClass(): void $this->assertArrayHasKey('fields', $resourceObjectType->config); $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderInterface::class); - $fieldsBuilderProphecy->getResourceObjectTypeFields('outputClass', $resourceMetadata->getGraphQlOperation('item_query'), false, 'item_query', 0, ['class' => 'outputClass'])->shouldBeCalled(); + $fieldsBuilderProphecy->getResourceObjectTypeFields('outputClass', $operation, false, 0, ['class' => 'outputClass'])->shouldBeCalled(); $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); $resourceObjectType->config['fields'](); } @@ -126,11 +130,11 @@ public function testGetResourceObjectTypeOutputClass(): void /** * @dataProvider resourceObjectTypeQuerySerializationGroupsProvider */ - public function testGetResourceObjectTypeQuerySerializationGroups(string $itemSerializationGroup, string $collectionSerializationGroup, string $shortName, string $queryName) + public function testGetResourceObjectTypeQuerySerializationGroups(string $itemSerializationGroup, string $collectionSerializationGroup, Operation $operation, string $shortName) { $resourceMetadata = new ResourceMetadataCollection('resourceClass', [(new ApiResource())->withGraphQlOperations([ - 'item_query' => (new Query())->withShortName('shortName')->withDescription('description')->withNormalizationContext(['groups' => [$itemSerializationGroup]]), - 'collection_query' => (new Query())->withShortName('shortName')->withDescription('description')->withCollection(true)->withNormalizationContext(['groups' => [$collectionSerializationGroup]]), + 'item_query' => (new Query())->withShortName('shortName')->withNormalizationContext(['groups' => [$itemSerializationGroup]]), + 'collection_query' => (new QueryCollection())->withShortName('shortName')->withNormalizationContext(['groups' => [$collectionSerializationGroup]]), ])]); $this->typesContainerProphecy->has($shortName)->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set($shortName, Argument::type(ObjectType::class))->shouldBeCalled(); @@ -138,7 +142,7 @@ public function testGetResourceObjectTypeQuerySerializationGroups(string $itemSe $this->typesContainerProphecy->set('Node', Argument::type(InterfaceType::class))->shouldBeCalled(); /** @var ObjectType $resourceObjectType */ - $resourceObjectType = $this->typeBuilder->getResourceObjectType('resourceClass', $resourceMetadata, $queryName, false); + $resourceObjectType = $this->typeBuilder->getResourceObjectType('resourceClass', $resourceMetadata, $operation, false); $this->assertSame($shortName, $resourceObjectType->name); } @@ -148,34 +152,35 @@ public function resourceObjectTypeQuerySerializationGroupsProvider(): array 'same serialization groups for item_query and collection_query' => [ 'group', 'group', + (new Query())->withShortName('shortName')->withNormalizationContext(['groups' => ['group']]), 'shortName', - 'item_query', ], 'different serialization groups for item_query and collection_query when using item_query' => [ 'item_group', 'collection_group', + (new Query())->withShortName('shortName')->withNormalizationContext(['groups' => ['item_group']]), 'shortNameItem', - 'item_query', ], 'different serialization groups for item_query and collection_query when using collection_query' => [ 'item_group', 'collection_group', + (new QueryCollection())->withName('collection_query')->withShortName('shortName')->withNormalizationContext(['groups' => ['collection_group']]), 'shortNameCollection', - 'collection_query', ], ]; } public function testGetResourceObjectTypeInput(): void { - $resourceMetadata = new ResourceMetadataCollection('resourceClass', [(new ApiResource())->withGraphQlOperations(['custom' => (new Mutation())->withShortName('shortName')->withDescription('description')])]); + $resourceMetadata = new ResourceMetadataCollection('resourceClass', []); $this->typesContainerProphecy->has('customShortNameInput')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('customShortNameInput', Argument::type(NonNull::class))->shouldBeCalled(); $this->typesContainerProphecy->has('Node')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('Node', Argument::type(InterfaceType::class))->shouldBeCalled(); + $operation = (new Mutation())->withName('custom')->withShortName('shortName')->withDescription('description'); /** @var NonNull $resourceObjectType */ - $resourceObjectType = $this->typeBuilder->getResourceObjectType('resourceClass', $resourceMetadata, 'custom', true); + $resourceObjectType = $this->typeBuilder->getResourceObjectType('resourceClass', $resourceMetadata, $operation, true); /** @var InputObjectType $wrappedType */ $wrappedType = $resourceObjectType->getWrappedType(); $this->assertInstanceOf(InputObjectType::class, $wrappedType); @@ -185,21 +190,47 @@ public function testGetResourceObjectTypeInput(): void $this->assertArrayHasKey('fields', $wrappedType->config); $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderInterface::class); - $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $resourceMetadata->getGraphQlOperation('custom'), true, 'custom', 0, null)->shouldBeCalled(); + $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, true, 0, null)->shouldBeCalled(); + $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); + $wrappedType->config['fields'](); + } + + public function testGetResourceObjectTypeNestedInput(): void + { + $resourceMetadata = new ResourceMetadataCollection('resourceClass', []); + $this->typesContainerProphecy->has('customShortNameNestedInput')->shouldBeCalled()->willReturn(false); + $this->typesContainerProphecy->set('customShortNameNestedInput', Argument::type(NonNull::class))->shouldBeCalled(); + $this->typesContainerProphecy->has('Node')->shouldBeCalled()->willReturn(false); + $this->typesContainerProphecy->set('Node', Argument::type(InterfaceType::class))->shouldBeCalled(); + + $operation = (new Mutation())->withName('custom')->withShortName('shortName')->withDescription('description'); + /** @var NonNull $resourceObjectType */ + $resourceObjectType = $this->typeBuilder->getResourceObjectType('resourceClass', $resourceMetadata, $operation, true, false, 1); + /** @var InputObjectType $wrappedType */ + $wrappedType = $resourceObjectType->getWrappedType(); + $this->assertInstanceOf(InputObjectType::class, $wrappedType); + $this->assertSame('customShortNameNestedInput', $wrappedType->name); + $this->assertSame('description', $wrappedType->description); + $this->assertArrayHasKey('interfaces', $wrappedType->config); + $this->assertArrayHasKey('fields', $wrappedType->config); + + $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderInterface::class); + $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, true, 1, null)->shouldBeCalled(); $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); $wrappedType->config['fields'](); } public function testGetResourceObjectTypeCustomMutationInputArgs(): void { - $resourceMetadata = new ResourceMetadataCollection('resourceClass', [(new ApiResource())->withGraphQlOperations(['custom' => (new Mutation())->withShortName('shortName')->withDescription('description')->withArgs([])])]); + $resourceMetadata = new ResourceMetadataCollection('resourceClass', []); $this->typesContainerProphecy->has('customShortNameInput')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('customShortNameInput', Argument::type(NonNull::class))->shouldBeCalled(); $this->typesContainerProphecy->has('Node')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('Node', Argument::type(InterfaceType::class))->shouldBeCalled(); + $operation = (new Mutation())->withName('custom')->withShortName('shortName')->withDescription('description')->withArgs([]); /** @var NonNull $resourceObjectType */ - $resourceObjectType = $this->typeBuilder->getResourceObjectType('resourceClass', $resourceMetadata, 'custom', true); + $resourceObjectType = $this->typeBuilder->getResourceObjectType('resourceClass', $resourceMetadata, $operation, true); /** @var InputObjectType $wrappedType */ $wrappedType = $resourceObjectType->getWrappedType(); $this->assertInstanceOf(InputObjectType::class, $wrappedType); @@ -209,9 +240,9 @@ public function testGetResourceObjectTypeCustomMutationInputArgs(): void $this->assertArrayHasKey('fields', $wrappedType->config); $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderInterface::class); - $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $resourceMetadata->getGraphQlOperation('custom'), true, 'custom', 0, null) + $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, true, 0, null) ->shouldBeCalled()->willReturn(['clientMutationId' => GraphQLType::string()]); - $fieldsBuilderProphecy->resolveResourceArgs([], 'custom', 'shortName')->shouldBeCalled(); + $fieldsBuilderProphecy->resolveResourceArgs([], $operation)->shouldBeCalled(); $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); $wrappedType->config['fields'](); } @@ -219,7 +250,7 @@ public function testGetResourceObjectTypeCustomMutationInputArgs(): void public function testGetResourceObjectTypeMutation(): void { $resourceMetadata = new ResourceMetadataCollection('resourceClass', [(new ApiResource())->withGraphQlOperations([ - 'create' => (new Mutation())->withShortName('shortName')->withDescription('description'), + 'create' => (new Mutation())->withName('create')->withShortName('shortName')->withDescription('description'), 'item_query' => (new Query())->withShortName('shortName')->withDescription('description'), ]), ]); @@ -228,8 +259,9 @@ public function testGetResourceObjectTypeMutation(): void $this->typesContainerProphecy->has('Node')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('Node', Argument::type(InterfaceType::class))->shouldBeCalled(); + $operation = (new Mutation())->withName('create')->withShortName('shortName')->withDescription('description'); /** @var ObjectType $resourceObjectType */ - $resourceObjectType = $this->typeBuilder->getResourceObjectType('resourceClass', $resourceMetadata, 'create', false); + $resourceObjectType = $this->typeBuilder->getResourceObjectType('resourceClass', $resourceMetadata, $operation, false); $this->assertSame('createShortNamePayload', $resourceObjectType->name); $this->assertSame('description', $resourceObjectType->description); $this->assertSame($this->defaultFieldResolver, $resourceObjectType->resolveFieldFn); @@ -251,15 +283,16 @@ public function testGetResourceObjectTypeMutationWrappedType(): void { $resourceMetadata = new ResourceMetadataCollection('resourceClass', [(new ApiResource())->withGraphQlOperations([ 'item_query' => (new Query())->withShortName('shortName')->withDescription('description')->withNormalizationContext(['groups' => ['item_query']]), - 'create' => (new Mutation())->withShortName('shortName')->withDescription('description')->withNormalizationContext(['groups' => ['create']]), + 'create' => (new Mutation())->withName('create')->withShortName('shortName')->withDescription('description')->withNormalizationContext(['groups' => ['create']]), ])]); $this->typesContainerProphecy->has('createShortNamePayload')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('createShortNamePayload', Argument::type(ObjectType::class))->shouldBeCalled(); $this->typesContainerProphecy->has('Node')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('Node', Argument::type(InterfaceType::class))->shouldBeCalled(); + $operation = (new Mutation())->withName('create')->withShortName('shortName')->withDescription('description')->withNormalizationContext(['groups' => ['create']]); /** @var ObjectType $resourceObjectType */ - $resourceObjectType = $this->typeBuilder->getResourceObjectType('resourceClass', $resourceMetadata, 'create', false); + $resourceObjectType = $this->typeBuilder->getResourceObjectType('resourceClass', $resourceMetadata, $operation, false); $this->assertSame('createShortNamePayload', $resourceObjectType->name); $this->assertSame('description', $resourceObjectType->description); $this->assertSame($this->defaultFieldResolver, $resourceObjectType->resolveFieldFn); @@ -285,21 +318,22 @@ public function testGetResourceObjectTypeMutationWrappedType(): void $this->assertArrayHasKey('fields', $wrappedType->config); $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderInterface::class); - $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $resourceMetadata->getGraphQlOperation('create'), false, 'create', 0, null)->shouldBeCalled(); + $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, false, 0, null)->shouldBeCalled(); $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); $wrappedType->config['fields'](); } public function testGetResourceObjectTypeMutationNested(): void { - $resourceMetadata = new ResourceMetadataCollection('resourceClass', [(new ApiResource())->withGraphQlOperations(['create' => (new Mutation())->withShortName('shortName')->withDescription('description')])]); + $resourceMetadata = new ResourceMetadataCollection('resourceClass', []); $this->typesContainerProphecy->has('createShortNameNestedPayload')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('createShortNameNestedPayload', Argument::type(ObjectType::class))->shouldBeCalled(); $this->typesContainerProphecy->has('Node')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('Node', Argument::type(InterfaceType::class))->shouldBeCalled(); + $operation = (new Mutation())->withName('create')->withShortName('shortName')->withDescription('description'); /** @var ObjectType $resourceObjectType */ - $resourceObjectType = $this->typeBuilder->getResourceObjectType('resourceClass', $resourceMetadata, 'create', false, false, 1); + $resourceObjectType = $this->typeBuilder->getResourceObjectType('resourceClass', $resourceMetadata, $operation, false, false, 1); $this->assertSame('createShortNameNestedPayload', $resourceObjectType->name); $this->assertSame('description', $resourceObjectType->description); $this->assertSame($this->defaultFieldResolver, $resourceObjectType->resolveFieldFn); @@ -307,7 +341,7 @@ public function testGetResourceObjectTypeMutationNested(): void $this->assertArrayHasKey('fields', $resourceObjectType->config); $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderInterface::class); - $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $resourceMetadata->getGraphQlOperation('create'), false, 'create', 1, null)->shouldBeCalled(); + $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, false, 1, null)->shouldBeCalled(); $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); $resourceObjectType->config['fields'](); } @@ -315,7 +349,7 @@ public function testGetResourceObjectTypeMutationNested(): void public function testGetResourceObjectTypeSubscription(): void { $resourceMetadata = new ResourceMetadataCollection('resourceClass', [(new ApiResource())->withGraphQlOperations([ - 'update' => (new Subscription())->withShortName('shortName')->withDescription('description')->withMercure(true), + 'update' => (new Subscription())->withName('update')->withShortName('shortName')->withDescription('description')->withMercure(true), 'item_query' => (new Query())->withShortName('shortName')->withDescription('description'), ]), ]); @@ -324,8 +358,9 @@ public function testGetResourceObjectTypeSubscription(): void $this->typesContainerProphecy->has('Node')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('Node', Argument::type(InterfaceType::class))->shouldBeCalled(); + $operation = (new Subscription())->withName('update')->withShortName('shortName')->withDescription('description')->withMercure(true); /** @var ObjectType $resourceObjectType */ - $resourceObjectType = $this->typeBuilder->getResourceObjectType('resourceClass', $resourceMetadata, 'update', false); + $resourceObjectType = $this->typeBuilder->getResourceObjectType('resourceClass', $resourceMetadata, $operation, false); $this->assertSame('updateShortNameSubscriptionPayload', $resourceObjectType->name); $this->assertSame('description', $resourceObjectType->description); $this->assertSame($this->defaultFieldResolver, $resourceObjectType->resolveFieldFn); @@ -349,15 +384,16 @@ public function testGetResourceObjectTypeSubscriptionWrappedType(): void { $resourceMetadata = new ResourceMetadataCollection('resourceClass', [(new ApiResource())->withGraphQlOperations([ 'item_query' => (new Query())->withShortName('shortName')->withDescription('description')->withNormalizationContext(['groups' => ['item_query']]), - 'update' => (new Subscription())->withShortName('shortName')->withDescription('description')->withNormalizationContext(['groups' => ['update']]), + 'update' => (new Subscription())->withName('update')->withShortName('shortName')->withDescription('description')->withNormalizationContext(['groups' => ['update']]), ])]); $this->typesContainerProphecy->has('updateShortNameSubscriptionPayload')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('updateShortNameSubscriptionPayload', Argument::type(ObjectType::class))->shouldBeCalled(); $this->typesContainerProphecy->has('Node')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('Node', Argument::type(InterfaceType::class))->shouldBeCalled(); + $operation = (new Subscription())->withName('update')->withShortName('shortName')->withDescription('description')->withNormalizationContext(['groups' => ['update']]); /** @var ObjectType $resourceObjectType */ - $resourceObjectType = $this->typeBuilder->getResourceObjectType('resourceClass', $resourceMetadata, 'update', false); + $resourceObjectType = $this->typeBuilder->getResourceObjectType('resourceClass', $resourceMetadata, $operation, false); $this->assertSame('updateShortNameSubscriptionPayload', $resourceObjectType->name); $this->assertSame('description', $resourceObjectType->description); $this->assertSame($this->defaultFieldResolver, $resourceObjectType->resolveFieldFn); @@ -384,21 +420,22 @@ public function testGetResourceObjectTypeSubscriptionWrappedType(): void $this->assertArrayHasKey('fields', $wrappedType->config); $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderInterface::class); - $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $resourceMetadata->getGraphQlOperation('update'), false, 'update', 0, null)->shouldBeCalled(); + $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, false, 0, null)->shouldBeCalled(); $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); $wrappedType->config['fields'](); } public function testGetResourceObjectTypeSubscriptionNested(): void { - $resourceMetadata = new ResourceMetadataCollection('resourceClass', [(new ApiResource())->withGraphQlOperations(['update' => (new Subscription())->withShortName('shortName')->withDescription('description')->withMercure(true)])]); + $resourceMetadata = new ResourceMetadataCollection('resourceClass', [(new ApiResource())->withGraphQlOperations([])]); $this->typesContainerProphecy->has('updateShortNameSubscriptionNestedPayload')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('updateShortNameSubscriptionNestedPayload', Argument::type(ObjectType::class))->shouldBeCalled(); $this->typesContainerProphecy->has('Node')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('Node', Argument::type(InterfaceType::class))->shouldBeCalled(); + $operation = (new Subscription())->withName('update')->withShortName('shortName')->withDescription('description')->withMercure(true); /** @var ObjectType $resourceObjectType */ - $resourceObjectType = $this->typeBuilder->getResourceObjectType('resourceClass', $resourceMetadata, 'update', false, false, 1); + $resourceObjectType = $this->typeBuilder->getResourceObjectType('resourceClass', $resourceMetadata, $operation, false, false, 1); $this->assertSame('updateShortNameSubscriptionNestedPayload', $resourceObjectType->name); $this->assertSame('description', $resourceObjectType->description); $this->assertSame($this->defaultFieldResolver, $resourceObjectType->resolveFieldFn); @@ -406,7 +443,7 @@ public function testGetResourceObjectTypeSubscriptionNested(): void $this->assertArrayHasKey('fields', $resourceObjectType->config); $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderInterface::class); - $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $resourceMetadata->getGraphQlOperation('update'), false, 'update', 1, null)->shouldBeCalled(); + $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, false, 1, null)->shouldBeCalled(); $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); $resourceObjectType->config['fields'](); } diff --git a/tests/GraphQl/Type/TypeConverterTest.php b/tests/GraphQl/Type/TypeConverterTest.php index 7950525d650..df08e0d5611 100644 --- a/tests/GraphQl/Type/TypeConverterTest.php +++ b/tests/GraphQl/Type/TypeConverterTest.php @@ -13,19 +13,23 @@ namespace ApiPlatform\Core\Tests\GraphQl\Type; +use ApiPlatform\Core\Metadata\Property\PropertyMetadata; use ApiPlatform\Core\Tests\ProphecyTrait; use ApiPlatform\Exception\ResourceClassNotFoundException; use ApiPlatform\GraphQl\Type\TypeBuilderInterface; use ApiPlatform\GraphQl\Type\TypeConverter; use ApiPlatform\GraphQl\Type\TypesContainerInterface; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GraphQl\Operation; use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\Tests\Fixtures\TestBundle\GraphQl\Type\Definition\DateTimeType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type as GraphQLType; use PHPUnit\Framework\TestCase; +use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; use Symfony\Component\PropertyInfo\Type; @@ -45,6 +49,9 @@ class TypeConverterTest extends TestCase /** @var ObjectProphecy */ private $resourceMetadataCollectionFactoryProphecy; + /** @var ObjectProphecy */ + private $propertyMetadataFactoryProphecy; + /** @var TypeConverter */ private $typeConverter; @@ -56,7 +63,8 @@ protected function setUp(): void $this->typeBuilderProphecy = $this->prophesize(TypeBuilderInterface::class); $this->typesContainerProphecy = $this->prophesize(TypesContainerInterface::class); $this->resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $this->typeConverter = new TypeConverter($this->typeBuilderProphecy->reveal(), $this->typesContainerProphecy->reveal(), $this->resourceMetadataCollectionFactoryProphecy->reveal()); + $this->propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $this->typeConverter = new TypeConverter($this->typeBuilderProphecy->reveal(), $this->typesContainerProphecy->reveal(), $this->resourceMetadataCollectionFactoryProphecy->reveal(), $this->propertyMetadataFactoryProphecy->reveal()); } /** @@ -68,7 +76,7 @@ public function testConvertType(Type $type, bool $input, int $depth, $expectedGr { $this->typeBuilderProphecy->isCollection($type)->willReturn(false); - $graphqlType = $this->typeConverter->convertType($type, $input, 'test', 'resourceClass', 'rootClass', null, $depth); + $graphqlType = $this->typeConverter->convertType($type, $input, (new Operation())->withName('test'), 'resourceClass', 'rootClass', null, $depth); $this->assertEquals($expectedGraphqlType, $graphqlType); } @@ -81,7 +89,6 @@ public function convertTypeProvider(): array [new Type(Type::BUILTIN_TYPE_STRING), false, 0, GraphQLType::string()], [new Type(Type::BUILTIN_TYPE_ARRAY), false, 0, 'Iterable'], [new Type(Type::BUILTIN_TYPE_ITERABLE), false, 0, 'Iterable'], - [new Type(Type::BUILTIN_TYPE_OBJECT), true, 1, GraphQLType::string()], [new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateTimeInterface::class), false, 0, GraphQLType::string()], [new Type(Type::BUILTIN_TYPE_OBJECT), false, 0, null], [new Type(Type::BUILTIN_TYPE_CALLABLE), false, 0, null], @@ -97,7 +104,7 @@ public function testConvertTypeNoGraphQlResourceMetadata(): void $this->typeBuilderProphecy->isCollection($type)->shouldBeCalled()->willReturn(false); $this->resourceMetadataCollectionFactoryProphecy->create('dummy')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('dummy', [new ApiResource()])); - $graphqlType = $this->typeConverter->convertType($type, false, 'test', 'resourceClass', 'rootClass', null, 0); + $graphqlType = $this->typeConverter->convertType($type, false, (new Operation())->withName('test'), 'resourceClass', 'rootClass', null, 0); $this->assertNull($graphqlType); } @@ -111,7 +118,7 @@ public function testConvertTypeNodeResource(): void $this->expectException(\UnexpectedValueException::class); $this->expectExceptionMessage('A "Node" resource cannot be used with GraphQL because the type is already used by the Relay specification.'); - $this->typeConverter->convertType($type, false, 'test', 'resourceClass', 'rootClass', null, 0); + $this->typeConverter->convertType($type, false, (new Operation())->withName('test'), 'resourceClass', 'rootClass', null, 0); } public function testConvertTypeResourceClassNotFound(): void @@ -121,22 +128,52 @@ public function testConvertTypeResourceClassNotFound(): void $this->typeBuilderProphecy->isCollection($type)->shouldBeCalled()->willReturn(false); $this->resourceMetadataCollectionFactoryProphecy->create('dummy')->shouldBeCalled()->willThrow(new ResourceClassNotFoundException()); - $graphqlType = $this->typeConverter->convertType($type, false, 'test', 'resourceClass', 'rootClass', null, 0); + $graphqlType = $this->typeConverter->convertType($type, false, (new Operation())->withName('test'), 'resourceClass', 'rootClass', null, 0); $this->assertNull($graphqlType); } + public function testConvertTypeResourceIri(): void + { + $type = new Type(Type::BUILTIN_TYPE_OBJECT, false, 'dummy'); + + $graphqlResourceMetadata = new ResourceMetadataCollection('dummy', [(new ApiResource())->withGraphQlOperations(['test' => new Query()])]); + $this->resourceMetadataCollectionFactoryProphecy->create('dummy')->willReturn($graphqlResourceMetadata); + $this->typeBuilderProphecy->isCollection($type)->willReturn(false); + $this->propertyMetadataFactoryProphecy->create('rootClass', 'dummyProperty', Argument::type('array'))->shouldBeCalled()->willReturn((new PropertyMetadata())->withWritableLink(false)); + + $graphqlType = $this->typeConverter->convertType($type, true, (new Operation())->withName('test'), 'dummy', 'rootClass', 'dummyProperty', 1); + $this->assertEquals(GraphQLType::string(), $graphqlType); + } + + public function testConvertTypeInputResource(): void + { + $type = new Type(Type::BUILTIN_TYPE_OBJECT, false, 'dummy'); + $operation = new Query(); + $graphqlResourceMetadata = new ResourceMetadataCollection('dummy', [(new ApiResource())->withGraphQlOperations(['test' => $operation])]); + $expectedGraphqlType = new ObjectType(['name' => 'resourceObjectType']); + + $this->resourceMetadataCollectionFactoryProphecy->create('dummy')->willReturn($graphqlResourceMetadata); + $this->typeBuilderProphecy->isCollection($type)->willReturn(false); + $this->propertyMetadataFactoryProphecy->create('rootClass', 'dummyProperty', Argument::type('array'))->shouldBeCalled()->willReturn((new PropertyMetadata())->withWritableLink(true)); + $this->typeBuilderProphecy->getResourceObjectType('dummy', $graphqlResourceMetadata, $operation, true, false, 1)->shouldBeCalled()->willReturn($expectedGraphqlType); + + $graphqlType = $this->typeConverter->convertType($type, true, (new Operation())->withName('test'), 'dummy', 'rootClass', 'dummyProperty', 1); + $this->assertEquals($expectedGraphqlType, $graphqlType); + } + /** * @dataProvider convertTypeResourceProvider */ - public function testConvertTypeResource(Type $type, ObjectType $expectedGraphqlType): void + public function testConvertTypeCollectionResource(Type $type, ObjectType $expectedGraphqlType): void { - $graphqlResourceMetadata = new ResourceMetadataCollection('dummyValue', [(new ApiResource())->withShortName('DummyValue')->withGraphQlOperations(['test' => new Query()])]); + $operation = (new Query())->withName('test'); + $graphqlResourceMetadata = new ResourceMetadataCollection('dummyValue', [(new ApiResource())->withShortName('DummyValue')->withGraphQlOperations(['test' => $operation])]); $this->typeBuilderProphecy->isCollection($type)->shouldBeCalled()->willReturn(true); $this->resourceMetadataCollectionFactoryProphecy->create('dummyValue')->shouldBeCalled()->willReturn($graphqlResourceMetadata); - $this->typeBuilderProphecy->getResourceObjectType('dummyValue', $graphqlResourceMetadata, 'test', false, false, 0)->shouldBeCalled()->willReturn($expectedGraphqlType); + $this->typeBuilderProphecy->getResourceObjectType('dummyValue', $graphqlResourceMetadata, $operation, false, false, 0)->shouldBeCalled()->willReturn($expectedGraphqlType); - $graphqlType = $this->typeConverter->convertType($type, false, 'test', 'resourceClass', 'rootClass', null, 0); + $graphqlType = $this->typeConverter->convertType($type, false, (new Operation())->withName('test'), 'resourceClass', 'rootClass', null, 0); $this->assertEquals($expectedGraphqlType, $graphqlType); }