Skip to content

Commit

Permalink
feat(graphql): use denormalization groups to allow creation of relati…
Browse files Browse the repository at this point in the history
…on in mutation
  • Loading branch information
alanpoulain committed Sep 8, 2021
1 parent 288851e commit f9703f5
Show file tree
Hide file tree
Showing 19 changed files with 411 additions and 249 deletions.
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 <dunglas@gmail.com>
* @author Alexandre Delplace <alexandre.delplacemille@gmail.com>
*
* @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

0 comments on commit f9703f5

Please sign in to comment.