Skip to content

feat(graphql): use denormalization groups to allow creation of relation in mutation #4434

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions features/graphql/mutation.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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 {
Expand All @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@
<argument type="service" id="api_platform.graphql.type_builder" />
<argument type="service" id="api_platform.graphql.types_container" />
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory" />
<argument type="service" id="api_platform.metadata.property.metadata_factory" />
</service>

<service id="api_platform.graphql.type_builder" class="ApiPlatform\GraphQl\Type\TypeBuilder" public="false">
Expand Down
3 changes: 3 additions & 0 deletions src/Core/Serializer/AbstractItemNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
115 changes: 50 additions & 65 deletions src/GraphQl/Type/FieldsBuilder.php

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions src/GraphQl/Type/FieldsBuilderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
12 changes: 6 additions & 6 deletions src/GraphQl/Type/SchemaBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
Expand Down
30 changes: 17 additions & 13 deletions src/GraphQl/Type/TypeBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion src/GraphQl/Type/TypeBuilderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand Down
49 changes: 38 additions & 11 deletions src/GraphQl/Type/TypeConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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:
Expand All @@ -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;
}
Expand All @@ -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) &&
Expand All @@ -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;
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/GraphQl/Type/TypeConverterInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion tests/Fixtures/TestBundle/Document/RelatedDummy.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
* @author Kévin Dunglas <[email protected]>
* @author Alexandre Delplace <[email protected]>
*
* @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
Expand Down
Loading