Skip to content

Commit

Permalink
fix(graphql): link relations requires the property (#5169)
Browse files Browse the repository at this point in the history
Co-authored-by: Alan Poulain <contact@alanpoulain.eu>
  • Loading branch information
develth and alanpoulain authored Dec 13, 2022
1 parent 2604044 commit 3d3c2c7
Show file tree
Hide file tree
Showing 11 changed files with 387 additions and 12 deletions.
48 changes: 48 additions & 0 deletions features/graphql/query.feature
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,54 @@ Feature: GraphQL query support
And the JSON node "data.dummy.name" should be equal to "Dummy #1"
And the JSON node "data.dummy.name_converted" should be equal to "Converted 1"

@createSchema
Scenario: Retrieve an item with different relations to the same resource
Given there are 2 multiRelationsDummy objects having each a manyToOneRelation, 2 manyToManyRelations and 3 oneToManyRelations
When I send the following GraphQL request:
"""
{
multiRelationsDummy(id: "/multi_relations_dummies/2") {
id
name
manyToOneRelation {
id
name
}
manyToManyRelations {
edges{
node {
id
name
}
}
}
oneToManyRelations {
edges{
node {
id
name
}
}
}
}
}
"""
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/json"
And the JSON node "data.multiRelationsDummy.id" should be equal to "/multi_relations_dummies/2"
And the JSON node "data.multiRelationsDummy.name" should be equal to "Dummy #2"
And the JSON node "data.multiRelationsDummy.manyToOneRelation.id" should not be null
And the JSON node "data.multiRelationsDummy.manyToOneRelation.name" should be equal to "RelatedManyToOneDummy #2"
And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges" should have 2 element
And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges[1].node.id" should not be null
And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges[0].node.name" should be equal to "RelatedManyToManyDummy12"
And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges[1].node.name" should be equal to "RelatedManyToManyDummy22"
And the JSON node "data.multiRelationsDummy.oneToManyRelations.edges" should have 3 element
And the JSON node "data.multiRelationsDummy.oneToManyRelations.edges[1].node.id" should not be null
And the JSON node "data.multiRelationsDummy.oneToManyRelations.edges[0].node.name" should be equal to "RelatedOneToManyDummy12"
And the JSON node "data.multiRelationsDummy.oneToManyRelations.edges[2].node.name" should be equal to "RelatedOneToManyDummy32"

@createSchema
Scenario: Retrieve a Relay Node
Given there are 2 dummy objects with relatedDummy
Expand Down
21 changes: 14 additions & 7 deletions src/Doctrine/Common/State/LinksHandlerTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,20 @@ private function getLinks(string $resourceClass, Operation $operation, array $co
return $links;
}

$newLinks = [];
$newLink = null;
$linkProperty = $context['linkProperty'] ?? null;

foreach ($links as $link) {
if ($linkClass === $link->getFromClass()) {
$newLinks[] = $link;
if ($linkClass === $link->getFromClass() && $linkProperty === $link->getFromProperty()) {
$newLink = $link;
break;
}
}

if ($newLink) {
return [$newLink];
}

// Using GraphQL, it's possible that we won't find a GraphQL Operation of the same type (e.g. it is disabled).
try {
$resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($linkClass);
Expand All @@ -62,16 +68,17 @@ private function getLinks(string $resourceClass, Operation $operation, array $co
}

foreach ($this->getOperationLinks($linkedOperation ?? null) as $link) {
if ($resourceClass === $link->getToClass()) {
$newLinks[] = $link;
if ($resourceClass === $link->getToClass() && $linkProperty === $link->getFromProperty()) {
$newLink = $link;
break;
}
}

if (!$newLinks) {
if (!$newLink) {
throw new RuntimeException(sprintf('The class "%s" cannot be retrieved from "%s".', $resourceClass, $linkClass));
}

return $newLinks;
return [$newLink];
}

private function getIdentifierValue(array &$identifiers, string $name = null): mixed
Expand Down
1 change: 1 addition & 0 deletions src/GraphQl/Resolver/Stage/ReadStage.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ public function __invoke(?string $resourceClass, ?string $rootClass, Operation $
if (isset($source[$info->fieldName], $source[ItemNormalizer::ITEM_IDENTIFIERS_KEY], $source[ItemNormalizer::ITEM_RESOURCE_CLASS_KEY])) {
$uriVariables = $source[ItemNormalizer::ITEM_IDENTIFIERS_KEY];
$normalizationContext['linkClass'] = $source[ItemNormalizer::ITEM_RESOURCE_CLASS_KEY];
$normalizationContext['linkProperty'] = $info->fieldName;
}

return $this->provider->provide($operation, $uriVariables, $normalizationContext);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,16 @@ private function mergeLinks(array $links, array $toMergeLinks): array
{
$classLinks = [];
foreach ($links as $link) {
$classLinks[$link->getToClass()] = $link;
$classLinks[$link->getToClass().'#'.$link->getFromProperty()] = $link;
}

foreach ($toMergeLinks as $link) {
if (isset($classLinks[$link->getToClass()])) {
$classLinks[$link->getToClass()] = $classLinks[$link->getToClass()]->withLink($link);
if (null !== $prevLink = $classLinks[$link->getToClass().'#'.$link->getFromProperty()] ?? null) {
$classLinks[$link->getToClass().'#'.$link->getFromProperty()] = $prevLink->withLink($link);

continue;
}
$classLinks[$link->getToClass()] = $link;
$classLinks[$link->getToClass().'#'.$link->getFromProperty()] = $link;
}

return array_values($classLinks);
Expand Down
50 changes: 50 additions & 0 deletions tests/Behat/DoctrineContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@
use ApiPlatform\Tests\Fixtures\TestBundle\Document\InitializeInput as InitializeInputDocument;
use ApiPlatform\Tests\Fixtures\TestBundle\Document\IriOnlyDummy as IriOnlyDummyDocument;
use ApiPlatform\Tests\Fixtures\TestBundle\Document\MaxDepthDummy as MaxDepthDummyDocument;
use ApiPlatform\Tests\Fixtures\TestBundle\Document\MultiRelationsDummy as MultiRelationsDummyDocument;
use ApiPlatform\Tests\Fixtures\TestBundle\Document\MultiRelationsRelatedDummy as MultiRelationsRelatedDummyDocument;
use ApiPlatform\Tests\Fixtures\TestBundle\Document\NetworkPathDummy as NetworkPathDummyDocument;
use ApiPlatform\Tests\Fixtures\TestBundle\Document\NetworkPathRelationDummy as NetworkPathRelationDummyDocument;
use ApiPlatform\Tests\Fixtures\TestBundle\Document\Order as OrderDocument;
Expand Down Expand Up @@ -138,6 +140,8 @@
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\InternalUser;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\IriOnlyDummy;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MaxDepthDummy;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MultiRelationsDummy;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MultiRelationsRelatedDummy;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\NetworkPathDummy;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\NetworkPathRelationDummy;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Order;
Expand Down Expand Up @@ -759,6 +763,42 @@ public function thereAreDummyObjectsWithRelatedDummies(int $nb, int $nbrelated):
$this->manager->flush();
}

/**
* @Given there are :nb multiRelationsDummy objects having each a manyToOneRelation, :nbmtmr manyToManyRelations and :nbotmr oneToManyRelations
*/
public function thereAreMultiRelationsDummyObjectsHavingEachAManyToOneRelationManyToManyRelationsAndOneToManyRelations(int $nb, int $nbmtmr, int $nbotmr): void
{
for ($i = 1; $i <= $nb; ++$i) {
$relatedDummy = $this->buildMultiRelationsRelatedDummy();
$relatedDummy->name = 'RelatedManyToOneDummy #'.$i;

$dummy = $this->buildMultiRelationsDummy();
$dummy->name = 'Dummy #'.$i;
$dummy->setManyToOneRelation($relatedDummy);

for ($j = 1; $j <= $nbmtmr; ++$j) {
$manyToManyItem = $this->buildMultiRelationsRelatedDummy();
$manyToManyItem->name = 'RelatedManyToManyDummy'.$j.$i;
$this->manager->persist($manyToManyItem);

$dummy->addManyToManyRelation($manyToManyItem);
}

for ($j = 1; $j <= $nbotmr; ++$j) {
$oneToManyItem = $this->buildMultiRelationsRelatedDummy();
$oneToManyItem->name = 'RelatedOneToManyDummy'.$j.$i;
$oneToManyItem->setOneToManyRelation($dummy);
$this->manager->persist($oneToManyItem);

$dummy->addOneToManyRelation($oneToManyItem);
}

$this->manager->persist($relatedDummy);
$this->manager->persist($dummy);
}
$this->manager->flush();
}

/**
* @Given there are :nb dummy objects with dummyDate
* @Given there is :nb dummy object with dummyDate
Expand Down Expand Up @@ -2300,4 +2340,14 @@ private function buildPayment(string $amount): Payment|PaymentDocument
{
return $this->isOrm() ? new Payment($amount) : new PaymentDocument($amount);
}

private function buildMultiRelationsDummy(): MultiRelationsDummy|MultiRelationsDummyDocument
{
return $this->isOrm() ? new MultiRelationsDummy() : new MultiRelationsDummyDocument();
}

private function buildMultiRelationsRelatedDummy(): MultiRelationsRelatedDummy|MultiRelationsRelatedDummyDocument
{
return $this->isOrm() ? new MultiRelationsRelatedDummy() : new MultiRelationsRelatedDummyDocument();
}
}
79 changes: 79 additions & 0 deletions tests/Fixtures/TestBundle/Document/MultiRelationsDummy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Tests\Fixtures\TestBundle\Document;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\GraphQl\QueryCollection;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;

/**
* Dummy using different kind of relations to the same resource.
*
* @author Thomas Helmrich <thomas@gigabit.de>
*/
#[ApiResource(graphQlOperations: [new QueryCollection(), new Query()])]
#[ODM\Document]
class MultiRelationsDummy
{
#[ODM\Id(strategy: 'INCREMENT', type: 'int')]
private ?int $id = null;

#[ODM\Field(type: 'string')]
public string $name;

#[ODM\ReferenceOne(targetDocument: MultiRelationsRelatedDummy::class, storeAs: 'id', nullable: true)]
public ?MultiRelationsRelatedDummy $manyToOneRelation = null;

/** @var Collection<int, MultiRelationsRelatedDummy> */
#[ODM\ReferenceMany(targetDocument: MultiRelationsRelatedDummy::class, storeAs: 'id', nullable: true)]
public Collection $manyToManyRelations;

/** @var Collection<int, MultiRelationsRelatedDummy> */
#[ODM\ReferenceMany(targetDocument: MultiRelationsRelatedDummy::class, mappedBy: 'oneToManyRelation', storeAs: 'id')]
public Collection $oneToManyRelations;

public function __construct()
{
$this->manyToManyRelations = new ArrayCollection();
$this->oneToManyRelations = new ArrayCollection();
}

public function getId(): ?int
{
return $this->id;
}

public function getManyToOneRelation(): ?MultiRelationsRelatedDummy
{
return $this->manyToOneRelation;
}

public function setManyToOneRelation(?MultiRelationsRelatedDummy $relatedMultiUsedDummy): void
{
$this->manyToOneRelation = $relatedMultiUsedDummy;
}

public function addManyToManyRelation(MultiRelationsRelatedDummy $relatedMultiUsedDummy): void
{
$this->manyToManyRelations->add($relatedMultiUsedDummy);
}

public function addOneToManyRelation(MultiRelationsRelatedDummy $relatedMultiUsedDummy): void
{
$this->oneToManyRelations->add($relatedMultiUsedDummy);
}
}
53 changes: 53 additions & 0 deletions tests/Fixtures/TestBundle/Document/MultiRelationsRelatedDummy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Tests\Fixtures\TestBundle\Document;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\GraphQl\QueryCollection;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;

/**
* Dummy used in different kind of relations in the same resource.
*
* @author Thomas Helmrich <thomas@gigabit.de>
*/
#[ApiResource(graphQlOperations: [new QueryCollection(), new Query()])]
#[ODM\Document]
class MultiRelationsRelatedDummy
{
#[ODM\Id(strategy: 'INCREMENT', type: 'int')]
private ?int $id = null;

#[ODM\Field(type: 'string', nullable: true)]
public ?string $name;

#[ODM\ReferenceOne(targetDocument: MultiRelationsDummy::class, inversedBy: 'oneToManyRelations', nullable: true, storeAs: 'id')]
private ?MultiRelationsDummy $oneToManyRelation;

public function getId(): ?int
{
return $this->id;
}

public function getOneToManyRelation(): ?MultiRelationsDummy
{
return $this->oneToManyRelation;
}

public function setOneToManyRelation(?MultiRelationsDummy $oneToManyRelation): void
{
$this->oneToManyRelation = $oneToManyRelation;
}
}
Loading

0 comments on commit 3d3c2c7

Please sign in to comment.