Skip to content

Commit

Permalink
fix(graphql): use default nested query operations (#5174)
Browse files Browse the repository at this point in the history
Instead of relying on a resource metadata factory to retrieve a "dynamic" nested query operation, this PR adds default nested query operations if the resource does not have one.
It simplifies a lot of things and catching the OperationNotFound exception is not needed anymore.
Since operations are "nested", they do not appear as top-level queries.
This PR also improves the XML/YAML compatibility.
  • Loading branch information
alanpoulain authored Nov 10, 2022
1 parent dbf4447 commit 5bc84ce
Show file tree
Hide file tree
Showing 25 changed files with 216 additions and 324 deletions.

This file was deleted.

23 changes: 10 additions & 13 deletions src/GraphQl/Type/FieldsBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
namespace ApiPlatform\GraphQl\Type;

use ApiPlatform\Api\ResourceClassResolverInterface;
use ApiPlatform\Exception\OperationNotFoundException;
use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactoryInterface;
use ApiPlatform\GraphQl\Type\Definition\TypeInterface;
use ApiPlatform\Metadata\GraphQl\Mutation;
Expand Down Expand Up @@ -45,7 +44,7 @@
*/
final class FieldsBuilder implements FieldsBuilderInterface
{
public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, private readonly TypeBuilderInterface $typeBuilder, private readonly TypeConverterInterface $typeConverter, private readonly ResolverFactoryInterface $itemResolverFactory, private readonly ResolverFactoryInterface $collectionResolverFactory, private readonly ResolverFactoryInterface $itemMutationResolverFactory, private readonly ResolverFactoryInterface $itemSubscriptionResolverFactory, private readonly ContainerInterface $filterLocator, private readonly Pagination $pagination, private readonly ?NameConverterInterface $nameConverter, private readonly string $nestingSeparator, private readonly ?ResourceMetadataCollectionFactoryInterface $graphQlNestedOperationResourceMetadataFactory = null)
public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, private readonly TypeBuilderInterface $typeBuilder, private readonly TypeConverterInterface $typeConverter, private readonly ResolverFactoryInterface $itemResolverFactory, private readonly ResolverFactoryInterface $collectionResolverFactory, private readonly ResolverFactoryInterface $itemMutationResolverFactory, private readonly ResolverFactoryInterface $itemSubscriptionResolverFactory, private readonly ContainerInterface $filterLocator, private readonly Pagination $pagination, private readonly ?NameConverterInterface $nameConverter, private readonly string $nestingSeparator)
{
}

Expand All @@ -68,6 +67,10 @@ public function getNodeQueryFields(): array
*/
public function getItemQueryFields(string $resourceClass, Operation $operation, array $configuration): array
{
if ($operation instanceof Query && $operation->getNested()) {
return [];
}

$fieldName = lcfirst('item_query' === $operation->getName() ? $operation->getShortName() : $operation->getName().$operation->getShortName());

if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $operation->getDescription(), $operation->getDeprecationReason(), new Type(Type::BUILTIN_TYPE_OBJECT, true, $resourceClass), $resourceClass, false, $operation)) {
Expand All @@ -85,6 +88,10 @@ public function getItemQueryFields(string $resourceClass, Operation $operation,
*/
public function getCollectionQueryFields(string $resourceClass, Operation $operation, array $configuration): array
{
if ($operation instanceof Query && $operation->getNested()) {
return [];
}

$fieldName = lcfirst('collection_query' === $operation->getName() ? $operation->getShortName() : $operation->getName().$operation->getShortName());

if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $operation->getDescription(), $operation->getDeprecationReason(), new Type(Type::BUILTIN_TYPE_OBJECT, false, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, $resourceClass)), $resourceClass, false, $operation)) {
Expand Down Expand Up @@ -257,17 +264,7 @@ private function getResourceFieldConfiguration(?string $property, ?string $field
$resourceOperation = $rootOperation;
if ($resourceClass && $rootOperation->getClass() && $this->resourceClassResolver->isResourceClass($resourceClass) && $rootOperation->getClass() !== $resourceClass) {
$resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass);
try {
$resourceOperation = $resourceMetadataCollection->getOperation($isCollectionType ? 'collection_query' : 'item_query');
} catch (OperationNotFoundException) {
// If there is no query operation for a nested resource we force one to exist
$nestedResourceMetadataCollection = $this->graphQlNestedOperationResourceMetadataFactory->create($resourceClass);
$resourceOperation = $nestedResourceMetadataCollection->getOperation($isCollectionType ? 'collection_query' : 'item_query');
// Add filters from the metadata defined on the resource itself.
if ($filters = $resourceMetadataCollection[0]?->getFilters()) {
$resourceOperation = $resourceOperation->withFilters($filters);
}
}
$resourceOperation = $resourceMetadataCollection->getOperation($isCollectionType ? 'collection_query' : 'item_query');
}

if (!$resourceOperation instanceof Operation) {
Expand Down
2 changes: 1 addition & 1 deletion src/GraphQl/Type/SchemaBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public function getSchema(): Schema
foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) {
$resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass);
foreach ($resourceMetadataCollection as $resourceMetadata) {
foreach ($resourceMetadata->getGraphQlOperations() ?? [] as $operationName => $operation) {
foreach ($resourceMetadata->getGraphQlOperations() ?? [] as $operation) {
$configuration = null !== $operation->getArgs() ? ['args' => $operation->getArgs()] : [];

if ($operation instanceof Query && $operation instanceof CollectionOperationInterface) {
Expand Down
18 changes: 4 additions & 14 deletions src/GraphQl/Type/TypeBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
use ApiPlatform\Metadata\GraphQl\Mutation;
use ApiPlatform\Metadata\GraphQl\Operation;
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\GraphQl\QueryCollection;
use ApiPlatform\Metadata\GraphQl\Subscription;
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
use ApiPlatform\State\Pagination\Pagination;
Expand Down Expand Up @@ -74,12 +73,9 @@ public function getResourceObjectType(?string $resourceClass, ResourceMetadataCo
}

if ('item_query' === $operationName || 'collection_query' === $operationName) {
// Test if the collection/item operation exists and it has different groups
try {
if ($resourceMetadataCollection->getOperation($operation instanceof CollectionOperationInterface ? 'item_query' : 'collection_query')->getNormalizationContext() !== $operation->getNormalizationContext()) {
$shortName .= $operation instanceof CollectionOperationInterface ? 'Collection' : 'Item';
}
} catch (OperationNotFoundException) {
// Test if the collection/item has different groups
if ($resourceMetadataCollection->getOperation($operation instanceof CollectionOperationInterface ? 'item_query' : 'collection_query')->getNormalizationContext() !== $operation->getNormalizationContext()) {
$shortName .= $operation instanceof CollectionOperationInterface ? 'Collection' : 'Item';
}
}

Expand Down Expand Up @@ -126,13 +122,7 @@ public function getResourceObjectType(?string $resourceClass, ResourceMetadataCo
$wrappedOperationName = $operation instanceof Query ? $operationName : 'item_query';
}

try {
$wrappedOperation = $resourceMetadataCollection->getOperation($wrappedOperationName);
} catch (OperationNotFoundException) {
$wrappedOperation = ('collection_query' === $wrappedOperationName ? new QueryCollection() : new Query())
->withResource($resourceMetadataCollection[0])
->withName($wrappedOperationName);
}
$wrappedOperation = $resourceMetadataCollection->getOperation($wrappedOperationName);

$fields = [
lcfirst($wrappedOperation->getShortName()) => $this->getResourceObjectType($resourceClass, $resourceMetadataCollection, $wrappedOperation instanceof Operation ? $wrappedOperation : null, $input, true, $depth),
Expand Down
21 changes: 7 additions & 14 deletions src/GraphQl/Type/TypeConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,8 @@
use ApiPlatform\Exception\InvalidArgumentException;
use ApiPlatform\Exception\OperationNotFoundException;
use ApiPlatform\Exception\ResourceClassNotFoundException;
use ApiPlatform\Metadata\CollectionOperationInterface;
use ApiPlatform\Metadata\GraphQl\Operation;
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\GraphQl\QueryCollection;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use GraphQL\Error\SyntaxError;
Expand Down Expand Up @@ -144,25 +142,20 @@ private function getResourceType(Type $type, bool $input, Operation $rootOperati
}

$operationName = $rootOperation->getName();
$isCollection = $rootOperation instanceof CollectionOperationInterface || 'collection_query' === $operationName;
$isCollection = $this->typeBuilder->isCollection($type);

// We're retrieving the type of a property which is a relation to the rootResource
if ($resourceClass !== $rootResource && $property && $rootOperation instanceof Query) {
$isCollection = $this->typeBuilder->isCollection($type);
// We're retrieving the type of a property which is a relation to the root resource.
if ($resourceClass !== $rootResource && $rootOperation instanceof Query) {
$operationName = $isCollection ? 'collection_query' : 'item_query';
}

try {
$operation = $resourceMetadataCollection->getOperation($operationName);

if (!$operation instanceof Operation) {
throw new OperationNotFoundException();
}
} catch (OperationNotFoundException) {
/** @var Operation $operation */
$operation = ($isCollection ? new QueryCollection() : new Query())
->withResource($resourceMetadataCollection[0])
->withName($operationName);
$operation = $resourceMetadataCollection->getOperation($isCollection ? 'collection_query' : 'item_query');
}
if (!$operation instanceof Operation) {
throw new OperationNotFoundException();
}

return $this->typeBuilder->getResourceObjectType($resourceClass, $resourceMetadataCollection, $operation, $input, false, $depth);
Expand Down
13 changes: 12 additions & 1 deletion src/Metadata/Extractor/XmlResourceExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@

use ApiPlatform\Exception\InvalidArgumentException;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\GraphQl\DeleteMutation;
use ApiPlatform\Metadata\GraphQl\Mutation;
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\GraphQl\QueryCollection;
use ApiPlatform\Metadata\GraphQl\Subscription;
use ApiPlatform\Metadata\Post;
use Symfony\Component\Config\Util\XmlUtils;
Expand Down Expand Up @@ -332,9 +334,18 @@ private function buildGraphQlOperations(\SimpleXMLElement $resource, array $root
}
}

$collection = $this->phpize($operation, 'collection', 'bool', false);
if (Query::class === $class && $collection) {
$class = QueryCollection::class;
}

$delete = $this->phpize($operation, 'delete', 'bool', false);
if (Mutation::class === $class && $delete) {
$class = DeleteMutation::class;
}

$data[] = array_merge($datum, [
'graphql_operation_class' => $class,
'collection' => $this->phpize($operation, 'collection', 'bool'),
'resolver' => $this->phpize($operation, 'resolver', 'string'),
'args' => $this->buildArgs($operation),
'class' => $this->phpize($operation, 'class', 'string'),
Expand Down
2 changes: 2 additions & 0 deletions src/Metadata/Extractor/schema/resources.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@
<xsd:attributeGroup ref="base"/>
<xsd:attribute type="xsd:string" name="resolver"/>
<xsd:attribute type="xsd:string" name="class"/>
<xsd:attribute type="xsd:boolean" name="collection"/>
<xsd:attribute type="xsd:boolean" name="delete"/>
<xsd:attribute type="xsd:boolean" name="read"/>
<xsd:attribute type="xsd:boolean" name="deserialize"/>
<xsd:attribute type="xsd:boolean" name="validate"/>
Expand Down
1 change: 0 additions & 1 deletion src/Metadata/GraphQl/Operation.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
class Operation extends AbstractOperation
{
/**
* @param string $resolver
* @param mixed|null $input
* @param mixed|null $output
* @param mixed|null $mercure
Expand Down
17 changes: 16 additions & 1 deletion src/Metadata/GraphQl/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,24 @@ public function __construct(
?string $name = null,
$provider = null,
$processor = null,
array $extraProperties = []
array $extraProperties = [],

protected ?bool $nested = null,
) {
parent::__construct(...\func_get_args());
$this->name = $name ?: 'item_query';
}

public function getNested(): ?bool
{
return $this->nested;
}

public function withNested(?bool $nested = null): self
{
$self = clone $this;
$self->nested = $nested;

return $self;
}
}
17 changes: 16 additions & 1 deletion src/Metadata/GraphQl/QueryCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,24 @@ public function __construct(
?string $name = null,
$provider = null,
$processor = null,
array $extraProperties = []
array $extraProperties = [],

protected ?bool $nested = null,
) {
parent::__construct(...\func_get_args());
$this->name = $name ?: 'collection_query';
}

public function getNested(): ?bool
{
return $this->nested;
}

public function withNested(?bool $nested = null): self
{
$self = clone $this;
$self->nested = $nested;

return $self;
}
}
Loading

0 comments on commit 5bc84ce

Please sign in to comment.