Skip to content

Commit 5e8f5eb

Browse files
jotweajosef.wagner
and
josef.wagner
authored
fix(graphql): consider writable flag also for nested input types (#5954)
Co-authored-by: josef.wagner <[email protected]>
1 parent 58f4a3d commit 5e8f5eb

File tree

10 files changed

+280
-5
lines changed

10 files changed

+280
-5
lines changed

features/graphql/mutation.feature

+73
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,79 @@ Feature: GraphQL mutation support
637637
And the JSON node "data.updateDummy.dummy.relatedDummies.edges[0].node.name" should be equal to "RelatedDummy11"
638638
And the JSON node "data.updateDummy.clientMutationId" should be equal to "myId"
639639

640+
@createSchema
641+
@!mongodb
642+
Scenario: Modify an item with embedded object through a mutation
643+
Given there is a fooDummy objects with fake names and embeddable
644+
When I send the following GraphQL request:
645+
"""
646+
mutation {
647+
updateFooDummy(input: {id: "/foo_dummies/1", name: "modifiedName", embeddedFoo: {dummyName: "Embedded name"}, clientMutationId: "myId"}) {
648+
fooDummy {
649+
id
650+
name
651+
embeddedFoo {
652+
dummyName
653+
}
654+
}
655+
clientMutationId
656+
}
657+
}
658+
"""
659+
Then the response status code should be 200
660+
And the response should be in JSON
661+
And the header "Content-Type" should be equal to "application/json"
662+
And the JSON node "data.updateFooDummy.fooDummy.name" should be equal to "modifiedName"
663+
And the JSON node "data.updateFooDummy.fooDummy.embeddedFoo.dummyName" should be equal to "Embedded name"
664+
And the JSON node "data.updateFooDummy.clientMutationId" should be equal to "myId"
665+
666+
@createSchema
667+
Scenario: Try to modify a non writable property through a mutation
668+
Given there is a fooDummy objects with fake names and embeddable
669+
When I send the following GraphQL request:
670+
"""
671+
mutation {
672+
updateFooDummy(input: {id: "/foo_dummies/1", name: "modifiedName", nonWritableProp: "written", embeddedFoo: {dummyName: "Embedded name"}, clientMutationId: "myId"}) {
673+
fooDummy {
674+
id
675+
name
676+
embeddedFoo {
677+
dummyName
678+
}
679+
}
680+
clientMutationId
681+
}
682+
}
683+
"""
684+
Then the response status code should be 200
685+
And the response should be in JSON
686+
And the header "Content-Type" should be equal to "application/json"
687+
And the JSON node "errors[0].message" should match '/^Field "nonWritableProp" is not defined by type "?updateFooDummyInput"?\.$/'
688+
689+
@createSchema
690+
@!mongodb
691+
Scenario: Try to modify a non writable embedded property through a mutation
692+
Given there is a fooDummy objects with fake names and embeddable
693+
When I send the following GraphQL request:
694+
"""
695+
mutation {
696+
updateFooDummy(input: {id: "/foo_dummies/1", name: "modifiedName", embeddedFoo: {dummyName: "Embedded name", nonWritableProp: "written"}, clientMutationId: "myId"}) {
697+
fooDummy {
698+
id
699+
name
700+
embeddedFoo {
701+
dummyName
702+
}
703+
}
704+
clientMutationId
705+
}
706+
}
707+
"""
708+
Then the response status code should be 200
709+
And the response should be in JSON
710+
And the header "Content-Type" should be equal to "application/json"
711+
And the JSON node "errors[0].message" should match '/^Field "nonWritableProp" is not defined by type "?FooEmbeddableNestedInput"?\.$/'
712+
640713
@!mongodb
641714
Scenario: Modify an item with composite identifiers through a mutation
642715
Given there are Composite identifier objects

features/main/default_order.feature

+10
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ Feature: Default order
7979
"@type": "FooDummy",
8080
"id": 5,
8181
"name": "Balbo",
82+
"nonWritableProp": "readonly",
83+
"embeddedFoo": null,
8284
"dummy": "/dummies/5",
8385
"soManies": [
8486
"/so_manies/13",
@@ -92,6 +94,8 @@ Feature: Default order
9294
"@type": "FooDummy",
9395
"id": 3,
9496
"name": "Sthenelus",
97+
"nonWritableProp": "readonly",
98+
"embeddedFoo": null,
9599
"dummy": "/dummies/3",
96100
"soManies": [
97101
"/so_manies/7",
@@ -104,6 +108,8 @@ Feature: Default order
104108
"@type": "FooDummy",
105109
"id": 2,
106110
"name": "Ephesian",
111+
"nonWritableProp": "readonly",
112+
"embeddedFoo": null,
107113
"dummy": "/dummies/2",
108114
"soManies": [
109115
"/so_manies/4",
@@ -116,6 +122,8 @@ Feature: Default order
116122
"@type": "FooDummy",
117123
"id": 1,
118124
"name": "Hawsepipe",
125+
"nonWritableProp": "readonly",
126+
"embeddedFoo": null,
119127
"dummy": "/dummies/1",
120128
"soManies": [
121129
"/so_manies/1",
@@ -128,6 +136,8 @@ Feature: Default order
128136
"@type": "FooDummy",
129137
"id": 4,
130138
"name": "Separativeness",
139+
"nonWritableProp": "readonly",
140+
"embeddedFoo": null,
131141
"dummy": "/dummies/4",
132142
"soManies": [
133143
"/so_manies/10",

src/GraphQl/Tests/Type/FieldsBuilderTest.php

+3-1
Original file line numberDiff line numberDiff line change
@@ -582,7 +582,8 @@ public static function resourceObjectTypeFieldsProvider(): iterable
582582
yield 'query input' => ['resourceClass', (new Query())->withClass('resourceClass'),
583583
[
584584
'property' => new ApiProperty(),
585-
'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(false),
585+
'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true),
586+
'nonWritableProperty' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withReadable(false)->withWritable(false),
586587
],
587588
true, 0, null,
588589
[
@@ -671,6 +672,7 @@ public static function resourceObjectTypeFieldsProvider(): iterable
671672
'property' => new ApiProperty(),
672673
'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withDescription('propertyBool description')->withReadable(false)->withWritable(true)->withDeprecationReason('not useful'),
673674
'propertySubresource' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true),
675+
'nonWritableProperty' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withReadable(false)->withWritable(false),
674676
'id' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)])->withReadable(false)->withWritable(true),
675677
],
676678
true, 0, null,

src/GraphQl/Type/FieldsBuilder.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ public function getResourceObjectTypeFields(?string $resourceClass, Operation $o
220220
if (
221221
!$propertyTypes
222222
|| (!$input && false === $propertyMetadata->isReadable())
223-
|| ($input && $operation instanceof Mutation && false === $propertyMetadata->isWritable())
223+
|| ($input && false === $propertyMetadata->isWritable())
224224
) {
225225
continue;
226226
}

tests/Behat/DoctrineContext.php

+21-1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
use ApiPlatform\Tests\Fixtures\TestBundle\Document\FileConfigDummy as FileConfigDummyDocument;
5959
use ApiPlatform\Tests\Fixtures\TestBundle\Document\Foo as FooDocument;
6060
use ApiPlatform\Tests\Fixtures\TestBundle\Document\FooDummy as FooDummyDocument;
61+
use ApiPlatform\Tests\Fixtures\TestBundle\Document\FooEmbeddable as FooEmbeddableDocument;
6162
use ApiPlatform\Tests\Fixtures\TestBundle\Document\FourthLevel as FourthLevelDocument;
6263
use ApiPlatform\Tests\Fixtures\TestBundle\Document\Greeting as GreetingDocument;
6364
use ApiPlatform\Tests\Fixtures\TestBundle\Document\InitializeInput as InitializeInputDocument;
@@ -144,6 +145,7 @@
144145
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FileConfigDummy;
145146
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Foo;
146147
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FooDummy;
148+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FooEmbeddable;
147149
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FourthLevel;
148150
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Greeting;
149151
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\InitializeInput;
@@ -354,7 +356,7 @@ public function thereAreFooObjectsWithFakeNames(int $nb): void
354356
/**
355357
* @Given there are :nb fooDummy objects with fake names
356358
*/
357-
public function thereAreFooDummyObjectsWithFakeNames($nb): void
359+
public function thereAreFooDummyObjectsWithFakeNames(int $nb, $embedd = false): void
358360
{
359361
$names = ['Hawsepipe', 'Ephesian', 'Sthenelus', 'Separativeness', 'Balbo'];
360362
$dummies = ['Lorem', 'Ipsum', 'Dolor', 'Sit', 'Amet'];
@@ -365,6 +367,11 @@ public function thereAreFooDummyObjectsWithFakeNames($nb): void
365367

366368
$foo = $this->buildFooDummy();
367369
$foo->setName($names[$i]);
370+
if ($embedd) {
371+
$embeddedFoo = $this->buildFooEmbeddable();
372+
$embeddedFoo->setDummyName('embedded'.$names[$i]);
373+
$foo->setEmbeddedFoo($embeddedFoo);
374+
}
368375
$foo->setDummy($dummy);
369376
for ($j = 0; $j < 3; ++$j) {
370377
$soMany = $this->buildSoMany();
@@ -379,6 +386,14 @@ public function thereAreFooDummyObjectsWithFakeNames($nb): void
379386
$this->manager->flush();
380387
}
381388

389+
/**
390+
* @Given there is a fooDummy objects with fake names and embeddable
391+
*/
392+
public function thereAreFooDummyObjectsWithFakeNamesAndEmbeddable(): void
393+
{
394+
$this->thereAreFooDummyObjectsWithFakeNames(1, true);
395+
}
396+
382397
/**
383398
* @Given there are :nb dummy group objects
384399
*/
@@ -2399,6 +2414,11 @@ private function buildFooDummy(): FooDummy|FooDummyDocument
23992414
return $this->isOrm() ? new FooDummy() : new FooDummyDocument();
24002415
}
24012416

2417+
private function buildFooEmbeddable(): FooEmbeddable|FooEmbeddableDocument
2418+
{
2419+
return $this->isOrm() ? new FooEmbeddable() : new FooEmbeddableDocument();
2420+
}
2421+
24022422
private function buildFourthLevel(): FourthLevel|FourthLevelDocument
24032423
{
24042424
return $this->isOrm() ? new FourthLevel() : new FourthLevelDocument();

tests/Fixtures/TestBundle/Document/FooDummy.php

+30-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313

1414
namespace ApiPlatform\Tests\Fixtures\TestBundle\Document;
1515

16+
use ApiPlatform\Metadata\ApiProperty;
1617
use ApiPlatform\Metadata\ApiResource;
18+
use ApiPlatform\Metadata\GraphQl\Mutation;
1719
use ApiPlatform\Metadata\GraphQl\QueryCollection;
1820
use Doctrine\Common\Collections\ArrayCollection;
1921
use Doctrine\Common\Collections\Collection;
@@ -24,7 +26,7 @@
2426
*
2527
* @author Vincent Chalamon <[email protected]>
2628
*/
27-
#[ApiResource(graphQlOperations: [new QueryCollection(name: 'collection_query', paginationType: 'page')], order: ['dummy.name'])]
29+
#[ApiResource(graphQlOperations: [new QueryCollection(name: 'collection_query', paginationType: 'page'), new Mutation(name: 'update')], order: ['dummy.name'])]
2830
#[ODM\Document]
2931
class FooDummy
3032
{
@@ -33,17 +35,26 @@ class FooDummy
3335
*/
3436
#[ODM\Id(strategy: 'INCREMENT', type: 'int')]
3537
private ?int $id = null;
38+
3639
/**
3740
* @var string The foo name
3841
*/
3942
#[ODM\Field]
4043
private $name;
44+
45+
#[ODM\Field(nullable: true)]
46+
private $nonWritableProp;
47+
4148
/**
4249
* @var Dummy The foo dummy
4350
*/
4451
#[ODM\ReferenceOne(targetDocument: Dummy::class, cascade: ['persist'], storeAs: 'id')]
4552
private ?Dummy $dummy = null;
4653

54+
#[ApiProperty(readableLink: true, writableLink: true)]
55+
#[ODM\EmbedOne(targetDocument: FooEmbeddable::class)]
56+
private ?FooEmbeddable $embeddedFoo = null;
57+
4758
/**
4859
* @var Collection<SoMany>
4960
*/
@@ -52,6 +63,7 @@ class FooDummy
5263

5364
public function __construct()
5465
{
66+
$this->nonWritableProp = 'readonly';
5567
$this->soManies = new ArrayCollection();
5668
}
5769

@@ -70,6 +82,11 @@ public function getName()
7082
return $this->name;
7183
}
7284

85+
public function getNonWritableProp()
86+
{
87+
return $this->nonWritableProp;
88+
}
89+
7390
public function getDummy(): ?Dummy
7491
{
7592
return $this->dummy;
@@ -79,4 +96,16 @@ public function setDummy(Dummy $dummy): void
7996
{
8097
$this->dummy = $dummy;
8198
}
99+
100+
public function getEmbeddedFoo(): ?FooEmbeddable
101+
{
102+
return $this->embeddedFoo && !$this->embeddedFoo->getDummyName() && !$this->embeddedFoo->getNonWritableProp() ? null : $this->embeddedFoo;
103+
}
104+
105+
public function setEmbeddedFoo(?FooEmbeddable $embeddedFoo): self
106+
{
107+
$this->embeddedFoo = $embeddedFoo;
108+
109+
return $this;
110+
}
82111
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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\Tests\Fixtures\TestBundle\Document;
15+
16+
use ApiPlatform\Metadata\ApiProperty;
17+
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
18+
19+
/**
20+
* Embeddable Foo.
21+
*/
22+
#[ODM\EmbeddedDocument]
23+
class FooEmbeddable
24+
{
25+
/**
26+
* @var string|null The dummy name
27+
*/
28+
#[ApiProperty(identifier: true)]
29+
#[ODM\Field(type: 'string')]
30+
private ?string $dummyName = null;
31+
32+
#[ODM\Field(nullable: true)]
33+
private $nonWritableProp;
34+
35+
public function __construct()
36+
{
37+
}
38+
39+
public function getDummyName(): ?string
40+
{
41+
return $this->dummyName;
42+
}
43+
44+
public function setDummyName(string $dummyName): void
45+
{
46+
$this->dummyName = $dummyName;
47+
}
48+
49+
public function getNonWritableProp()
50+
{
51+
return $this->nonWritableProp;
52+
}
53+
}

tests/Fixtures/TestBundle/Entity/EmbeddableDummy.php

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity;
1515

16+
use ApiPlatform\Metadata\ApiProperty;
1617
use Doctrine\ORM\Mapping as ORM;
1718
use Symfony\Component\Serializer\Annotation\Groups;
1819
use Symfony\Component\Validator\Constraints as Assert;
@@ -28,6 +29,7 @@ class EmbeddableDummy
2829
/**
2930
* @var string The dummy name
3031
*/
32+
#[ApiProperty(identifier: true)]
3133
#[ORM\Column(nullable: true)]
3234
#[Groups(['embed'])]
3335
private ?string $dummyName = null;

0 commit comments

Comments
 (0)