Skip to content

Commit d70925d

Browse files
alanpoulaindunglas
authored andcommitted
Delete mutation for an item
1 parent babe924 commit d70925d

File tree

9 files changed

+359
-20
lines changed

9 files changed

+359
-20
lines changed

features/graphql/mutation.feature

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
Feature: GraphQL mutation support
2+
3+
@createSchema
4+
Scenario: Introspect types
5+
When I send the following GraphQL request:
6+
"""
7+
{
8+
__type(name: "Mutation") {
9+
fields {
10+
name
11+
description
12+
type {
13+
name
14+
kind
15+
}
16+
args {
17+
name
18+
type {
19+
name
20+
kind
21+
}
22+
}
23+
}
24+
}
25+
}
26+
"""
27+
Then the response status code should be 200
28+
And the response should be in JSON
29+
And the header "Content-Type" should be equal to "application/json"
30+
And the JSON node "data.__type.fields[0].name" should contain "delete"
31+
And the JSON node "data.__type.fields[0].description" should contain "Deletes "
32+
And the JSON node "data.__type.fields[0].type.name" should contain "DeleteMutation"
33+
And the JSON node "data.__type.fields[0].type.kind" should be equal to "OBJECT"
34+
And the JSON node "data.__type.fields[0].args[0].name" should be equal to "input"
35+
And the JSON node "data.__type.fields[0].args[0].type.name" should contain "InputDeleteMutation"
36+
And the JSON node "data.__type.fields[0].args[0].type.kind" should be equal to "INPUT_OBJECT"
37+
And print last JSON response
38+
39+
Scenario: Delete an item through a mutation
40+
Given there is 1 dummy objects
41+
When I send the following GraphQL request:
42+
"""
43+
mutation {
44+
deleteDummy(input: {id: 1}) {
45+
id
46+
}
47+
}
48+
"""
49+
Then print last response
50+
Then the response status code should be 200
51+
And the response should be in JSON
52+
And the header "Content-Type" should be equal to "application/json"
53+
And the JSON node "data.deleteDummy.id" should be equal to 1
54+
55+
@dropSchema
56+
Scenario: Delete an item with composite identifiers through a mutation
57+
Given there are Composite identifier objects
58+
When I send the following GraphQL request:
59+
"""
60+
mutation {
61+
deleteCompositeRelation(input: {compositeItem: {id: 1}, compositeLabel: {id: 1}}) {
62+
compositeItem {
63+
id
64+
},
65+
compositeLabel {
66+
id
67+
}
68+
}
69+
}
70+
"""
71+
Then the response status code should be 200
72+
And the response should be in JSON
73+
And the header "Content-Type" should be equal to "application/json"
74+
And the JSON node "data.deleteCompositeRelation.compositeItem.id" should be equal to 1
75+
And the JSON node "data.deleteCompositeRelation.compositeLabel.id" should be equal to 1
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Bridge\Graphql\Resolver;
15+
16+
use ApiPlatform\Core\Api\IdentifiersExtractorInterface;
17+
use ApiPlatform\Core\DataPersister\DataPersisterInterface;
18+
use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
19+
use GraphQL\Error\Error;
20+
use GraphQL\Type\Definition\ResolveInfo;
21+
22+
/**
23+
* Creates a function resolving a GraphQL mutation of an item.
24+
*
25+
* @author Alan Poulain <[email protected]>
26+
*
27+
* @internal
28+
*/
29+
final class ItemMutationResolverFactory implements ItemMutationResolverFactoryInterface
30+
{
31+
private $identifiersExtractor;
32+
private $itemDataProvider;
33+
private $dataPersister;
34+
35+
public function __construct(IdentifiersExtractorInterface $identifiersExtractor, ItemDataProviderInterface $itemDataProvider, DataPersisterInterface $dataPersister)
36+
{
37+
$this->identifiersExtractor = $identifiersExtractor;
38+
$this->itemDataProvider = $itemDataProvider;
39+
$this->dataPersister = $dataPersister;
40+
}
41+
42+
public function createItemMutationResolver(string $resourceClass, string $mutationName): callable
43+
{
44+
return function ($root, $args, $context, ResolveInfo $info) use ($resourceClass, $mutationName) {
45+
$identifiers = $this->identifiersExtractor->getIdentifiersFromResourceClass($resourceClass);
46+
if (\count($identifiers) > 1) {
47+
$identifierPairs = \array_map(function ($identifier) use ($args, $info) {
48+
if (\is_array($args['input'][$identifier])) {
49+
if (\count($args['input'][$identifier]) > 1) {
50+
throw Error::createLocatedError('Composite identifiers are not allowed for a resource already used as a composite identifier', $info->fieldNodes, $info->path);
51+
}
52+
53+
return $identifier.'='.\reset($args['input'][$identifier]);
54+
}
55+
56+
return "{$identifier}={$args['input'][$identifier]}";
57+
}, $identifiers);
58+
$id = \implode(';', $identifierPairs);
59+
} else {
60+
$id = $args['input'][$identifiers[0]];
61+
}
62+
$item = $this->itemDataProvider->getItem($resourceClass, $id);
63+
64+
if (null === $item) {
65+
throw Error::createLocatedError("Item $resourceClass $id not found", $info->fieldNodes, $info->path);
66+
}
67+
68+
if ('delete' === $mutationName) {
69+
$this->dataPersister->remove($item);
70+
}
71+
72+
return $args['input'];
73+
};
74+
}
75+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Bridge\Graphql\Resolver;
15+
16+
/**
17+
* Creates a function resolving a GraphQL mutation of an item.
18+
*
19+
* @author Alan Poulain <[email protected]>
20+
*
21+
* @internal
22+
*/
23+
interface ItemMutationResolverFactoryInterface
24+
{
25+
public function createItemMutationResolver(string $resourceClass, string $mutationName): callable;
26+
}

src/Bridge/Graphql/Type/SchemaBuilder.php

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use ApiPlatform\Core\Api\IdentifiersExtractorInterface;
1717
use ApiPlatform\Core\Bridge\Graphql\Resolver\CollectionResolverFactoryInterface;
18+
use ApiPlatform\Core\Bridge\Graphql\Resolver\ItemMutationResolverFactoryInterface;
1819
use ApiPlatform\Core\Bridge\Graphql\Resolver\ItemResolverFactoryInterface;
1920
use ApiPlatform\Core\Exception\ResourceClassNotFoundException;
2021
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
@@ -47,39 +48,53 @@ final class SchemaBuilder implements SchemaBuilderInterface
4748
private $resourceMetadataFactory;
4849
private $collectionResolverFactory;
4950
private $itemResolverFactory;
51+
private $itemMutationResolverFactory;
5052
private $identifiersExtractor;
5153
private $paginationEnabled;
5254
private $resourceTypesCache = [];
5355

54-
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, CollectionResolverFactoryInterface $collectionResolverFactory, ItemResolverFactoryInterface $itemResolverFactory, IdentifiersExtractorInterface $identifiersExtractor, bool $paginationEnabled)
56+
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, CollectionResolverFactoryInterface $collectionResolverFactory, ItemResolverFactoryInterface $itemResolverFactory, ItemMutationResolverFactoryInterface $itemMutationResolverFactory, IdentifiersExtractorInterface $identifiersExtractor, bool $paginationEnabled)
5557
{
5658
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
5759
$this->propertyMetadataFactory = $propertyMetadataFactory;
5860
$this->resourceNameCollectionFactory = $resourceNameCollectionFactory;
5961
$this->resourceMetadataFactory = $resourceMetadataFactory;
6062
$this->collectionResolverFactory = $collectionResolverFactory;
6163
$this->itemResolverFactory = $itemResolverFactory;
64+
$this->itemMutationResolverFactory = $itemMutationResolverFactory;
6265
$this->identifiersExtractor = $identifiersExtractor;
6366
$this->paginationEnabled = $paginationEnabled;
6467
}
6568

6669
public function getSchema(): Schema
6770
{
6871
$queryFields = [];
72+
$mutationFields = [];
73+
6974
foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) {
7075
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
71-
if (!isset($resourceMetadata->getGraphql()['query'])) {
72-
continue;
73-
}
76+
$graphqlConfiguration = $resourceMetadata->getGraphql() ?? [];
7477

75-
$queryFields += $this->getQueryFields($resourceClass, $resourceMetadata);
78+
foreach ($graphqlConfiguration as $operationName => $value) {
79+
if ('query' === $operationName) {
80+
$queryFields += $this->getQueryFields($resourceClass, $resourceMetadata);
81+
82+
continue;
83+
}
84+
85+
$mutationFields[$operationName.$resourceMetadata->getShortName()] = $this->getMutationFields($resourceClass, $resourceMetadata, $operationName);
86+
}
7687
}
7788

7889
return new Schema([
7990
'query' => new ObjectType([
8091
'name' => 'Query',
8192
'fields' => $queryFields,
8293
]),
94+
'mutation' => new ObjectType([
95+
'name' => 'Mutation',
96+
'fields' => $mutationFields,
97+
]),
8398
]);
8499
}
85100

@@ -104,16 +119,32 @@ private function getQueryFields(string $resourceClass, ResourceMetadata $resourc
104119
}
105120

106121
/**
107-
* Gets the field configuration of a resource.
122+
* Gets the mutation field for the given operation name.
123+
*/
124+
private function getMutationFields(string $resourceClass, ResourceMetadata $resourceMetadata, string $mutationName): array
125+
{
126+
$shortName = $resourceMetadata->getShortName();
127+
$resourceType = new Type(Type::BUILTIN_TYPE_OBJECT, true, $resourceClass);
128+
129+
if ($fieldConfiguration = $this->getResourceFieldConfiguration(ucfirst("{$mutationName}s a $shortName."), $resourceType, $resourceClass, false, true, $mutationName)) {
130+
$fieldConfiguration['args'] += ['input' => $this->getResourceFieldConfiguration(null, $resourceType, $resourceClass, true, true, $mutationName)];
131+
$fieldConfiguration['resolve'] = $resourceType->isCollection() ? null : $this->itemMutationResolverFactory->createItemMutationResolver($resourceClass, $mutationName);
132+
}
133+
134+
return $fieldConfiguration;
135+
}
136+
137+
/**
138+
* Get the field configuration of a resource.
108139
*
109140
* @see http://webonyx.github.io/graphql-php/type-system/object-types/
110141
*
111142
* @return array|null
112143
*/
113-
private function getResourceFieldConfiguration(string $fieldDescription = null, Type $type, string $rootResource, bool $isInput = false)
144+
private function getResourceFieldConfiguration(string $fieldDescription = null, Type $type, string $rootResource, bool $isInput = false, bool $isMutation = false, string $mutationName = null)
114145
{
115146
try {
116-
$graphqlType = $this->convertType($type, $isInput);
147+
$graphqlType = $this->convertType($type, $isInput, $isMutation, $mutationName);
117148
$graphqlWrappedType = $graphqlType instanceof WrappingType ? $graphqlType->getWrappedType() : $graphqlType;
118149
$isInternalGraphqlType = in_array($graphqlWrappedType, GraphQLType::getInternalTypes(), true);
119150
if ($isInternalGraphqlType) {
@@ -123,7 +154,7 @@ private function getResourceFieldConfiguration(string $fieldDescription = null,
123154
}
124155

125156
$args = [];
126-
if ($this->paginationEnabled && !$isInternalGraphqlType && $type->isCollection() && !$isInput) {
157+
if ($this->paginationEnabled && !$isInternalGraphqlType && $type->isCollection() && !$isInput && !$isMutation) {
127158
$args = [
128159
'first' => [
129160
'type' => GraphQLType::int(),
@@ -136,7 +167,7 @@ private function getResourceFieldConfiguration(string $fieldDescription = null,
136167
];
137168
}
138169

139-
if ($isInternalGraphqlType || $isInput) {
170+
if ($isInternalGraphqlType || $isInput || $isMutation) {
140171
$resolve = null;
141172
} else {
142173
$resolve = $type->isCollection() ? $this->collectionResolverFactory->createCollectionResolver($className, $rootResource) : $this->itemResolverFactory->createItemResolver($className, $rootResource);
@@ -183,7 +214,7 @@ private function getResourceIdentifiersArgumentsConfiguration(string $resourceCl
183214
*
184215
* @throws InvalidTypeException
185216
*/
186-
private function convertType(Type $type, bool $isInput = false): GraphQLType
217+
private function convertType(Type $type, bool $isInput = false, bool $isMutation = false, string $mutationName = null): GraphQLType
187218
{
188219
switch ($type->getBuiltinType()) {
189220
case Type::BUILTIN_TYPE_BOOL:
@@ -211,7 +242,7 @@ private function convertType(Type $type, bool $isInput = false): GraphQLType
211242
throw new InvalidTypeException();
212243
}
213244

214-
$graphqlType = $this->getResourceObjectType($className, $resourceMetadata, $isInput);
245+
$graphqlType = $this->getResourceObjectType($className, $resourceMetadata, $isInput, $isMutation, $mutationName);
215246
break;
216247
default:
217248
throw new InvalidTypeException();
@@ -229,18 +260,19 @@ private function convertType(Type $type, bool $isInput = false): GraphQLType
229260
*
230261
* @return ObjectType|InputObjectType
231262
*/
232-
private function getResourceObjectType(string $resource, ResourceMetadata $resourceMetadata, bool $isInput = false)
263+
private function getResourceObjectType(string $resource, ResourceMetadata $resourceMetadata, bool $isInput = false, bool $isMutation = false, string $mutationName = null)
233264
{
234-
$shortName = $resourceMetadata->getShortName().($isInput ? 'Input' : '');
265+
$shortName = $resourceMetadata->getShortName().($isInput ? 'Input' : '').($isMutation ? ucfirst($mutationName).'Mutation' : '');
266+
235267
if (isset($this->resourceTypesCache[$shortName])) {
236268
return $this->resourceTypesCache[$shortName];
237269
}
238270

239271
$configuration = [
240272
'name' => $shortName,
241273
'description' => $resourceMetadata->getDescription(),
242-
'fields' => function () use ($resource, $isInput) {
243-
return $this->getResourceObjectTypeFields($resource, $isInput);
274+
'fields' => function () use ($resource, $isInput, $isMutation, $mutationName) {
275+
return $this->getResourceObjectTypeFields($resource, $isInput, $isMutation, $mutationName);
244276
},
245277
];
246278

@@ -250,16 +282,18 @@ private function getResourceObjectType(string $resource, ResourceMetadata $resou
250282
/**
251283
* Gets the fields of the type of the given resource.
252284
*/
253-
private function getResourceObjectTypeFields(string $resource, bool $isInput = false): array
285+
private function getResourceObjectTypeFields(string $resource, bool $isInput = false, bool $isMutation = false, string $mutationName = null): array
254286
{
255287
$fields = [];
256288
foreach ($this->propertyNameCollectionFactory->create($resource) as $property) {
257289
$propertyMetadata = $this->propertyMetadataFactory->create($resource, $property);
258-
if (null === ($propertyType = $propertyMetadata->getType()) || !$propertyMetadata->isReadable()) {
290+
if (null === ($propertyType = $propertyMetadata->getType())
291+
|| !$propertyMetadata->isReadable()
292+
|| ($isMutation && 'delete' === $mutationName && !$propertyMetadata->isIdentifier())) {
259293
continue;
260294
}
261295

262-
if ($fieldConfiguration = $this->getResourceFieldConfiguration($propertyMetadata->getDescription(), $propertyType, $resource, $isInput)) {
296+
if ($fieldConfiguration = $this->getResourceFieldConfiguration($propertyMetadata->getDescription(), $propertyType, $resource, $isInput, $isMutation, $mutationName)) {
263297
$fields[$property] = $fieldConfiguration;
264298
}
265299
}

src/Bridge/Symfony/Bundle/Resources/config/graphql.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,20 @@
2525
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
2626
</service>
2727

28+
<service id="api_platform.graphql.item_mutation_resolver_factory" class="ApiPlatform\Core\Bridge\Graphql\Resolver\ItemMutationResolverFactory" public="false">
29+
<argument type="service" id="api_platform.identifiers_extractor.cached" />
30+
<argument type="service" id="api_platform.item_data_provider" />
31+
<argument type="service" id="api_platform.data_persister" />
32+
</service>
33+
2834
<service id="api_platform.graphql.schema_builder" class="ApiPlatform\Core\Bridge\Graphql\Type\SchemaBuilder" public="false">
2935
<argument type="service" id="api_platform.metadata.property.name_collection_factory" />
3036
<argument type="service" id="api_platform.metadata.property.metadata_factory" />
3137
<argument type="service" id="api_platform.metadata.resource.name_collection_factory" />
3238
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
3339
<argument type="service" id="api_platform.graphql.collection_resolver_factory" />
3440
<argument type="service" id="api_platform.graphql.item_resolver_factory" />
41+
<argument type="service" id="api_platform.graphql.item_mutation_resolver_factory" />
3542
<argument type="service" id="api_platform.identifiers_extractor.cached" />
3643
<argument>%api_platform.collection.pagination.enabled%</argument>
3744
</service>

src/Metadata/Resource/Factory/OperationResourceMetadataFactory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ public function create(string $resourceClass): ResourceMetadata
8484

8585
$graphql = $resourceMetadata->getGraphql();
8686
if (null === $graphql) {
87-
$resourceMetadata = $resourceMetadata->withGraphql(['query' => []]);
87+
$resourceMetadata = $resourceMetadata->withGraphql(['query' => [], 'delete' => []]);
8888
} else {
8989
$resourceMetadata = $this->normalizeGraphQl($resourceMetadata, $graphql);
9090
}

0 commit comments

Comments
 (0)