I have never understood why GraphQL mutations need to be "wrapped". You'll often see something like this demonstrated (assuming a mutation named addThing which requires a name):
mutation addThing($name: String!) {
addThing(name: $name) {
id
name
}
}
However, the outer "wrapper" could be named anything. For example, the following nonsense works equally well:
mutation nonsense($name: String!) {
addThing(name: $name) {
id
name
}
}
What is the reasoning for "wrapping" the mutations this way?
Related
I'm trying to find a way to achieve include multiple queries in the same query based on required arguments.
This might be confusing, so let's take a look at an example
If we have 2 resolvers both with required IDs, let's say:
post(id: ID!) {...}
user(id: ID!) {...}
And we want to fetch both within the same query/request, but we might or not have an ID like the user, something as:
query($postId: ID!, $userId: ID) {
post(id: $postId) { ... }
user(id: $userId) { ... }
}
Now if we look we see that $user: ID is not mandatory in the query, but it is in the resolver.
I've looked at #include and #skip directives although, I didn't find a way to convert the $userId to a boolean.
Also, I've tried to pass a boolean as shouldIncludeUser although, as expected, the GQL will complain that the variable $userId of type ID doesn't match the type ID!.
Any ideas ?
Is this even possible?
I have a mutation:
const createSomethingMutation = gql`
mutation($data: SomethingCreateInput!) {
createSomething(data: $data) {
something {
id
name
}
}
}
`;
How do I create many Somethings in one request? Do I need to create a new Mutation on my GraphQL server like this:
mutation {
addManySomethings(data: [SomethingCreateInput]): [Something]
}
Or is there a way to use the one existing createSomethingMutation from Apollo Client multiple times with different arguments in one request?
You can in fact do this using aliases, and separate variables for each alias:
const createSomethingMutation = gql`
mutation($dataA: SomethingCreateInput!) {
createA: createSomething(data: $dataA) {
something {
id
name
}
}
createB: createSomething(data: $dataB) {
something {
id
name
}
}
}
`;
You can see more examples of aliases in the spec.
Then you just need to provide a variables object with two properties -- dataA and dataB. Things can get pretty messy if you need the number of mutations to be dynamic, though. Generally, in cases like this it's probably easier (and more efficient) to just expose a single mutation to handle creating/updating one or more instances of a model.
If you're trying to reduce the number of network requests from the client to server, you could also look into query batching.
It's not possible so easily.
Because the mutation has one consistent name and graphql will not allow to have the same operation multiple times in one query. So for this to work Apollo would have to map the mutations into aliases and then even map the variables data into some unknown iterable form, which i highly doubt it does.
All docs and tutorials usually show simple examples of mutations that look like this:
extend type Mutation {
edit(postId: String): String
}
But this way the edit method has to be unique across all entities, which to me seems like not a very robust way to write things. I would like to describe mutation similar to how we describe Queries, something like this:
type PostMutation {
edit(postId: String): String
}
extend type Mutation {
post: PostMutation
}
This seems to be a valid schema (it compiles and I can see it reflected in the generated graph-i-ql docs). But I can't find a way to make resolvers work with this schema.
Is this a supported case for GraphQL?
It's possible but generally not a good idea because:
It breaks the spec. From section 6.3.1:
Because the resolution of fields other than top‐level mutation fields must always be side effect‐free and idempotent, the execution order must not affect the result, and hence the server has the freedom to execute the field entries in whatever order it deems optimal.
In other words, only fields on the mutation root type should have side effects like CRUD operations.
Having the mutations at the root makes sense conceptually. Whatever action you're doing (liking a post, verifying an email, submitting an order, etc.) doesn't rely on GraphQL having to resolve additional fields before the action is taken. This is unlike when you're actually querying data. For example, to get comments on a post, we may have to resolve a user field, then a posts field and then finally the comments field for each post. At each "level", the field's contents are dependent on the value the parent field resolved to. This normally is not the case with mutations.
Under the hood, mutations are resolved sequentially. This is contrary to normal field resolution which happens in parallel. That means, for example, the firstName and lastName of a User type are resolved at the same time. However, if your operation type is mutation, the root fields will all be resolved one at a time. So in a query like this:
mutation SomeOperationName {
createUser
editUser
deleteUser
}
Each mutation will happen one at a time, in the order that they appear in the document. However, this only works for the root and only when the operation is a mutation, so these three fields will resolve in parallel:
mutation SomeOperationName {
user {
create
edit
delete
}
}
If you still want to do it, despite the above, this is how you do it when using makeExecutableSchema, which is what Apollo uses under the hood:
const resolvers = {
Mutation: {
post: () => ({}), // return an empty object,
},
PostMutation: {
edit: () => editPost(),
},
// Other types here
}
Your schema defined PostMutation as an object type, so GraphQL is expecting that field to return an object. If you omit the resolver for post, it will return null, which means none of the resolvers for the returning type (PostMutation) will be fired. That also means, we can also write:
mutation {
post
}
which does nothing but is still a valid query. Which is yet another reason to avoid this sort of schema structure.
Absolutely disagree with Daniel!
This is an amazing approach which helps to frontenders fastly understand what operations have one or another resource/model. And do not list loooong lists of mutations.
Calling multiple mutations in one request is common antipattern. For such cases better to create one complex mutation.
But even if you need to do such operation with several mutations you may use aliases:
await graphql({
schema,
source: `
mutation {
op1: article { like(id: 1) }
op2: article { like(id: 2) }
op3: article { unlike(id: 3) }
op4: article { like(id: 4) }
}
`,
});
expect(serialResults).toEqual([
'like 1 executed with timeout 100ms',
'like 2 executed with timeout 100ms',
'unlike 3 executed with timeout 5ms',
'like 4 executed with timeout 100ms',
]);
See the following test case: https://github.com/nodkz/conf-talks/blob/master/articles/graphql/schema-design/tests/mutations-test.js
Methods like/unlike are async with timeouts and works sequentially
consider the following
const createBob1 = gql`
mutation createBob2($var: data) {
createBob3(var: $var) {
id
}
}
`
at three levels, we're naming the mutation:
createBob1 defines the graphql document
createBob2 defines the apollo mutation
createBob3 defines the graphql resolver function to call
(I think?)
Has anyone settled on a naming convention for how to keep all three names consistent? Do you name them all the same?
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:
User
Assignment
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?
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.
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
}