Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 72 additions & 2 deletions features/graphql/query.feature
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Feature: GraphQL query support

@createSchema
Scenario: Retrieve an item with different relations to the same resource
Given there are 2 multiRelationsDummy objects having each a manyToOneRelation, 2 manyToManyRelations, 3 oneToManyRelations and 4 embeddedRelations
Given there are 2 multiRelationsDummy objects having each 1 manyToOneRelation, 2 manyToManyRelations, 3 oneToManyRelations and 4 embeddedRelations
When I send the following GraphQL request:
"""
{
Expand All @@ -33,6 +33,10 @@ Feature: GraphQL query support
id
name
}
manyToOneResolveRelation {
id
name
}
manyToManyRelations {
edges{
node {
Expand Down Expand Up @@ -70,7 +74,7 @@ Feature: GraphQL query support

@createSchema
Scenario: Retrieve embedded collections
Given there are 2 multiRelationsDummy objects having each a manyToOneRelation, 2 manyToManyRelations, 3 oneToManyRelations and 4 embeddedRelations
Given there are 2 multiRelationsDummy objects having each 1 manyToOneRelation, 2 manyToManyRelations, 3 oneToManyRelations and 4 embeddedRelations
When I send the following GraphQL request:
"""
{
Expand All @@ -81,6 +85,10 @@ Feature: GraphQL query support
id
name
}
manyToOneResolveRelation {
id
name
}
manyToManyRelations {
edges{
node {
Expand Down Expand Up @@ -113,10 +121,13 @@ Feature: GraphQL query support
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 "errors" should not exist
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.manyToOneResolveRelation.id" should not be null
And the JSON node "data.multiRelationsDummy.manyToOneResolveRelation.name" should be equal to "RelatedManyToOneResolveDummy #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 match "#RelatedManyToManyDummy(1|2)2#"
Expand All @@ -135,6 +146,65 @@ Feature: GraphQL query support
And the JSON node "data.multiRelationsDummy.nestedPaginatedCollection.edges[2].node.name" should be equal to "NestedPaginatedDummy3"
And the JSON node "data.multiRelationsDummy.nestedPaginatedCollection.edges[3].node.name" should be equal to "NestedPaginatedDummy4"

@createSchema
Scenario: Retrieve an item with different relations (all unset)
Given there are 2 multiRelationsDummy objects having each 0 manyToOneRelation, 0 manyToManyRelations, 0 oneToManyRelations and 0 embeddedRelations
When I send the following GraphQL request:
"""
{
multiRelationsDummy(id: "/multi_relations_dummies/2") {
id
name
manyToOneRelation {
id
name
}
manyToOneResolveRelation {
id
name
}
manyToManyRelations {
edges{
node {
id
name
}
}
}
oneToManyRelations {
edges{
node {
id
name
}
}
}
nestedCollection {
name
}
nestedPaginatedCollection {
edges{
node {
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 "errors" should not exist
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" should be null
And the JSON node "data.multiRelationsDummy.manyToOneResolveRelation" should be null
And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges" should have 0 element
And the JSON node "data.multiRelationsDummy.oneToManyRelations.edges" should have 0 element
And the JSON node "data.multiRelationsDummy.nestedCollection" should have 0 element
And the JSON node "data.multiRelationsDummy.nestedPaginatedCollection.edges" should have 0 element

@createSchema @!mongodb
Scenario: Retrieve an item with child relation to the same resource
Given there are tree dummies
Expand Down
3 changes: 2 additions & 1 deletion src/GraphQl/Resolver/Factory/CollectionResolverFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface;
use ApiPlatform\Metadata\GraphQl\Operation;
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Metadata\Util\CloneTrait;
use ApiPlatform\State\Pagination\ArrayPaginator;
use GraphQL\Type\Definition\ResolveInfo;
Expand All @@ -40,7 +41,7 @@ public function __construct(private readonly ReadStageInterface $readStage, priv
{
}

public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null): callable
public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable
{
return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation): ?array {
// If authorization has failed for a relation field (e.g. via ApiProperty security), the field is not present in the source: null can be returned directly to ensure the collection isn't in the response.
Expand Down
3 changes: 2 additions & 1 deletion src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use ApiPlatform\GraphQl\Resolver\Stage\WriteStageInterface;
use ApiPlatform\Metadata\DeleteOperationInterface;
use ApiPlatform\Metadata\GraphQl\Operation;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Metadata\Util\ClassInfoTrait;
use ApiPlatform\Metadata\Util\CloneTrait;
use GraphQL\Type\Definition\ResolveInfo;
Expand All @@ -44,7 +45,7 @@ public function __construct(private readonly ReadStageInterface $readStage, priv
{
}

public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null): callable
public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable
{
return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation): ?array {
if (null === $resourceClass || null === $operation) {
Expand Down
3 changes: 2 additions & 1 deletion src/GraphQl/Resolver/Factory/ItemResolverFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface;
use ApiPlatform\Metadata\GraphQl\Operation;
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Metadata\Util\ClassInfoTrait;
use ApiPlatform\Metadata\Util\CloneTrait;
use GraphQL\Type\Definition\ResolveInfo;
Expand All @@ -41,7 +42,7 @@ public function __construct(private readonly ReadStageInterface $readStage, priv
{
}

public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null): callable
public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable
{
return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation) {
// Data already fetched and normalized (field or nested resource)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use ApiPlatform\GraphQl\Subscription\MercureSubscriptionIriGeneratorInterface;
use ApiPlatform\GraphQl\Subscription\SubscriptionManagerInterface;
use ApiPlatform\Metadata\GraphQl\Operation;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Metadata\Util\ClassInfoTrait;
use ApiPlatform\Metadata\Util\CloneTrait;
use GraphQL\Type\Definition\ResolveInfo;
Expand All @@ -37,7 +38,7 @@ public function __construct(private readonly ReadStageInterface $readStage, priv
{
}

public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null): callable
public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable
{
return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation): ?array {
if (null === $resourceClass || null === $operation) {
Expand Down
30 changes: 20 additions & 10 deletions src/GraphQl/Resolver/Factory/ResolverFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use ApiPlatform\Metadata\GraphQl\Mutation;
use ApiPlatform\Metadata\GraphQl\Operation;
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\State\Pagination\ArrayPaginator;
use ApiPlatform\State\ProcessorInterface;
use ApiPlatform\State\ProviderInterface;
Expand All @@ -30,21 +31,30 @@ public function __construct(
) {
}

public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null): callable
public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable
{
return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation) {
// Data already fetched and normalized (field or nested resource)
if ($body = $source[$info->fieldName] ?? null) {
return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation, $propertyMetadataFactory) {
if (\array_key_exists($info->fieldName, $source ?? [])) {
$body = $source[$info->fieldName];

// special treatment for nested resources without a resolver/provider
if ($operation instanceof Query && $operation->getNested() && !$operation->getResolver() && (!$operation->getProvider() || NoopProvider::class === $operation->getProvider())) {
return $this->resolve($source, $args, $info, $rootClass, $operation, new ArrayPaginator($body, 0, \count($body)));
return \is_array($body) ? $this->resolve(
$source,
$args,
$info,
$rootClass,
$operation,
new ArrayPaginator($body, 0, \count($body))
) : $body;
}

return $body;
}

if (null === $resourceClass && \array_key_exists($info->fieldName, $source ?? [])) {
return $body;
$propertyMetadata = $rootClass ? $propertyMetadataFactory?->create($rootClass, $info->fieldName) : null;
$type = $propertyMetadata?->getBuiltinTypes()[0] ?? null;
// Data already fetched and normalized (field or nested resource)
if ($body || null === $resourceClass || ($type && !$type->isCollection())) {
return $body;
}
}

// If authorization has failed for a relation field (e.g. via ApiProperty security), the field is not present in the source: null can be returned directly to ensure the collection isn't in the response.
Expand Down
3 changes: 2 additions & 1 deletion src/GraphQl/Resolver/Factory/ResolverFactoryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace ApiPlatform\GraphQl\Resolver\Factory;

use ApiPlatform\Metadata\GraphQl\Operation;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;

/**
* Builds a GraphQL resolver.
Expand All @@ -22,5 +23,5 @@
*/
interface ResolverFactoryInterface
{
public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null): callable;
public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable;
}
6 changes: 5 additions & 1 deletion src/GraphQl/Tests/Resolver/Factory/ResolverFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@
namespace ApiPlatform\GraphQl\Tests\Resolver\Factory;

use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactory;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\GraphQl\Mutation;
use ApiPlatform\Metadata\GraphQl\Operation;
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\State\ProcessorInterface;
use ApiPlatform\State\ProviderInterface;
use GraphQL\Type\Definition\ResolveInfo;
Expand All @@ -38,11 +40,13 @@ public function testGraphQlResolver(?string $resourceClass = null, ?string $root
$provider->expects($this->once())->method('provide')->with($providedOperation ?: $operation, [], $context)->willReturn($body);
$processor = $this->createMock(ProcessorInterface::class);
$processor->expects($this->once())->method('process')->with($body, $processedOperation ?: $operation, [], $context)->willReturn($returnValue);
$propertyMetadataFactory = $this->createMock(PropertyMetadataFactoryInterface::class);
$propertyMetadataFactory->expects($this->once())->method('create')->with($rootClass, 'test')->willReturn(new ApiProperty(schema: ['type' => 'array']));
$resolveInfo = $this->createMock(ResolveInfo::class);
$resolveInfo->fieldName = 'test';

$resolverFactory = new ResolverFactory($provider, $processor);
$this->assertEquals($resolverFactory->__invoke($resourceClass, $rootClass, $operation)(['test' => null], [], [], $resolveInfo), $returnValue);
$this->assertEquals($resolverFactory->__invoke($resourceClass, $rootClass, $operation, $propertyMetadataFactory)(['test' => null], [], [], $resolveInfo), $returnValue);
}

public static function graphQlQueries(): array
Expand Down
2 changes: 1 addition & 1 deletion src/GraphQl/Type/FieldsBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,7 @@ private function getResourceFieldConfiguration(?string $property, ?string $field
if ($isStandardGraphqlType || $input) {
$resolve = null;
} else {
$resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $resourceOperation);
$resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $resourceOperation, $this->propertyMetadataFactory);
}
} else {
if ($isStandardGraphqlType || $input) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactoryInterface;
use ApiPlatform\Metadata\GraphQl\Operation;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use GraphQL\Type\Definition\ResolveInfo;
use Symfony\Component\HttpFoundation\RequestStack;

Expand All @@ -24,7 +25,7 @@ public function __construct(private readonly ResolverFactoryInterface $resolverF
{
}

public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null): callable
public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable
{
return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation) {
if ($this->requestStack && null !== $request = $this->requestStack->getCurrentRequest()) {
Expand Down
21 changes: 18 additions & 3 deletions tests/Behat/DoctrineContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
use ApiPlatform\Tests\Fixtures\TestBundle\Document\MultiRelationsNested as MultiRelationsNestedDocument;
use ApiPlatform\Tests\Fixtures\TestBundle\Document\MultiRelationsNestedPaginated as MultiRelationsNestedPaginatedDocument;
use ApiPlatform\Tests\Fixtures\TestBundle\Document\MultiRelationsRelatedDummy as MultiRelationsRelatedDummyDocument;
use ApiPlatform\Tests\Fixtures\TestBundle\Document\MultiRelationsResolveDummy as MultiRelationsResolveDummyDocument;
use ApiPlatform\Tests\Fixtures\TestBundle\Document\MusicGroup as MusicGroupDocument;
use ApiPlatform\Tests\Fixtures\TestBundle\Document\NetworkPathDummy as NetworkPathDummyDocument;
use ApiPlatform\Tests\Fixtures\TestBundle\Document\NetworkPathRelationDummy as NetworkPathRelationDummyDocument;
Expand Down Expand Up @@ -165,6 +166,7 @@
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MultiRelationsNested;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MultiRelationsNestedPaginated;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MultiRelationsRelatedDummy;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MultiRelationsResolveDummy;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MusicGroup;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\NetworkPathDummy;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\NetworkPathRelationDummy;
Expand Down Expand Up @@ -809,17 +811,24 @@ public function thereAreDummyObjectsWithRelatedDummies(int $nb, int $nbrelated):
}

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

$resolveDummy = $this->buildMultiRelationsResolveDummy();
$resolveDummy->name = 'RelatedManyToOneResolveDummy #'.$i;

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

if ($nbmtor) {
$dummy->setManyToOneRelation($relatedDummy);
$dummy->setManyToOneResolveRelation($resolveDummy);
}

for ($j = 1; $j <= $nbmtmr; ++$j) {
$manyToManyItem = $this->buildMultiRelationsRelatedDummy();
Expand Down Expand Up @@ -855,6 +864,7 @@ public function thereAreMultiRelationsDummyObjectsHavingEachAManyToOneRelationMa
$dummy->setNestedPaginatedCollection($nestedPaginated);

$this->manager->persist($relatedDummy);
$this->manager->persist($resolveDummy);
$this->manager->persist($dummy);
}
$this->manager->flush();
Expand Down Expand Up @@ -2658,6 +2668,11 @@ private function buildMultiRelationsRelatedDummy(): MultiRelationsRelatedDummy|M
return $this->isOrm() ? new MultiRelationsRelatedDummy() : new MultiRelationsRelatedDummyDocument();
}

private function buildMultiRelationsResolveDummy(): MultiRelationsResolveDummy|MultiRelationsResolveDummyDocument
{
return $this->isOrm() ? new MultiRelationsResolveDummy() : new MultiRelationsResolveDummyDocument();
}

private function buildMultiRelationsNested(): MultiRelationsNested|MultiRelationsNestedDocument
{
return $this->isOrm() ? new MultiRelationsNested() : new MultiRelationsNestedDocument();
Expand Down
Loading