Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Are mutation methods required to be on the top level?

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?

like image 487
timetowonder Avatar asked Jan 31 '19 11:01

timetowonder


People also ask

How the mutation fields are executed?

While query fields are executed in parallel, mutation fields run in series, one after the other. This means that if we send two updateAuthor mutations in one request, the first is guaranteed to finish before the second begins. This ensures that we don't end up with a race condition with ourselves.

What other types can be used in a GraphQL schema?

The GraphQL schema language supports the scalar types of String , Int , Float , Boolean , and ID , so you can use these directly in the schema you pass to buildSchema . By default, every type is nullable - it's legitimate to return null as any of the scalar types.


2 Answers

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.

like image 90
Daniel Rearden Avatar answered Oct 20 '22 13:10

Daniel Rearden


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

like image 34
nodkz Avatar answered Oct 20 '22 14:10

nodkz