2

I have an User entity that can add his/hers contacts from Google using GooglePeopleApi.

The API provides an array of contacts to the front-end as it is (Nextjs). The question is how to insert all those contacts in one single mutation.

I could, of course, have the front-end loop through the array and post the contacts one by one, but that is a bit silly.

It should be possible to create a type array with the Contact input and then, with that, set up a customArgsMutation. ( I've seen some examples of that from Hasura ).

Entity wise, it looks like this for now ( only relevant code ):

User.php

 ....
/**
 * @ORM\OneToMany(targetEntity="App\Entity\Contact", mappedBy="user")
 * @Groups({"put-contacts", "get-admin", "get-owner"})
 */
private $contacts;

Contact.php

/**
* @ApiResource(
*      attributes={"pagination_enabled"=false},
*      graphql={
*          "item_query"={
*              "normalization_context"={"groups"={"get-admin", "get-owner"}},
*          },
*          "collection_query"={
*              "normalization_context"={"groups"={"get-admin", "get-owner"}},
*          },
*          "delete"={"security"="is_granted('IS_AUTHENTICATED_FULLY') and object.getUser() == user"},
*          "create"={
*              "security"="is_granted('IS_AUTHENTICATED_FULLY')",
*              "denormalization_context"={"groups"={"post", "put"}},
*              "normalization_context"={"groups"={"get-owner", "get-admin"}},
*          },
*      }
* )
* @ORM\Entity(repositoryClass="App\Repository\ContactRepository")
*/
class Contact
{
/**
 * @ORM\Id()
 * @ORM\GeneratedValue()
 * @ORM\Column(type="integer")
 */
private $id;

/**
 * @ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="contacts")
 * @Groups({"post", "get-admin", "get-owner"})
 */
private $user;

/**
 * @ORM\Column(type="string", length=180)
 * @Groups({"post", "put", "get-admin", "get-owner"})
 */
private $email;

/**
 * @ORM\Column(type="string", length=180, nullable=true)
 * @Groups({"post", "put", "get-admin", "get-owner"})
 */
private $familyName;

/**
 * @ORM\Column(type="string", length=180, nullable=true)
 * @Groups({"post", "put", "get-admin", "get-owner"})
 */
private $givenName;

/**
 * @ORM\Column(type="string", length=180, nullable=true)
 * @Groups({"post", "put", "get-admin", "get-owner"})
 */
private $displayName;

From graphiql, the createContact input looks like this:

 user: String
 email: String!
 familyName: String
 givenName: String
 displayName: String
 clientMutationId: String
BernardA
  • 1,391
  • 19
  • 48

2 Answers2

0

You have a couple of options here, depending on the amount of concurrency you want:

1. These should be performed in Serial

The client can make a single HTTP request with multiple mutations as aliases:

mutation CreateUsers {
  user1: createUser({ //userInput1 }) { ...userFields }
  user2: createUser({ //userInput2 }) { ...userFields }
  user3: createUser({ //userInput3 }) { ...userFields }
}

fragment userFields on CreateUserPayload {
  firstName
  // etc
}

The response will look like this:

{
  "data": {
    "user1": {...},
    "user2": {...},
    "user3": {...},
  }
}

Pros:

  • If any single mutation fails, just that one will error out without special handling
  • Order is maintained. Because the API consumer is specifically labelling the mutations, they know which one has which results.

Cons:

  • By design, multiple mutations run in Serial, where the first must complete fully before the next one is started. Because of this it's going to be a bit slower.
  • The client has to add the fields themselves or use a fragment for each mutation (what I showed above)

2. These should be performed in Parallel

It is a common practice (as common as it can be, I guess) to create a "bulk mutation" that allows you to create multiple users.

mutation CreateUsers {
  createUsers([
    { //userInput1 },
    { //userInput2 },
    { //userInput3 }
  ]) {
    firstName
    // etc
  }
}

Pros:

  • Runs in parallel, so it's faster
  • The client doesn't have to do string interpolation to build the query. They just need the array of objects to pass in.

Cons:

  • You have to build the logic yourself to be able to return nulls vs errors on your own.
  • You have to build the logic to maintain order
  • The client has to build logic to find their results in the response array if they care about order.
Dan Crews
  • 3,067
  • 17
  • 20
  • Just to have this asked: is this supported by Api Platform? – Nico Haase Jan 09 '21 at 14:03
  • @Dan Crews I am afraid this is still not supported by API-Platform or rather Graphql. See this issue https://github.com/api-platform/core/issues/3258 – BernardA Jan 23 '21 at 21:09
0

This is how I did it:

Create a custom type:

class CustomType extends InputObjectType implements TypeInterface
{
    public function __construct()
    {
        $config = [
            'name' => 'CustomType',
            'fields' => [
                'id_1' => Type::nonNull(Type::id()),
                'id_2' => Type::nonNull(Type::id()),
                'value' => Type::string(),
            ],
        ];
        parent::__construct($config);
    }

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

Register CustomType in src/config/services.yaml

(...)
App\Type\Definition\CustomType:
    tags:
        - { name: api_platform.graphql.type }

Add custom mutation to entity, using CustomType argument as array:

/**
* @ApiResource(
* (...)
*     graphql={
*     "customMutation"={
*         "args"={
*             (...)
*             "customArgumentName"={"type"=[CustomType!]"}
*         }
*     }
*)

Then the new customMutation can be called with an array of arguments like this:

mutation myMutation($input: customMutationEntitynameInput!) {
    customMutationEntityname(input: $input) {
        otherEntity {
            id
        }
    }
}

And graphql variables:

{
    "input": {
        "customArgumentName": [
            {
                "id_1": 3,
                "id_2": 4
            },{
                "id_1": 4,
                "id_2": 4
            }
        ]
    }
}