summaryrefslogtreecommitdiff
path: root/doc/development
diff options
context:
space:
mode:
authorBob Van Landuyt <bob@vanlanduyt.co>2018-07-10 16:19:45 +0200
committerBob Van Landuyt <bob@vanlanduyt.co>2018-07-25 18:37:12 +0200
commit3bcb04f100f5e982379fbeae37a30a191581d1ef (patch)
treee01065b8a6728bcc75af16166baafcbddd1a6cf5 /doc/development
parent9fe58f5a23f2960f666c4d641b463c744138d29c (diff)
downloadgitlab-ce-3bcb04f100f5e982379fbeae37a30a191581d1ef.tar.gz
Add mutation toggling WIP state of merge requests
This is mainly the setup of mutations for GraphQL. Including authorization and basic return type-structure.
Diffstat (limited to 'doc/development')
-rw-r--r--doc/development/api_graphql_styleguide.md174
1 files changed, 174 insertions, 0 deletions
diff --git a/doc/development/api_graphql_styleguide.md b/doc/development/api_graphql_styleguide.md
index b4a2349844b..6c6e198a7c3 100644
--- a/doc/development/api_graphql_styleguide.md
+++ b/doc/development/api_graphql_styleguide.md
@@ -201,6 +201,148 @@ lot of dependant objects.
To limit the amount of queries performed, we can use `BatchLoader`.
+## Mutations
+
+Mutations are used to change any stored values, or to trigger
+actions. In the same way a GET-request should not modify data, we
+cannot modify data in a regular GraphQL-query. We can however in a
+mutation.
+
+### Fields
+
+In the most common situations, a mutation would return 2 fields:
+
+- The resource being modified
+- A list of errors explaining why the action could not be
+ performed. If the mutation succeeded, this list would be empty.
+
+By inheriting any new mutations from `Mutations::BaseMutation` the
+`errors` field is automatically added. A `clientMutationId` field is
+also added, this can be used by the client to identify the result of a
+single mutation when multiple are performed within a single request.
+
+### Building Mutations
+
+Mutations live in `app/graphql/mutations` ideally grouped per
+resources they are mutating, similar to our services. They should
+inherit `Mutations::BaseMutation`. The fields defined on the mutation
+will be returned as the result of the mutation.
+
+Always provide a consistent GraphQL-name to the mutation, this name is
+used to generate the input types and the field the mutation is mounted
+on. The name should look like `<Resource being modified><Mutation
+class name>`, for example the `Mutations::MergeRequests::SetWip`
+mutation has GraphQL name `MergeRequestSetWip`.
+
+Arguments required by the mutation can be defined as arguments
+required for a field. These will be wrapped up in an input type for
+the mutation. For example, the `Mutations::MergeRequests::SetWip`
+with GraphQL-name `MergeRequestSetWip` defines these arguments:
+
+```ruby
+argument :project_path, GraphQL::ID_TYPE,
+ required: true,
+ description: "The project the merge request to mutate is in"
+
+argument :iid, GraphQL::ID_TYPE,
+ required: true,
+ description: "The iid of the merge request to mutate"
+
+argument :wip,
+ GraphQL::BOOLEAN_TYPE,
+ required: false,
+ description: <<~DESC
+ Whether or not to set the merge request as a WIP.
+ If not passed, the value will be toggled.
+ DESC
+```
+
+This would automatically generate an input type called
+`MergeRequestSetWipInput` with the 3 arguments we specified and the
+`clientMutationId`.
+
+These arguments are then passed to the `resolve` method of a mutation
+as keyword arguments. From here, we can call the service that will
+modify the resource.
+
+The `resolve` method should then return a hash with the same field
+names as defined on the mutation and an `errors` array. For example,
+the `Mutations::MergeRequests::SetWip` defines a `merge_request`
+field:
+
+```ruby
+field :merge_request,
+ Types::MergeRequestType,
+ null: true,
+ description: "The merge request after mutation"
+```
+
+This means that the hash returned from `resolve` in this mutation
+should look like this:
+
+```ruby
+{
+ # The merge request modified, this will be wrapped in the type
+ # defined on the field
+ merge_request: merge_request,
+ # An array if strings if the mutation failed after authorization
+ errors: merge_request.errors.full_messages
+}
+```
+
+To make the mutation available it should be defined on the mutation
+type that lives in `graphql/types/mutation_types`. The
+`mount_mutation` helper method will define a field based on the
+GraphQL-name of the mutation:
+
+```ruby
+module Types
+ class MutationType < BaseObject
+ include Gitlab::Graphql::MountMutation
+
+ graphql_name "Mutation"
+
+ mount_mutation Mutations::MergeRequests::SetWip
+ end
+end
+```
+
+Will generate a field called `mergeRequestSetWip` that
+`Mutations::MergeRequests::SetWip` to be resolved.
+
+### Authorizing resources
+
+To authorize resources inside a mutation, we can include the
+`Gitlab::Graphql::Authorize::AuthorizeResource` concern in the
+mutation.
+
+This allows us to provide the required abilities on the mutation like
+this:
+
+```ruby
+module Mutations
+ module MergeRequests
+ class SetWip < Base
+ graphql_name 'MergeRequestSetWip'
+
+ authorize :update_merge_request
+ end
+ end
+end
+```
+
+We can then call `authorize!` in the `resolve` method, passing in the resource we
+want to validate the abilities for.
+
+Alternatively, we can add a `find_object` method that will load the
+object on the mutation. This would allow you to use the
+`authorized_find!` and `authorized_find!` helper methods.
+
+When a user is not allowed to perform the action, or an object is not
+found, we should raise a
+`Gitlab::Graphql::Errors::ResourceNotAvailable` error. Which will be
+correctly rendered to the clients.
+
## Testing
_full stack_ tests for a graphql query or mutation live in
@@ -212,3 +354,35 @@ be used to test if the query renders valid results.
Using the `GraphqlHelpers#all_graphql_fields_for`-helper, a query
including all available fields can be constructed. This makes it easy
to add a test rendering all possible fields for a query.
+
+To test GraphQL mutation requests, `GraphqlHelpers` provides 2
+helpers: `graphql_mutation` which takes the name of the mutation, and
+a hash with the input for the mutation. This will return a struct with
+a mutation query, and prepared variables.
+
+This struct can then be passed to the `post_graphql_mutation` helper,
+that will post the request with the correct params, like a GraphQL
+client would do.
+
+To access the response of a mutation, the `graphql_mutation_response`
+helper is available.
+
+Using these helpers, we can build specs like this:
+
+```ruby
+let(:mutation) do
+ graphql_mutation(
+ :merge_request_set_wip,
+ project_path: 'gitlab-org/gitlab-ce',
+ iid: '1',
+ wip: true
+ )
+end
+
+it 'returns a successfull response' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(graphql_mutation_response(:merge_request_set_wip)['errors']).to be_empty
+end
+```