Коммит 810df4fb создал по автору Jan Provaznik's avatar Jan Provaznik
Просмотр файлов

Merge branch '62826-graphql-note-mutations' into 'master'

GraphQL mutations for managing Notes

See merge request gitlab-org/gitlab-ce!30210
владельцы 227d8b44 073c8b25
......@@ -66,6 +66,8 @@ def object_from_id(global_id)
if gid.model_class < ApplicationRecord
Gitlab::Graphql::Loaders::BatchModelLoader.new(gid.model_class, gid.model_id).find
elsif gid.model_class.respond_to?(:lazy_find)
gid.model_class.lazy_find(gid.model_id)
else
gid.find
end
......
# frozen_string_literal: true
module Mutations
module Notes
class Base < BaseMutation
include Gitlab::Graphql::Authorize::AuthorizeResource
field :note,
Types::Notes::NoteType,
null: true,
description: 'The note after mutation'
private
def find_object(id:)
GitlabSchema.object_from_id(id)
end
def check_object_is_noteable!(object)
unless object.is_a?(Noteable)
raise Gitlab::Graphql::Errors::ResourceNotAvailable,
'Cannot add notes to this resource'
end
end
def check_object_is_note!(object)
unless object.is_a?(Note)
raise Gitlab::Graphql::Errors::ResourceNotAvailable,
'Resource is not a note'
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module Notes
module Create
# This is a Base class for the Note creation Mutations and is not
# mounted as a GraphQL mutation itself.
class Base < Mutations::Notes::Base
authorize :create_note
argument :noteable_id,
GraphQL::ID_TYPE,
required: true,
description: 'The global id of the resource to add a note to'
argument :body,
GraphQL::STRING_TYPE,
required: true,
description: copy_field_description(Types::Notes::NoteType, :body)
private
def resolve(args)
noteable = authorized_find!(id: args[:noteable_id])
check_object_is_noteable!(noteable)
note = ::Notes::CreateService.new(
noteable.project,
current_user,
create_note_params(noteable, args)
).execute
{
note: (note if note.persisted?),
errors: errors_on_object(note)
}
end
def create_note_params(noteable, args)
{
noteable: noteable,
note: args[:body]
}
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module Notes
module Create
class DiffNote < Base
graphql_name 'CreateDiffNote'
argument :position,
Types::Notes::DiffPositionInputType,
required: true,
description: copy_field_description(Types::Notes::NoteType, :position)
private
def create_note_params(noteable, args)
super(noteable, args).merge({
type: 'DiffNote',
position: position(noteable, args)
})
end
def position(noteable, args)
position = args[:position].to_h
position[:position_type] = 'text'
position.merge!(position[:paths].to_h)
Gitlab::Diff::Position.new(position)
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module Notes
module Create
class ImageDiffNote < Base
graphql_name 'CreateImageDiffNote'
argument :position,
Types::Notes::DiffImagePositionInputType,
required: true,
description: copy_field_description(Types::Notes::NoteType, :position)
private
def create_note_params(noteable, args)
super(noteable, args).merge({
type: 'DiffNote',
position: position(noteable, args)
})
end
def position(noteable, args)
position = args[:position].to_h
position[:position_type] = 'image'
position.merge!(position[:paths].to_h)
Gitlab::Diff::Position.new(position)
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module Notes
module Create
class Note < Base
graphql_name 'CreateNote'
argument :discussion_id,
GraphQL::ID_TYPE,
required: false,
description: 'The global id of the discussion this note is in reply to'
private
def create_note_params(noteable, args)
discussion_id = nil
if args[:discussion_id]
discussion = GitlabSchema.object_from_id(args[:discussion_id])
authorize_discussion!(discussion)
discussion_id = discussion.id
end
super(noteable, args).merge({
in_reply_to_discussion_id: discussion_id
})
end
def authorize_discussion!(discussion)
unless Ability.allowed?(current_user, :read_note, discussion, scope: :user)
raise Gitlab::Graphql::Errors::ResourceNotAvailable,
"The discussion does not exist or you don't have permission to perform this action"
end
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module Notes
class Destroy < Base
graphql_name 'DestroyNote'
authorize :admin_note
argument :id,
GraphQL::ID_TYPE,
required: true,
description: 'The global id of the note to destroy'
def resolve(id:)
note = authorized_find!(id: id)
check_object_is_note!(note)
::Notes::DestroyService.new(note.project, current_user).execute(note)
{
errors: []
}
end
end
end
end
# frozen_string_literal: true
module Mutations
module Notes
class Update < Base
graphql_name 'UpdateNote'
authorize :admin_note
argument :id,
GraphQL::ID_TYPE,
required: true,
description: 'The global id of the note to update'
argument :body,
GraphQL::STRING_TYPE,
required: true,
description: copy_field_description(Types::Notes::NoteType, :body)
def resolve(args)
note = authorized_find!(id: args[:id])
check_object_is_note!(note)
note = ::Notes::UpdateService.new(
note.project,
current_user,
{ note: args[:body] }
).execute(note)
{
note: note.reset,
errors: errors_on_object(note)
}
end
end
end
end
......@@ -2,5 +2,6 @@
module Types
class BaseInputObject < GraphQL::Schema::InputObject
prepend Gitlab::Graphql::CopyFieldDescription
end
end
# frozen_string_literal: true
module Types
# rubocop: disable Graphql/AuthorizeTypes
class DiffPathsInputType < BaseInputObject
argument :old_path, GraphQL::STRING_TYPE, required: false,
description: 'The path of the file on the start sha'
argument :new_path, GraphQL::STRING_TYPE, required: false,
description: 'The path of the file on the head sha'
end
# rubocop: enable Graphql/AuthorizeTypes
end
# frozen_string_literal: true
module Types
# rubocop: disable Graphql/AuthorizeTypes
# Types that use DiffRefsType should have their own authorization
class DiffRefsType < BaseObject
graphql_name 'DiffRefs'
field :head_sha, GraphQL::STRING_TYPE, null: false, description: 'The sha of the head at the time the comment was made'
field :base_sha, GraphQL::STRING_TYPE, null: false, description: 'The merge base of the branch the comment was made on'
field :start_sha, GraphQL::STRING_TYPE, null: false, description: 'The sha of the branch being compared against'
end
# rubocop: enable Graphql/AuthorizeTypes
end
......@@ -23,6 +23,7 @@ class MergeRequestType < BaseObject
field :updated_at, Types::TimeType, null: false
field :source_project, Types::ProjectType, null: true
field :target_project, Types::ProjectType, null: false
field :diff_refs, Types::DiffRefsType, null: true
# Alias for target_project
field :project, Types::ProjectType, null: false
field :project_id, GraphQL::INT_TYPE, null: false, method: :target_project_id
......
......@@ -10,5 +10,10 @@ class MutationType < BaseObject
mount_mutation Mutations::AwardEmojis::Remove
mount_mutation Mutations::AwardEmojis::Toggle
mount_mutation Mutations::MergeRequests::SetWip, calls_gitaly: true
mount_mutation Mutations::Notes::Create::Note, calls_gitaly: true
mount_mutation Mutations::Notes::Create::DiffNote, calls_gitaly: true
mount_mutation Mutations::Notes::Create::ImageDiffNote, calls_gitaly: true
mount_mutation Mutations::Notes::Update
mount_mutation Mutations::Notes::Destroy
end
end
# frozen_string_literal: true
module Types
module Notes
# rubocop: disable Graphql/AuthorizeTypes
class DiffImagePositionInputType < DiffPositionBaseInputType
graphql_name 'DiffImagePositionInput'
argument :x, GraphQL::INT_TYPE, required: true,
description: copy_field_description(Types::Notes::DiffPositionType, :x)
argument :y, GraphQL::INT_TYPE, required: true,
description: copy_field_description(Types::Notes::DiffPositionType, :y)
argument :width, GraphQL::INT_TYPE, required: true,
description: copy_field_description(Types::Notes::DiffPositionType, :width)
argument :height, GraphQL::INT_TYPE, required: true,
description: copy_field_description(Types::Notes::DiffPositionType, :height)
end
# rubocop: enable Graphql/AuthorizeTypes
end
end
# frozen_string_literal: true
module Types
module Notes
# rubocop: disable Graphql/AuthorizeTypes
class DiffPositionBaseInputType < BaseInputObject
argument :head_sha, GraphQL::STRING_TYPE, required: true,
description: copy_field_description(Types::DiffRefsType, :head_sha)
argument :base_sha, GraphQL::STRING_TYPE, required: false,
description: copy_field_description(Types::DiffRefsType, :base_sha)
argument :start_sha, GraphQL::STRING_TYPE, required: true,
description: copy_field_description(Types::DiffRefsType, :start_sha)
argument :paths,
Types::DiffPathsInputType,
required: true,
description: 'The paths of the file that was changed. ' \
'Both of the properties of this input are optional, but at least one of them is required'
end
# rubocop: enable Graphql/AuthorizeTypes
end
end
# frozen_string_literal: true
module Types
module Notes
# rubocop: disable Graphql/AuthorizeTypes
class DiffPositionInputType < DiffPositionBaseInputType
graphql_name 'DiffPositionInput'
argument :old_line, GraphQL::INT_TYPE, required: false,
description: copy_field_description(Types::Notes::DiffPositionType, :old_line)
argument :new_line, GraphQL::INT_TYPE, required: true,
description: copy_field_description(Types::Notes::DiffPositionType, :new_line)
end
# rubocop: enable Graphql/AuthorizeTypes
end
end
......@@ -7,12 +7,7 @@ module Notes
class DiffPositionType < BaseObject
graphql_name 'DiffPosition'
field :head_sha, GraphQL::STRING_TYPE, null: false,
description: "The sha of the head at the time the comment was made"
field :base_sha, GraphQL::STRING_TYPE, null: true,
description: "The merge base of the branch the comment was made on"
field :start_sha, GraphQL::STRING_TYPE, null: false,
description: "The sha of the branch being compared against"
field :diff_refs, Types::DiffRefsType, null: false
field :file_path, GraphQL::STRING_TYPE, null: false,
description: "The path of the file that was changed"
......
......@@ -8,8 +8,16 @@ class DiscussionType < BaseObject
authorize :read_note
field :id, GraphQL::ID_TYPE, null: false
field :reply_id, GraphQL::ID_TYPE, null: false, description: 'The ID used to reply to this discussion'
field :created_at, Types::TimeType, null: false
field :notes, Types::Notes::NoteType.connection_type, null: false, description: "All notes in the discussion"
# The gem we use to generate Global IDs is hard-coded to work with
# `id` properties. To generate a GID for the `reply_id` property,
# we must use the ::Gitlab::GlobalId module.
def reply_id
::Gitlab::GlobalId.build(object, id: object.reply_id)
end
end
end
end
......@@ -38,6 +38,17 @@ def self.build_collection(notes, context_noteable = nil)
grouped_notes.values.map { |notes| build(notes, context_noteable) }
end
def self.lazy_find(discussion_id)
BatchLoader.for(discussion_id).batch do |discussion_ids, loader|
results = Note.where(discussion_id: discussion_ids).fresh.to_a.group_by(&:discussion_id)
results.each do |discussion_id, notes|
next if notes.empty?
loader.call(discussion_id, Discussion.build(notes))
end
end
end
# Returns an alphanumeric discussion ID based on `build_discussion_id`
def self.discussion_id(note)
Digest::SHA1.hexdigest(build_discussion_id(note).join("-"))
......
......@@ -158,6 +158,8 @@ def discussions(context_noteable = nil)
Discussion.build_collection(all.includes(:noteable).fresh, context_noteable)
end
# Note: Where possible consider using Discussion#lazy_find to return
# Discussions in order to benefit from having records batch loaded.
def find_discussion(discussion_id)
notes = where(discussion_id: discussion_id).fresh.to_a
......
Поддерживает Markdown
0% или .
You are about to add 0 people to the discussion. Proceed with caution.
Сначала завершите редактирование этого сообщения!
Пожалуйста, зарегистрируйтесь или чтобы прокомментировать