diff options
author | Bob Van Landuyt <bob@vanlanduyt.co> | 2018-07-10 16:19:45 +0200 |
---|---|---|
committer | Bob Van Landuyt <bob@vanlanduyt.co> | 2018-07-25 18:37:12 +0200 |
commit | 3bcb04f100f5e982379fbeae37a30a191581d1ef (patch) | |
tree | e01065b8a6728bcc75af16166baafcbddd1a6cf5 /doc/development | |
parent | 9fe58f5a23f2960f666c4d641b463c744138d29c (diff) | |
download | gitlab-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.md | 174 |
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 +``` |