[GraphQL] Implement namespaced mutations
Everyone can contribute. Help move this issue forward while earning points, leveling up and collecting rewards.
Problem
As all our mutations are top-level, as our GraphQL API grows, discoverability of mutations becomes more difficult.
To help improve discoverability we encourage mutations to follow the naming convention of <resource><action>
(i.e.: issueUpdate
), however, organising mutations by their resource as a namespace, may lead to an even more clear API design.
Namespacing options
GraphQL specification RFC
At time of writing, it there is an open RFC to add namespaces to the GraphQL spec, which has had some customer validation but has been described as not a huge/main roadmap item so has just been on the sideline so the timeframe of when this may become part of the specification is uncertain.
Benefits:
Official support, if merged this will be how clients would expect mutations (and other things) to be namespaced.
Downsides:
The RFC is open, with no timeframe for when it will be adopted. Any such change to the specification would need to have implementations in Apollo and ruby-graphql
before we could use it ourselves. In the meantime, our mutations will continue to grow as they are.
MutationType
Using Types within A method described in https://graphql-rules.com/rules/mutation-namespaces allows a form of namespacing to happen now.
We can add regular BaseObject
types to MutationType
to represent namespaces, which contain the mutations within it:
Click to see example backend implementation
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index ae5469ba5b2..b47af22ed14 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -1,11 +1,25 @@
# frozen_string_literal: true
module Types
+
+ class IssueMutationsType < BaseObject
+ graphql_name 'IssueMutations'
+
+ field :set_confidential, mutation: Mutations::Issues::SetConfidential
+ field :set_due_date, mutation: Mutations::Issues::SetDueDate
+ field :update, mutation: Mutations::Issues::Update
+ end
+
class MutationType < BaseObject
include Gitlab::Graphql::MountMutation
graphql_name 'Mutation'
+ field :issue, IssueMutationsType,
+ null: true,
+ description: "Issue mutations"
+ def issue; {} end
+
mount_mutation Mutations::Admin::SidekiqQueues::DeleteJobs
mount_mutation Mutations::AlertManagement::CreateAlertIssue
Click to see example query
mutation {
issue {
update(input: { projectPath: "my/project" iid: "1", title: "foo" }) {
issue {
title
}
errors
}
}
}
Click to see example response
{
"data": {
"issue": {
"update": {
"issue": {
"title": "foo"
},
"errors": []
}
}
}
}
MutationType
Using Types with parameters within Similar to just using types, but we can make the intermediate objects do more work by passing parameters to them, and allowing them to do the work of lookup and authorization (making running multiple mutations in serial more efficient).
A motivating example is weight and assignees of an issue:
Click to see example query
mutation($path: ID!, $iid: ID!, $weight: Int!, $assignees: [UserID!]) {
issue(projectPath: $path, iid: $iid) {
setWeight(weight: $weight) {
errors
}
addAssignees(assigneeIds: $assignees) {
errors
}
}
}
Click to see example response
{
"data": {
"issue": {
"setWeight": {
"errors": []
},
"addAssignees": {
"errors": []
}
}
}
}
In this example we would only need to fetch the note and authorize the user to update it once, rather than twice.
Benefits:
We could do this now.
If we do lookup of objects in the namespacing type, we can potentially combine multiple mutations with minimal overhead.
Downsides:
This approach is almost certain to be entirely incompatible with whatever official support for namespacing eventually lands. We would likely need to deprecate this approach with the official approach at some point in the future.
It may actually make our mutations less discoverable. Without the semantic benefit of a proper namespace
this method appears to make it harder to find/understand the mutation schema in GraphiQL, for example.