'How to cascade deletes in GraphQL?

I'm using GraphQL and want to delete an entity from a database and another entity which is assigned to the first one by relation.

Let's say I have three types in my schema:

  1. User
  2. Assignment
  3. Task

The logic is as follows: User is somehow related to a Task. This relation is expressed by "intermediate" object Assignment. (In Assignment I can set that User is a supervisor of a task or worker or really anything, it's not important.)

Now, I want to delete User and related Assignment (Task should not be deleted).

Can I do that while executing only one mutation query with only one parameter: User ID?

I was thinking of something like:

mutation deleteUser($userId: ID!){
  deleteUser(id: $userId){
    id
    assignment {
      id # use that id somehow below
    }
  } {
    deleteAssignment(id: id_from_above) {
    }
  }
}

Is something like that possible?



Solution 1:[1]

I think you will benefit from reading the Mutation section of the GraphQL spec.

If the operation is a mutation, the result of the operation is the result of executing the mutation’s top level selection set on the mutation root object type. This selection set should be executed serially.

The key point here is that mutations are:

  • considered to be a type of query, except they have side-effects
  • they are not done in whichever order is convenient or even concurrently - like queries - but done sequentially

To be specific about your question: if deleting a user needs to conceptually also delete that user's assignments, and deleting an assignment deletes all tasks, then the exposed mutation (i.e. "query") could be just:

mutation deleteUser($userId: ID!) {
  deleteUser(id: $userId)
}

and the associated deletions just happen, and don't need to return anything. If you did want to return things, you could add those things as available for viewing:

mutation deleteUser($userId: ID!) {
  deleteUser(id: $userId) {
    assignment {
      task
    }
  }
}

Or, if you want the deletion of assignments and tasks to be controlled by the client, so that "viewing" the sub-deletions (which triggers such actual deletions - these would be queries that mutate the data, so would be questionable from a design point of view):

mutation deleteUser($userId: ID!) {
  deleteUser(id: $userId) {
    deleteAssignment {
      deleteTask
    }
  }
}

Assume of course that you define this Mutation field's return-type appropriately to have these fields available, and that the underlying software acts accordingly with the above required behaviour.

Solution 2:[2]

If you simply wish to cascade the deletion, there's nothing for the client to do - no special query needed. Simply execute the appropriate logic in your mutation resolver.

E.g. if you have a service method for deleting users that the mutation resolver will end up calling (the example is in Java, but whatever language you have (you didn't mention), the logic is the same):

boolean deleteUser(String id) {
   // either do the assignment deletion yourself here (not good)
   // or set your database to cascade the deletions (preferable)
   dataBase.execute("DELETE FROM User WHERE id = :id");
   return true; //have to return *something*
}

The client does not need to care about this, they just tell your system to delete the user:

mutation deleteUser($userId: ID!){
  deleteUser(id: $userId)
}

If you want the client to be able to get something better than a boolean success flag, return that something (this of course means changing the schema accordingly):

String deleteUser(String id) {       
   dataBase.execute("DELETE FROM User WHERE id = :id");
   //return the e.g. the ID
   return id;
}

or

String deleteUser(String id) { 
   User user = dataBase.execute("SELECT FROM User WHERE id = :id");   
   dataBase.execute("DELETE FROM User WHERE id = :id");
   //return the whole deleted user
   return user;
}

The latter enables the client to query the result (these are sub-queries, not sub-mutations, there is no such thing as sub-mutations):

mutation deleteUser($userId: ID!){
  deleteUser(id: $userId) {
    id
    assignments {
      id
    }
  }
}

The thing to note is that, unlike the queries, the mutations can not be nested, but yes, you can send multiple top-level mutations (as in your example). Unfortunately, there is no way to use the results from the first as the input for the second. There are requests to introduce something this into the GraphQL spec, but it may or may not ever happen.

Meaning your example:

mutation deleteUser($userId: ID!) {
  deleteUser(id: $userId) {
    id
    assignment {
      id # use that id somehow below
    }
  } {

  deleteAssignment(id: id_from_above) {
    id
  }
}

is unfortunately not possible.

You'll have to do this either as two separate requests, or come up with a more elaborate approach. What you can do if you need to allow the client a deeper level of control is to accept a more complex input, not just an ID, e.g:

input DeleteUserInput {
  id: ID!
  deleteOwnAssignments: Boolean
  deleteManagedAssignments: Boolean
}

mutation deleteUser($input: DeleteUserInput!) {
  deleteUser(input: $input)
}

boolean deleteUser(String id, boolean deleteOwnAssignments, boolean deleteManagedAssignments) {
   if (deleteOwnAssignments) {
       dataBase.execute("DELETE FROM Assignment WHERE assigned_to = :id");
   }
   if (deleteManagedAssignments) {
       dataBase.execute("DELETE FROM Assignment WHERE manager_id = :id");
   }
   dataBase.execute("DELETE FROM User WHERE id = :id");
   return true; //or return whatever is appropriate
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2