api-platform / core

The server component of API Platform: hypermedia and GraphQL APIs in minutes
https://api-platform.com
MIT License
2.45k stars 874 forks source link

[GraphQl] Support for unions and interfaces for search functionality #2927

Open samzijlmans opened 5 years ago

samzijlmans commented 5 years ago

I am trying to create a general search functionality in my app that can return multiple types of objects. For each type of object I want to retrieve different fields.

In the graphQl specification this is supported using interfaces and unions. See this example below:

type Query {
    # Search across all content
    search(query: String!): [SearchResult]
}

union SearchResult = Conference | Festival | Concert | Venue
query {
  search(query: "Madison") {
    ... on Venue {
      id
      name
      address
    }

    ... on Festival {
      id
      name
      performers
    }

    ... on Concert {
      id
      name
      performingBand
    }

    ... on Conference {
      speakers
      workshops
    }
  }
}

See this guide for more details.

I tried making all my entities extend a 'base' entity and creating the search query on that entity, but that did not work.

Thanks in advance!

codedge commented 1 year ago

Is this something that is planned or any advice how to deal with it meanwhile? This rules out a big part of GraphQL capabilities.

shanginn commented 10 months ago

would love to see this implemented. in a meantime I got fragments working for collection for my setup like this.

I have different types, not OneComment and AnotherComment. this is just for demonstration

// api/src/Entity/Comment.php
//...
#[ORM\InheritanceType('SINGLE_TABLE')]
#[ORM\DiscriminatorColumn(name: 'type', type: 'string')]
#[ORM\DiscriminatorMap([
    'one'            => OneComment::class,
    'another'        => AnotherComment::class,
])]
class Comment
{
//...
// api/src/Entity/OneComment.php
//...
class OneComment extends Comment
{
//...
// api/src/Entity/AnotherComment.php
//...
class AnotherComment extends Comment
{
//...
# api/config/services.yaml:
# ...
    App\Type\TypeConverter:
        decorates: api_platform.graphql.type_converter

    App\Type\UnionCommentType:
        tags:
            - { name: api_platform.graphql.type }

    ApiPlatform\GraphQl\Type\TypeBuild
// api/src/Type/TypeConverter.php
<?php

namespace App\Type;

use ApiPlatform\GraphQl\Type\TypeConverterInterface;
use ApiPlatform\Metadata\GraphQl\Operation;
use App\Entity\Comment;
use GraphQL\Type\Definition\Type as GraphQLType;
use Symfony\Component\PropertyInfo\Type;

final class TypeConverter implements TypeConverterInterface
{
    public function __construct(
        private TypeConverterInterface $defaultTypeConverter,
        private UnionCommentType $unionCommentType,
    ) {
    }

    /**
     * {@inheritdoc}
     */
    public function convertType(
        Type $type,
        bool $input,
        Operation $rootOperation,
        string $resourceClass,
        string $rootResource,
        ?string $property,
        int $depth
    ): GraphQLType|string|null {
        if ($type->isCollection() && $type->getCollectionValueTypes()[0]->getClassName() === Comment::class) {
            return $this->unionCommentType;
        }

        return $this->defaultTypeConverter->convertType(
            type: $type,
            input: $input,
            rootOperation: $rootOperation,
            resourceClass: $resourceClass,
            rootResource: $rootResource,
            property: $property,
            depth: $depth,
        );
    }

    /**
     * {@inheritdoc}
     */
    public function resolveType(string $type): ?GraphQLType
    {
        return $this->defaultTypeConverter->resolveType($type);
    }
}
// api/src/Type/UnionCommentType.php
<?php

namespace App\Type;

use ApiPlatform\GraphQl\Type\Definition\TypeInterface;
use ApiPlatform\GraphQl\Type\TypeBuilderEnumInterface;
use ApiPlatform\Metadata\GraphQl\QueryCollection;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use App\Entity\OneComment;
use App\Entity\AnotherComment;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\UnionType;

final class UnionCommentType extends UnionType implements TypeInterface
{
    /**
     * @var array<string, ObjectType>
     */
    private array $commentTypes;

    public function __construct(
        private readonly TypeBuilderEnumInterface $typeBuilder,
        private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory,
    ) {
        $this->name              = 'UnionComment';
        $this->description       = 'Union type for all Comment types';
        $this->astNode           = null;
        $this->extensionASTNodes = [];

        $this->commentTypes = [
            OneComment::class            => $this->getCommentType(OneComment::class),
            AnotherComment::class        => $this->getCommentType(AnotherComment::class),
        ];

        $this->config = [
            'name'              => 'UnionComment',
            'description'       => 'Union type for all Comment types',
            'types'             => $this->commentTypes,
            'resolveType'       => fn ($objectValue) => $this->commentTypes[$objectValue['#itemResourceClass']],
            'astNode'           => null,
            'extensionASTNodes' => [],
        ];
    }

    public function getName(): string
    {
        return 'UnionComment';
    }

    private function getCommentType(string $resourceClass): ObjectType
    {
        $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass);

        $operationName = 'collection_query';
        $operation     = $resourceMetadataCollection->getOperation($operationName);

        assert($operation instanceof QueryCollection);

        $type = $this->typeBuilder->getResourceObjectType(
            resourceClass: $resourceClass,
            resourceMetadataCollection: $resourceMetadataCollection,
            operation: $operation,
            input: false,
            wrapped: false,
            depth: 0
        );

        assert($type instanceof ObjectType);

        return $type;
    }
}
query {
  comments() {
    collection {
      id
      ... on OneComment {
        id
        body
      }
      ... on AnotherComment {
        id
        body
      }
    }
  }
}