Core: cannot perform GraphQL sub-selections on non-resource collections

Created on 24 Aug 2019  路  11Comments  路  Source: api-platform/core

This issue is related to #2740 and #2687, but it's different enough that I felt it deserves a separate issue.

I'm having trouble performing sub-selection queries on non-doctrine, non-resource collections. Here's an example. Say I have a very simple resource class:

/**
 * @ApiResource
 */
class Book
{
    /**
     * @ApiProperty(identifier=true)
     *
     * @var string
     */
    public $id;

    /**
     * @var Chapter[]
     */
    public $chapters;
}

with Chapter is a simple POPO:

class Chapter
{
    /**
     * @var int
     */
    public $startingPage;

    /**
     * @var string
     */
    public $name;
}

For testing purposes I have custom data provider that serves dummy instances of books, each with several chapters. I also have a Normalizer that handles normalizing Chapters. This works well initially:

basic query

But say I want to fetch only the chapter names, I run into the following error:

Field \"chapters\" of type \"Iterable!\" must not have a sub selection.

sub-selection errors

This is a contrived example, of course, but you can imagine some of the properties on the nested object being bandwidth-heavy or expensive to compute.

I saw that there has been quite a bit of refactoring of the GraphQL code in master, but unfortunately I'm experiencing this issue with both the 2.4 branch as well as master. I also tried to go through the GraphQL code but I'll admit that it's a bit over my head.

Any guidance would be appreciated. Thanks!

GraphQL duplicate enhancement

All 11 comments

Today the schema is built only with the resources. That's why there is this issue.
I plan to do it relatively soon since it's a very asked feature.

Creating a custom type (from ObjectType) then using TypeConverter to replace the default type with the custom type seems to work for me.

Yes it can be a solution.

Creating a custom type (from ObjectType) then using TypeConverter to replace the default type with the custom type seems to work for me.

@hoangnd25 any chance you could share a little pseudo-code showing your custom type and TypeConverter? Would really appreciate any tips you could offer.

You could create custom type like this

<?php

use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;

class ChapterType extends ObjectType
{
    public function __construct()
    {
        parent::__construct([
            'name' => 'Chapter',
            'fields' => [
                'startingPage' => [
                    'type' => Type::int(),
                ],
                'name' => [
                    'type' => Type::string()
                ],
            ],
            'resolveField' => [$this, 'resolve']
        ]);
    }

    public function resolve($root, $args, $context, ResolveInfo $resolveInfo)
    {
         if ($resolveInfo->fieldName === 'startingPage') {
                return $root['startingPage'];
         }
    }
}

Then use TypeConverter to replace it

<?php

final class TypeConverter implements TypeConverterInterface
{

    public function convertType(Type $type, bool $input, ?string $queryName, ?string $mutationName, string $resourceClass, string $rootResource, ?string $property, int $depth)
    {
        if (Type::BUILTIN_TYPE_OBJECT === $type->getBuiltinType()
            && is_a($type->getClassName(), Chapter::class, true)
        ) {
            return 'Chapter';

            // or use the type container
            // return $this->typeContainer->get(ChapterType::class)
        }

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

You would need to extend ObjectType or equivalent, ApiPlatform\Core\GraphQl\Type\Definition\TypeInterface only works for scalar types

You might need to create a normalizer for Chapter as well.

This is just a hack imo.

Thank you, @hoangnd25! I will give this a shot. I agree that it's a bit of a hack, but hopefully it'll be a stop-gap until @alanpoulain and crew hook us up with a more elegant solution.

You would need to extend ObjectType or equivalent, ApiPlatform\Core\GraphQl\Type\Definition\TypeInterface only works for scalar types

That's where I got hung up before. Thanks for the tip.

@alanpoulain perhaps you or someone else can share a correct way on configuring the annotations on the model classes? Or is there some example for this case?

Setting the child property to @var ChildClass[] still results in Iterable for me, turning it into Collection<ChildClass> does result in a correct Introspection result however query results are empty in this case while the returned object from the dataprovider definitely isn't.

Tested with api-platform/core version 2.5.4 and 2.5.5

Thanks!

I had this issue with a regular object type.

<?php

declare(strict_types=1);

namespace App\Type\Definition;

use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use ApiPlatform\Core\GraphQl\Type\Definition\TypeInterface;

final class SeoPropsType extends ObjectType implements TypeInterface
{
    public function __construct(array $config = [])
    {
        $config['fields'] = [
            'title' => Type::string(),
            'description' => Type::string(),
        ];

        parent::__construct($config);
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function serialize($value)
    {
        return $value;
    }

    public function parseValue($value)
    {
        return $value;
    }

    public function parseLiteral($valueNode, ?array $variables = null)
    {
        return $valueNode;
    }
}

because TypeInterface extends LeafType and it causes the error.

The solution was to not implement TypeInterface and register the type manually
_(because only TypeInterface implementations are registered automatically)_

final class SeoPropsType extends ObjectType { ... }
services:
    App\Type\Definition\SeoPropsType:
      tags: [ 'api_platform.graphql.type' ]

Isn't TypeInterface extends LeafType a bug?

Because this is a conflict
MyType extends ObjectType implements TypeInterface馃LeafType
Object is not a Leaf

@alanpoulain Thanks. But I can't use it because it's not merged and I can't merge it in my fork because I get conflicts.

Was this page helpful?
0 / 5 - 0 ratings