Skip to content

Commit be9b7fa

Browse files
committed
Delete mutation for an item
1 parent 091865f commit be9b7fa

File tree

8 files changed

+360
-14
lines changed

8 files changed

+360
-14
lines changed

features/graphql/mutation.feature

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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 one"
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+
38+
Scenario: Delete an item through a mutation
39+
Given there is 1 dummy objects
40+
When I send the following GraphQL request:
41+
"""
42+
mutation {
43+
deleteDummy(input: {id: 1}) {
44+
id
45+
}
46+
}
47+
"""
48+
Then the response status code should be 200
49+
And the response should be in JSON
50+
And the header "Content-Type" should be equal to "application/json"
51+
And the JSON node "data.deleteDummy.id" should be equal to 1
52+
53+
@dropSchema
54+
Scenario: Delete an item with composite identifiers through a mutation
55+
Given there are Composite identifier objects
56+
When I send the following GraphQL request:
57+
"""
58+
mutation {
59+
deleteCompositeRelation(input: {compositeItem: {id: 1}, compositeLabel: {id: 1}}) {
60+
compositeItem {
61+
id
62+
},
63+
compositeLabel {
64+
id
65+
}
66+
}
67+
}
68+
"""
69+
Then the response status code should be 200
70+
And the response should be in JSON
71+
And the header "Content-Type" should be equal to "application/json"
72+
And the JSON node "data.deleteCompositeRelation.compositeItem.id" should be equal to 1
73+
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 $operationName): callable
43+
{
44+
return function ($root, $args, $context, ResolveInfo $info) use ($resourceClass, $operationName) {
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' === $operationName) {
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 $operationName): callable;
26+
}

src/Bridge/Graphql/Type/SchemaBuilder.php

Lines changed: 57 additions & 14 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;
@@ -48,35 +49,43 @@ final class SchemaBuilder implements SchemaBuilderInterface
4849
private $resourceMetadataFactory;
4950
private $collectionResolverFactory;
5051
private $itemResolverFactory;
52+
private $itemMutationResolverFactory;
5153
private $identifiersExtractor;
5254
private $paginationEnabled;
5355
private $resourceTypesCache = [];
5456

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

6770
public function getSchema(): Schema
6871
{
6972
$queryFields = [];
73+
$mutationFields = [];
7074

7175
foreach ($this->resourceNameCollectionFactory->create() as $resource) {
7276
$queryFields += $this->getQueryFields($resource);
77+
$mutationFields += $this->getMutationFields($resource);
7378
}
7479

7580
return new Schema([
7681
'query' => new ObjectType([
7782
'name' => 'Query',
7883
'fields' => $queryFields,
7984
]),
85+
'mutation' => new ObjectType([
86+
'name' => 'Mutation',
87+
'fields' => $mutationFields,
88+
]),
8089
]);
8190
}
8291

@@ -107,23 +116,44 @@ private function getQueryFields(string $resource): array
107116
return $queryFields;
108117
}
109118

119+
/**
120+
* Gets the mutation fields of the schema.
121+
*/
122+
private function getMutationFields(string $resource): array
123+
{
124+
$mutationFields = [];
125+
$resourceMetadata = $this->resourceMetadataFactory->create($resource);
126+
$shortName = $resourceMetadata->getShortName();
127+
$resourceType = new Type(Type::BUILTIN_TYPE_OBJECT, true, $resource);
128+
129+
foreach ($this->getOperations($resourceMetadata, false, true) as $operationName => $mutationItemOperation) {
130+
if ($fieldConfiguration = $this->getResourceFieldConfiguration(ucfirst("{$operationName}s one $shortName."), $resourceType, $resource, $operationName, false, true)) {
131+
$fieldConfiguration['args'] += $this->getResourceMutationArgumentsConfiguration($resourceType, $resource, $operationName);
132+
$fieldConfiguration['resolve'] = $resourceType->isCollection() ? null : $this->itemMutationResolverFactory->createItemMutationResolver($resource, $operationName);
133+
$mutationFields[$operationName.$shortName] = $fieldConfiguration;
134+
}
135+
}
136+
137+
return $mutationFields;
138+
}
139+
110140
/**
111141
* Get the field configuration of a resource.
112142
*
113143
* @see http://webonyx.github.io/graphql-php/type-system/object-types/
114144
*
115145
* @return array|null
116146
*/
117-
private function getResourceFieldConfiguration(string $fieldDescription = null, Type $type, string $rootResource, string $operationName, bool $isInput = false)
147+
private function getResourceFieldConfiguration(string $fieldDescription = null, Type $type, string $rootResource, string $operationName, bool $isInput = false, bool $isMutation = false)
118148
{
119149
try {
120-
$graphqlType = $this->convertType($type, $operationName, $isInput);
150+
$graphqlType = $this->convertType($type, $operationName, $isInput, $isMutation);
121151
$graphqlWrappedType = $graphqlType instanceof WrappingType ? $graphqlType->getWrappedType() : $graphqlType;
122152
$isInternalGraphqlType = in_array($graphqlWrappedType, GraphQLType::getInternalTypes(), true);
123153
$className = $isInternalGraphqlType ? '' : ($type->isCollection() ? $type->getCollectionValueType()->getClassName() : $type->getClassName());
124154

125155
$args = [];
126-
if ($this->paginationEnabled && !$isInternalGraphqlType && $type->isCollection() && !$isInput) {
156+
if ($this->paginationEnabled && !$isInternalGraphqlType && $type->isCollection() && !$isInput && !$isMutation) {
127157
$args = [
128158
'first' => [
129159
'type' => GraphQLType::int(),
@@ -136,7 +166,7 @@ private function getResourceFieldConfiguration(string $fieldDescription = null,
136166
];
137167
}
138168

139-
if ($isInternalGraphqlType || $isInput) {
169+
if ($isInternalGraphqlType || $isInput || $isMutation) {
140170
$resolve = null;
141171
} else {
142172
$resolve = $type->isCollection() ? $this->collectionResolverFactory->createCollectionResolver($className, $rootResource, $operationName) : $this->itemResolverFactory->createItemResolver($className, $rootResource, $operationName);
@@ -153,6 +183,14 @@ private function getResourceFieldConfiguration(string $fieldDescription = null,
153183
}
154184
}
155185

186+
/**
187+
* Gets the field arguments of the mutation of a given resource.
188+
*/
189+
private function getResourceMutationArgumentsConfiguration(Type $type, string $resource, string $operationName): array
190+
{
191+
return ['input' => $this->getResourceFieldConfiguration(null, $type, $resource, $operationName, true, true)];
192+
}
193+
156194
/**
157195
* Gets the field arguments of the identifier of a given resource.
158196
*
@@ -183,7 +221,7 @@ private function getResourceIdentifiersArgumentsConfiguration(string $resource,
183221
*
184222
* @throws InvalidTypeException
185223
*/
186-
private function convertType(Type $type, string $operationName, bool $isInput = false): GraphQLType
224+
private function convertType(Type $type, string $operationName, bool $isInput = false, bool $isMutation = false): GraphQLType
187225
{
188226
switch ($type->getBuiltinType()) {
189227
case Type::BUILTIN_TYPE_BOOL:
@@ -211,7 +249,7 @@ private function convertType(Type $type, string $operationName, bool $isInput =
211249
throw new InvalidTypeException();
212250
}
213251

214-
$graphqlType = $this->getResourceObjectType($className, $resourceMetadata, $operationName, $isInput);
252+
$graphqlType = $this->getResourceObjectType($className, $resourceMetadata, $operationName, $isInput, $isMutation);
215253
break;
216254
default:
217255
throw new InvalidTypeException();
@@ -229,9 +267,9 @@ private function convertType(Type $type, string $operationName, bool $isInput =
229267
*
230268
* @return ObjectType|InputObjectType
231269
*/
232-
private function getResourceObjectType(string $resource, ResourceMetadata $resourceMetadata, string $operationName, bool $isInput = false)
270+
private function getResourceObjectType(string $resource, ResourceMetadata $resourceMetadata, string $operationName, bool $isInput = false, bool $isMutation = false)
233271
{
234-
$shortName = $resourceMetadata->getShortName().($isInput ? 'Input' : '');
272+
$shortName = $resourceMetadata->getShortName().($isInput ? 'Input' : '').($isMutation ? ucfirst($operationName)."Mutation" : '');
235273

236274
if (isset($this->resourceTypesCache[$shortName])) {
237275
return $this->resourceTypesCache[$shortName];
@@ -240,8 +278,8 @@ private function getResourceObjectType(string $resource, ResourceMetadata $resou
240278
$configuration = [
241279
'name' => $shortName,
242280
'description' => $resourceMetadata->getDescription(),
243-
'fields' => function () use ($resource, $operationName, $isInput) {
244-
return $this->getResourceObjectTypeFields($resource, $operationName, $isInput);
281+
'fields' => function () use ($resource, $operationName, $isInput, $isMutation) {
282+
return $this->getResourceObjectTypeFields($resource, $operationName, $isInput, $isMutation);
245283
},
246284
];
247285

@@ -251,18 +289,19 @@ private function getResourceObjectType(string $resource, ResourceMetadata $resou
251289
/**
252290
* Gets the fields of the type of the given resource.
253291
*/
254-
private function getResourceObjectTypeFields(string $resource, string $operationName, bool $isInput = false): array
292+
private function getResourceObjectTypeFields(string $resource, string $operationName, bool $isInput = false, bool $isMutation = false): array
255293
{
256294
$fields = [];
257295

258296
foreach ($this->propertyNameCollectionFactory->create($resource) as $property) {
259297
$propertyMetadata = $this->propertyMetadataFactory->create($resource, $property);
260298
if (null === ($propertyType = $propertyMetadata->getType())
261-
|| !$propertyMetadata->isReadable()) {
299+
|| !$propertyMetadata->isReadable()
300+
|| ($isMutation && 'delete' === $operationName && !$propertyMetadata->isIdentifier())) {
262301
continue;
263302
}
264303

265-
if ($fieldConfiguration = $this->getResourceFieldConfiguration($propertyMetadata->getDescription(), $propertyType, $resource, $operationName, $isInput)) {
304+
if ($fieldConfiguration = $this->getResourceFieldConfiguration($propertyMetadata->getDescription(), $propertyType, $resource, $operationName, $isInput, $isMutation)) {
266305
$fields[$property] = $fieldConfiguration;
267306
}
268307
}
@@ -335,6 +374,10 @@ private function getOperations(ResourceMetadata $resourceMetadata, bool $isQuery
335374
continue;
336375
}
337376

377+
if (Request::METHOD_PATCH === $operation['method']) {
378+
continue;
379+
}
380+
338381
yield $operationName => $operation;
339382
}
340383
}

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>

0 commit comments

Comments
 (0)