Skip to content

Commit 5167847

Browse files
committed
Delete mutation for an item
1 parent abf7e9c commit 5167847

File tree

7 files changed

+218
-4
lines changed

7 files changed

+218
-4
lines changed

features/graphql/mutation.feature

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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 be equal to "deleteRelatedDummy"
31+
And the JSON node "data.__type.fields[0].description" should be equal to "Deletes one RelatedDummy."
32+
And the JSON node "data.__type.fields[0].type.name" should be equal to "RelatedDummy"
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 be equal to "RelatedDummyInput"
36+
And the JSON node "data.__type.fields[0].args[0].type.kind" should be equal to "INPUT_OBJECT"
37+
And the JSON node "data.__type.fields[1].name" should be equal to "putRelatedDummy"
38+
And the JSON node "data.__type.fields[1].description" should be equal to "Puts one RelatedDummy."
39+
And the JSON node "data.__type.fields[1].type.name" should be equal to "RelatedDummy"
40+
And the JSON node "data.__type.fields[1].type.kind" should be equal to "OBJECT"
41+
42+
@dropSchema
43+
Scenario: Delete an item through a mutation
44+
Given there is 1 dummy objects
45+
When I send the following GraphQL request:
46+
"""
47+
mutation {
48+
deleteDummy(input: {id: 1, name: "Dummy #1"}) {
49+
id
50+
}
51+
}
52+
"""
53+
Then the response status code should be 200
54+
And the response should be in JSON
55+
And the header "Content-Type" should be equal to "application/json"
56+
And the JSON node "data.deleteDummy.id" should be equal to 1
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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\Api\UrlGeneratorInterface;
18+
use ApiPlatform\Core\Bridge\Symfony\Routing\RouteNameGenerator;
19+
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
20+
use GraphQL\Error\Error;
21+
use GraphQL\Type\Definition\ResolveInfo;
22+
use Symfony\Component\HttpFoundation\Request;
23+
use Symfony\Component\HttpFoundation\Response;
24+
use Symfony\Component\HttpKernel\HttpKernelInterface;
25+
26+
/**
27+
* Creates a function resolving a GraphQL mutation of an item.
28+
*
29+
* @author Alan Poulain <[email protected]>
30+
*
31+
* @internal
32+
*/
33+
final class ItemMutationResolverFactory implements ItemMutationResolverFactoryInterface
34+
{
35+
private $httpKernel;
36+
private $urlGenerator;
37+
private $resourceMetadataFactory;
38+
private $identifiersExtractor;
39+
40+
public function __construct(HttpKernelInterface $httpKernel, UrlGeneratorInterface $urlGenerator, ResourceMetadataFactoryInterface $resourceMetadataFactory, IdentifiersExtractorInterface $identifiersExtractor)
41+
{
42+
$this->httpKernel = $httpKernel;
43+
$this->urlGenerator = $urlGenerator;
44+
$this->resourceMetadataFactory = $resourceMetadataFactory;
45+
$this->identifiersExtractor = $identifiersExtractor;
46+
}
47+
48+
public function createItemMutationResolver(string $resourceClass, string $operationName, array $operation): callable
49+
{
50+
return function ($root, $args, $context, ResolveInfo $info) use ($resourceClass, $operationName, $operation) {
51+
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
52+
$resourceShortName = $resourceMetadata->getShortName();
53+
54+
$route = RouteNameGenerator::generate($operationName, $resourceShortName, 'item');
55+
$identifiers = $this->identifiersExtractor->getIdentifiersFromResourceClass($resourceClass);
56+
if (count($identifiers) > 1) {
57+
$identifierPairs = array_map(function ($identifier) use ($args) {
58+
return "{$identifier}={$args['input'][$identifier]}";
59+
}, $identifiers);
60+
$id = implode(';', $identifierPairs);
61+
} else {
62+
$id = $args['input'][$identifiers[0]];
63+
}
64+
$uri = $this->urlGenerator->generate($route, ['id' => $id]);
65+
$request = Request::create($uri, $operation['method']);
66+
$request->headers->set('Accept', 'application/json');
67+
68+
$response = $this->httpKernel->handle($request, HttpKernelInterface::SUB_REQUEST);
69+
if (!in_array($response->getStatusCode(), [Response::HTTP_OK, Response::HTTP_NO_CONTENT], true)) {
70+
throw Error::createLocatedError('Mutation was not resolved correctly.', $info->fieldNodes, $info->path);
71+
}
72+
73+
return ['id' => $id];
74+
};
75+
}
76+
}
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, array $operation): callable;
26+
}

src/Bridge/Graphql/Type/SchemaBuilder.php

Lines changed: 46 additions & 4 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,14 +116,35 @@ 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)) {
131+
$fieldConfiguration['args'] += $this->getResourceMutationArgumentsConfiguration($resourceType, $resource);
132+
$fieldConfiguration['resolve'] = $resourceType->isCollection() ? null : $this->itemMutationResolverFactory->createItemMutationResolver($resource, $operationName, $mutationItemOperation);
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, bool $isInput = false)
147+
private function getResourceFieldConfiguration(string $fieldDescription = null, Type $type, string $rootResource, bool $isInput = false, bool $isMutation = false)
118148
{
119149
try {
120150
$graphqlType = $this->convertType($type, $isInput);
@@ -123,7 +153,7 @@ private function getResourceFieldConfiguration(string $fieldDescription = null,
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) : $this->itemResolverFactory->createItemResolver($className, $rootResource);
@@ -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): array
190+
{
191+
return ['input' => $this->getResourceFieldConfiguration(null, $type, $resource, true)];
192+
}
193+
156194
/**
157195
* Gets the field arguments of the identifier of a given resource.
158196
*
@@ -335,6 +373,10 @@ private function getOperations(ResourceMetadata $resourceMetadata, bool $isQuery
335373
continue;
336374
}
337375

376+
if (Request::METHOD_PATCH === $operation['method']) {
377+
continue;
378+
}
379+
338380
yield $operationName => $operation;
339381
}
340382
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,21 @@
2323
<argument type="service" id="api_platform.identifiers_extractor.cached" />
2424
</service>
2525

26+
<service id="api_platform.graphql.item_mutation_resolver_factory" class="ApiPlatform\Core\Bridge\Graphql\Resolver\ItemMutationResolverFactory" public="false">
27+
<argument type="service" id="http_kernel" />
28+
<argument type="service" id="api_platform.router" />
29+
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
30+
<argument type="service" id="api_platform.identifiers_extractor.cached" />
31+
</service>
32+
2633
<service id="api_platform.graphql.schema_builder" class="ApiPlatform\Core\Bridge\Graphql\Type\SchemaBuilder" public="false">
2734
<argument type="service" id="api_platform.metadata.property.name_collection_factory" />
2835
<argument type="service" id="api_platform.metadata.property.metadata_factory" />
2936
<argument type="service" id="api_platform.metadata.resource.name_collection_factory" />
3037
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
3138
<argument type="service" id="api_platform.graphql.collection_resolver_factory" />
3239
<argument type="service" id="api_platform.graphql.item_resolver_factory" />
40+
<argument type="service" id="api_platform.graphql.item_mutation_resolver_factory" />
3341
<argument type="service" id="api_platform.identifiers_extractor.cached" />
3442
<argument>%api_platform.collection.pagination.enabled%</argument>
3543
</service>

tests/Bridge/Graphql/Type/SchemaBuilderTest.php

Lines changed: 4 additions & 0 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\Bridge\Graphql\Type\SchemaBuilder;
2021
use ApiPlatform\Core\Exception\ResourceClassNotFoundException;
@@ -183,6 +184,7 @@ private function mockSchemaBuilder($propertyMetadataMockBuilder, bool $paginatio
183184
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
184185
$collectionResolverFactoryProphecy = $this->prophesize(CollectionResolverFactoryInterface::class);
185186
$itemResolverFactoryProphecy = $this->prophesize(ItemResolverFactoryInterface::class);
187+
$itemMutationResolverFactoryProphecy = $this->prophesize(ItemMutationResolverFactoryInterface::class);
186188
$identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
187189

188190
$resourceClassNames = [];
@@ -227,6 +229,7 @@ private function mockSchemaBuilder($propertyMetadataMockBuilder, bool $paginatio
227229

228230
$collectionResolverFactoryProphecy->createCollectionResolver(Argument::cetera())->willReturn(function () {});
229231
$itemResolverFactoryProphecy->createItemResolver(Argument::cetera())->willReturn(function () {});
232+
$itemMutationResolverFactoryProphecy->createItemMutationResolver(Argument::cetera())->willReturn(function () {});
230233

231234
return new SchemaBuilder(
232235
$propertyNameCollectionFactoryProphecy->reveal(),
@@ -235,6 +238,7 @@ private function mockSchemaBuilder($propertyMetadataMockBuilder, bool $paginatio
235238
$resourceMetadataFactoryProphecy->reveal(),
236239
$collectionResolverFactoryProphecy->reveal(),
237240
$itemResolverFactoryProphecy->reveal(),
241+
$itemMutationResolverFactoryProphecy->reveal(),
238242
$identifiersExtractorProphecy->reveal(),
239243
$paginationEnabled
240244
);

tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ public function testDisableGraphql()
231231
$containerBuilderProphecy->setDefinition('api_platform.graphql.collection_resolver_factory')->shouldNotBeCalled();
232232
$containerBuilderProphecy->setDefinition('api_platform.graphql.executor')->shouldNotBeCalled();
233233
$containerBuilderProphecy->setDefinition('api_platform.graphql.item_resolver_factory')->shouldNotBeCalled();
234+
$containerBuilderProphecy->setDefinition('api_platform.graphql.item_mutation_resolver_factory')->shouldNotBeCalled();
234235
$containerBuilderProphecy->setDefinition('api_platform.graphql.schema_builder')->shouldNotBeCalled();
235236
$containerBuilderProphecy->setParameter('api_platform.graphql.enabled', true)->shouldNotBeCalled();
236237
$containerBuilderProphecy->setParameter('api_platform.graphql.enabled', false)->shouldBeCalled();
@@ -545,6 +546,7 @@ private function getBaseContainerBuilderProphecy()
545546
'api_platform.graphql.collection_resolver_factory',
546547
'api_platform.graphql.executor',
547548
'api_platform.graphql.item_resolver_factory',
549+
'api_platform.graphql.item_mutation_resolver_factory',
548550
'api_platform.graphql.schema_builder',
549551
'api_platform.jsonld.normalizer.item',
550552
'api_platform.jsonld.encoder',

0 commit comments

Comments
 (0)