api-platform / core

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

Calling GraphQL DeleteMutation ignores serialization groups and causes error #6432

Open danaki opened 1 week ago

danaki commented 1 week ago

API Plaform: v3.3

Description:

Calling

mutation {
  deleteGreeting(input: {id: "/api/greetings/cd"}) {
    greeting {
      id
      _id
      name
    }
    clientMutationId
  }
}

produces

        "debugMessage": "Cannot return null for non-nullable field \"Greeting.name\".",
        "file": "/mnt/c/work/ap-gql-bug/my-api/api/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",

while name is defined as non-nullable.

Expected behavior:

An output like this:

{
  "data": {
    "greeting": {
      "id": "/api/greetings/cd",
      "_id": "cd",
      "name": "cd"
    }
  }
}

Entity/Greeting.php

Notice: ApiResource has totaly valid DataPersister and DataProvider defined.

<?php
// Entity/Greeting.php

namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GraphQl\DeleteMutation;
use ApiPlatform\Metadata\GraphQl\Mutation;
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\GraphQl\QueryCollection;
use App\State\DataProvider;
use App\State\DataPersister;
use Symfony\Component\Validator\Constraints as Assert;

#[ApiResource(
    processor: DataPersister::class,
    provider: DataProvider::class,
    graphQlOperations: [
        new Query(),
        new QueryCollection(),
        new DeleteMutation(
            name: 'delete'
        )
    ]
)]
class Greeting
{
    private ?string $id = null;

    public string $name = '';

    public function __construct($id)
    {
        $this->id = $id;
        $this->name = $id;
    }

    public function getId(): ?string
    {
        return $this->id;
    }
}

State/DataProvider.php

<?php
// State/DataProvider.php

namespace App\State;

use App\Entity\Greeting;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use ApiPlatform\State\Pagination\TraversablePaginator;
use ApiPlatform\Metadata\CollectionOperationInterface;

final class DataProvider implements ProviderInterface
{
    private $data;

    function __construct()
    {
        $this->data = [
            'ab' => new Greeting('ab'),
            'cd' => new Greeting('cd'),
        ];
    }

    public function provide(Operation $operation, array $uriVariables = [], array $context = []): iterable|Greeting|null
    {
        if ($operation instanceof CollectionOperationInterface) {
            return new TraversablePaginator(new \ArrayObject($this->data), 0, count($this->data), count($this->data));
        }

        return $this->data[$uriVariables['id']] ?? null;
    }
}

State/DataPersister.php

<?php
// State/DataPersister.php

namespace App\State;

use ApiPlatform\Metadata\DeleteOperationInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;

final class DataPersister implements ProcessorInterface
{
    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
    {
       return $data;
    }
}

Here's the repository reproducing mentioned bug: https://github.com/danaki/api-platform-grapql-delete-mutation-bug-demo

danaki commented 1 day ago

Update: it seems it has nothing to do with custom Provider/Persister, but with serialization after DeleteMutation(). Obvious normalizationContext and denormalizationContext defining groups has no effect.

#[ApiResource(
    processor: DataPersister::class,
    provider: DataProvider::class,
    normalizationContext: ['groups' => ['delete']],
    denormalizationContext: ['groups' => ['delete']],
    graphQlOperations: [
        new Query(),
        new QueryCollection(),
        new DeleteMutation(
            name: 'delete',
            //serialize: false
        )
    ]
)]
class Greeting
{
    #[Groups(['delete'])]
    private ?string $id = null;

    #[Groups(['delete'])]
    public string $name = '';

    public function __construct($id)
    {
        $this->id = $id;
        $this->name = $id;
    }

    public function getId(): ?string
    {
        return $this->id;
    }
}

fails with ""Cannot return null for non-nullable field \"Greeting.name\"".

serialize: false (a commented line) helps to avoid error but also gives no output. Issue title updated.

soyuka commented 1 day ago

don't you need to do something like https://github.com/api-platform/core/issues/6354 ?