Skip to content

fix(graphql): link relations requires the property #5169

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Dec 13, 2022
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 <[email protected]>
*
* 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 <[email protected]>
*/
#[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 <[email protected]>
*
* 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 <[email protected]>
*/
#[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