diff options
Diffstat (limited to 'app/graphql')
47 files changed, 958 insertions, 100 deletions
diff --git a/app/graphql/mutations/issues/base.rb b/app/graphql/mutations/issues/base.rb new file mode 100644 index 00000000000..b7fa234a50b --- /dev/null +++ b/app/graphql/mutations/issues/base.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Mutations + module Issues + class Base < BaseMutation + include Mutations::ResolvesProject + + argument :project_path, GraphQL::ID_TYPE, + required: true, + description: "The project the issue to mutate is in" + + argument :iid, GraphQL::STRING_TYPE, + required: true, + description: "The iid of the issue to mutate" + + field :issue, + Types::IssueType, + null: true, + description: "The issue after mutation" + + authorize :update_issue + + private + + def find_object(project_path:, iid:) + project = resolve_project(full_path: project_path) + resolver = Resolvers::IssuesResolver + .single.new(object: project, context: context) + + resolver.resolve(iid: iid) + end + end + end +end diff --git a/app/graphql/mutations/issues/set_confidential.rb b/app/graphql/mutations/issues/set_confidential.rb new file mode 100644 index 00000000000..0fff5518665 --- /dev/null +++ b/app/graphql/mutations/issues/set_confidential.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Mutations + module Issues + class SetConfidential < Base + graphql_name 'IssueSetConfidential' + + argument :confidential, + GraphQL::BOOLEAN_TYPE, + required: true, + description: 'Whether or not to set the issue as a confidential.' + + def resolve(project_path:, iid:, confidential:) + issue = authorized_find!(project_path: project_path, iid: iid) + project = issue.project + + ::Issues::UpdateService.new(project, current_user, confidential: confidential) + .execute(issue) + + { + issue: issue, + errors: issue.errors.full_messages + } + end + end + end +end diff --git a/app/graphql/mutations/issues/set_due_date.rb b/app/graphql/mutations/issues/set_due_date.rb new file mode 100644 index 00000000000..1855c6f053b --- /dev/null +++ b/app/graphql/mutations/issues/set_due_date.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Mutations + module Issues + class SetDueDate < Base + graphql_name 'IssueSetDueDate' + + argument :due_date, + Types::TimeType, + required: true, + description: 'The desired due date for the issue' + + def resolve(project_path:, iid:, due_date:) + issue = authorized_find!(project_path: project_path, iid: iid) + project = issue.project + + ::Issues::UpdateService.new(project, current_user, due_date: due_date) + .execute(issue) + + { + issue: issue, + errors: issue.errors.full_messages + } + end + end + end +end diff --git a/app/graphql/mutations/snippets/base.rb b/app/graphql/mutations/snippets/base.rb new file mode 100644 index 00000000000..9dc6d49774e --- /dev/null +++ b/app/graphql/mutations/snippets/base.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Mutations + module Snippets + class Base < BaseMutation + field :snippet, + Types::SnippetType, + null: true, + description: 'The snippet after mutation' + + private + + def find_object(id:) + GitlabSchema.object_from_id(id) + end + + def authorized_resource?(snippet) + Ability.allowed?(context[:current_user], ability_for(snippet), snippet) + end + + def ability_for(snippet) + "#{ability_name}_#{snippet.to_ability_name}".to_sym + end + + def ability_name + raise NotImplementedError + end + end + end +end diff --git a/app/graphql/mutations/snippets/create.rb b/app/graphql/mutations/snippets/create.rb new file mode 100644 index 00000000000..fe1f543ea1a --- /dev/null +++ b/app/graphql/mutations/snippets/create.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module Mutations + module Snippets + class Create < BaseMutation + include Mutations::ResolvesProject + + graphql_name 'CreateSnippet' + + field :snippet, + Types::SnippetType, + null: true, + description: 'The snippet after mutation' + + argument :title, GraphQL::STRING_TYPE, + required: true, + description: 'Title of the snippet' + + argument :file_name, GraphQL::STRING_TYPE, + required: false, + description: 'File name of the snippet' + + argument :content, GraphQL::STRING_TYPE, + required: true, + description: 'Content of the snippet' + + argument :description, GraphQL::STRING_TYPE, + required: false, + description: 'Description of the snippet' + + argument :visibility_level, Types::VisibilityLevelsEnum, + description: 'The visibility level of the snippet', + required: true + + argument :project_path, GraphQL::ID_TYPE, + required: false, + description: 'The project full path the snippet is associated with' + + def resolve(args) + project_path = args.delete(:project_path) + + if project_path.present? + project = find_project!(project_path: project_path) + elsif !can_create_personal_snippet? + raise_resource_not_avaiable_error! + end + + snippet = CreateSnippetService.new(project, + context[:current_user], + args).execute + + { + snippet: snippet.valid? ? snippet : nil, + errors: errors_on_object(snippet) + } + end + + private + + def find_project!(project_path:) + authorized_find!(full_path: project_path) + end + + def find_object(full_path:) + resolve_project(full_path: full_path) + end + + def authorized_resource?(project) + Ability.allowed?(context[:current_user], :create_project_snippet, project) + end + + def can_create_personal_snippet? + Ability.allowed?(context[:current_user], :create_personal_snippet) + end + end + end +end diff --git a/app/graphql/mutations/snippets/destroy.rb b/app/graphql/mutations/snippets/destroy.rb new file mode 100644 index 00000000000..115fcfd6488 --- /dev/null +++ b/app/graphql/mutations/snippets/destroy.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Mutations + module Snippets + class Destroy < Base + graphql_name 'DestroySnippet' + + ERROR_MSG = 'Error deleting the snippet' + + argument :id, + GraphQL::ID_TYPE, + required: true, + description: 'The global id of the snippet to destroy' + + def resolve(id:) + snippet = authorized_find!(id: id) + + result = snippet.destroy + errors = result ? [] : [ERROR_MSG] + + { + errors: errors + } + end + + private + + def ability_name + "admin" + end + end + end +end diff --git a/app/graphql/mutations/snippets/mark_as_spam.rb b/app/graphql/mutations/snippets/mark_as_spam.rb new file mode 100644 index 00000000000..260a9753f76 --- /dev/null +++ b/app/graphql/mutations/snippets/mark_as_spam.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Mutations + module Snippets + class MarkAsSpam < Base + graphql_name 'MarkAsSpamSnippet' + + argument :id, + GraphQL::ID_TYPE, + required: true, + description: 'The global id of the snippet to update' + + def resolve(id:) + snippet = authorized_find!(id: id) + + result = mark_as_spam(snippet) + errors = result ? [] : ['Error with Akismet. Please check the logs for more info.'] + + { + errors: errors + } + end + + private + + def mark_as_spam(snippet) + SpamService.new(snippet).mark_as_spam! + end + + def authorized_resource?(snippet) + super && snippet.submittable_as_spam_by?(context[:current_user]) + end + + def ability_name + "admin" + end + end + end +end diff --git a/app/graphql/mutations/snippets/update.rb b/app/graphql/mutations/snippets/update.rb new file mode 100644 index 00000000000..27c232bc7f8 --- /dev/null +++ b/app/graphql/mutations/snippets/update.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module Mutations + module Snippets + class Update < Base + graphql_name 'UpdateSnippet' + + argument :id, + GraphQL::ID_TYPE, + required: true, + description: 'The global id of the snippet to update' + + argument :title, GraphQL::STRING_TYPE, + required: false, + description: 'Title of the snippet' + + argument :file_name, GraphQL::STRING_TYPE, + required: false, + description: 'File name of the snippet' + + argument :content, GraphQL::STRING_TYPE, + required: false, + description: 'Content of the snippet' + + argument :description, GraphQL::STRING_TYPE, + required: false, + description: 'Description of the snippet' + + argument :visibility_level, Types::VisibilityLevelsEnum, + description: 'The visibility level of the snippet', + required: false + + def resolve(args) + snippet = authorized_find!(id: args.delete(:id)) + + result = UpdateSnippetService.new(snippet.project, + context[:current_user], + snippet, + args).execute + + { + snippet: result ? snippet : snippet.reset, + errors: errors_on_object(snippet) + } + end + + private + + def ability_name + "update" + end + end + end +end diff --git a/app/graphql/mutations/todos/base.rb b/app/graphql/mutations/todos/base.rb index b6c7b320be1..2a72019fbac 100644 --- a/app/graphql/mutations/todos/base.rb +++ b/app/graphql/mutations/todos/base.rb @@ -9,6 +9,12 @@ module Mutations GitlabSchema.object_from_id(id) end + def map_to_global_ids(ids) + return [] if ids.blank? + + ids.map { |id| to_global_id(id) } + end + def to_global_id(id) ::URI::GID.build(app: GlobalID.app, model_name: Todo.name, model_id: id, params: nil).to_s end diff --git a/app/graphql/mutations/todos/mark_all_done.rb b/app/graphql/mutations/todos/mark_all_done.rb new file mode 100644 index 00000000000..5694985717c --- /dev/null +++ b/app/graphql/mutations/todos/mark_all_done.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Mutations + module Todos + class MarkAllDone < ::Mutations::Todos::Base + graphql_name 'TodosMarkAllDone' + + authorize :update_user + + field :updated_ids, + [GraphQL::ID_TYPE], + null: false, + description: 'Ids of the updated todos' + + def resolve + authorize!(current_user) + + updated_ids = mark_all_todos_done + + { + updated_ids: map_to_global_ids(updated_ids), + errors: [] + } + end + + private + + def mark_all_todos_done + return [] unless current_user + + TodoService.new.mark_all_todos_as_done_by_user(current_user) + end + end + end +end diff --git a/app/graphql/mutations/todos/mark_done.rb b/app/graphql/mutations/todos/mark_done.rb index 5483708b5c6..d738e387c43 100644 --- a/app/graphql/mutations/todos/mark_done.rb +++ b/app/graphql/mutations/todos/mark_done.rb @@ -16,22 +16,21 @@ module Mutations null: false, description: 'The requested todo' - # rubocop: disable CodeReuse/ActiveRecord def resolve(id:) todo = authorized_find!(id: id) - mark_done(Todo.where(id: todo.id)) unless todo.done? + + mark_done(todo) { todo: todo.reset, errors: errors_on_object(todo) } end - # rubocop: enable CodeReuse/ActiveRecord private def mark_done(todo) - TodoService.new.mark_todos_as_done(todo, current_user) + TodoService.new.mark_todo_as_done(todo, current_user) end end end diff --git a/app/graphql/mutations/todos/restore.rb b/app/graphql/mutations/todos/restore.rb new file mode 100644 index 00000000000..c4597bd84a2 --- /dev/null +++ b/app/graphql/mutations/todos/restore.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Mutations + module Todos + class Restore < ::Mutations::Todos::Base + graphql_name 'TodoRestore' + + authorize :update_todo + + argument :id, + GraphQL::ID_TYPE, + required: true, + description: 'The global id of the todo to restore' + + field :todo, Types::TodoType, + null: false, + description: 'The requested todo' + + def resolve(id:) + todo = authorized_find!(id: id) + restore(todo.id) if todo.done? + + { + todo: todo.reset, + errors: errors_on_object(todo) + } + end + + private + + def restore(id) + TodoService.new.mark_todos_as_pending_by_ids([id], current_user) + end + end + end +end diff --git a/app/graphql/resolvers/base_resolver.rb b/app/graphql/resolvers/base_resolver.rb index 85d6b377934..62dcc41dd9c 100644 --- a/app/graphql/resolvers/base_resolver.rb +++ b/app/graphql/resolvers/base_resolver.rb @@ -2,6 +2,8 @@ module Resolvers class BaseResolver < GraphQL::Schema::Resolver + extend ::Gitlab::Utils::Override + def self.single @single ||= Class.new(self) do def resolve(**args) @@ -36,5 +38,13 @@ module Resolvers # complexity difference is minimal in this case. [args[:iid], args[:iids]].any? ? 0 : 0.01 end + + override :object + def object + super.tap do |obj| + # If the field this resolver is used in is wrapped in a presenter, go back to it's subject + break obj.subject if obj.is_a?(Gitlab::View::Presenter::Base) + end + end end end diff --git a/app/graphql/resolvers/concerns/resolves_snippets.rb b/app/graphql/resolvers/concerns/resolves_snippets.rb new file mode 100644 index 00000000000..483372bbf63 --- /dev/null +++ b/app/graphql/resolvers/concerns/resolves_snippets.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module ResolvesSnippets + extend ActiveSupport::Concern + + included do + type Types::SnippetType, null: false + + argument :ids, [GraphQL::ID_TYPE], + required: false, + description: 'Array of global snippet ids, e.g., "gid://gitlab/ProjectSnippet/1"' + + argument :visibility, Types::Snippets::VisibilityScopesEnum, + required: false, + description: 'The visibility of the snippet' + end + + def resolve(**args) + resolve_snippets(args) + end + + private + + def resolve_snippets(args) + SnippetsFinder.new(context[:current_user], snippet_finder_params(args)).execute + end + + def snippet_finder_params(args) + { + ids: resolve_ids(args[:ids]), + scope: args[:visibility] + }.merge(options_by_type(args[:type])) + end + + def resolve_ids(ids) + Array.wrap(ids).map { |id| resolve_gid(id, :id) } + end + + def resolve_gid(gid, argument) + return unless gid.present? + + GlobalID.parse(gid)&.model_id.tap do |id| + raise Gitlab::Graphql::Errors::ArgumentError, "Invalid global id format for param #{argument}" if id.nil? + end + end + + def options_by_type(type) + case type + when 'personal' + { only_personal: true } + when 'project' + { only_project: true } + else + {} + end + end +end diff --git a/app/graphql/resolvers/echo_resolver.rb b/app/graphql/resolvers/echo_resolver.rb index 2ce55544254..fe0b1893a23 100644 --- a/app/graphql/resolvers/echo_resolver.rb +++ b/app/graphql/resolvers/echo_resolver.rb @@ -2,9 +2,11 @@ module Resolvers class EchoResolver < BaseResolver - argument :text, GraphQL::STRING_TYPE, required: true # rubocop:disable Graphql/Descriptions description 'Testing endpoint to validate the API with' + argument :text, GraphQL::STRING_TYPE, required: true, + description: 'Text to echo back' + def resolve(**args) username = context[:current_user]&.username diff --git a/app/graphql/resolvers/error_tracking/sentry_detailed_error_resolver.rb b/app/graphql/resolvers/error_tracking/sentry_detailed_error_resolver.rb new file mode 100644 index 00000000000..63455ff3acb --- /dev/null +++ b/app/graphql/resolvers/error_tracking/sentry_detailed_error_resolver.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Resolvers + module ErrorTracking + class SentryDetailedErrorResolver < BaseResolver + argument :id, GraphQL::ID_TYPE, + required: true, + description: 'ID of the Sentry issue' + + def resolve(**args) + project = object + current_user = context[:current_user] + issue_id = GlobalID.parse(args[:id]).model_id + + # Get data from Sentry + response = ::ErrorTracking::IssueDetailsService.new( + project, + current_user, + { issue_id: issue_id } + ).execute + issue = response[:issue] + issue.gitlab_project = project if issue + + issue + end + end + end +end diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb index 1fbc61cd950..664e0955535 100644 --- a/app/graphql/resolvers/issues_resolver.rb +++ b/app/graphql/resolvers/issues_resolver.rb @@ -4,17 +4,17 @@ module Resolvers class IssuesResolver < BaseResolver argument :iid, GraphQL::STRING_TYPE, required: false, - description: 'The IID of the issue, e.g., "1"' + description: 'IID of the issue. For example, "1"' argument :iids, [GraphQL::STRING_TYPE], required: false, - description: 'The list of IIDs of issues, e.g., [1, 2]' + description: 'List of IIDs of issues. For example, [1, 2]' argument :state, Types::IssuableStateEnum, required: false, - description: 'Current state of Issue' + description: 'Current state of this issue' argument :label_name, GraphQL::STRING_TYPE.to_list_type, required: false, - description: 'Labels applied to the Issue' + description: 'Labels applied to this issue' argument :created_before, Types::TimeType, required: false, description: 'Issues created before this date' @@ -33,8 +33,9 @@ module Resolvers argument :closed_after, Types::TimeType, required: false, description: 'Issues closed after this date' - argument :search, GraphQL::STRING_TYPE, # rubocop:disable Graphql/Descriptions - required: false + argument :search, GraphQL::STRING_TYPE, + required: false, + description: 'Search query for finding issues by title or description' argument :sort, Types::IssueSortEnum, description: 'Sort issues by this criteria', required: false, @@ -53,6 +54,7 @@ module Resolvers # https://gitlab.com/gitlab-org/gitlab-foss/issues/54520 args[:project_id] = project.id args[:iids] ||= [args[:iid]].compact + args[:attempt_project_search_optimizations] = args[:search].present? IssuesFinder.new(context[:current_user], args).execute end diff --git a/app/graphql/resolvers/projects/snippets_resolver.rb b/app/graphql/resolvers/projects/snippets_resolver.rb new file mode 100644 index 00000000000..bf9aa45349f --- /dev/null +++ b/app/graphql/resolvers/projects/snippets_resolver.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Resolvers + module Projects + class SnippetsResolver < BaseResolver + include ResolvesSnippets + + alias_method :project, :object + + def resolve(**args) + return Snippet.none if project.nil? + + super + end + + private + + def snippet_finder_params(args) + super.merge(project: project) + end + end + end +end diff --git a/app/graphql/resolvers/snippets_resolver.rb b/app/graphql/resolvers/snippets_resolver.rb new file mode 100644 index 00000000000..530a288a25b --- /dev/null +++ b/app/graphql/resolvers/snippets_resolver.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Resolvers + class SnippetsResolver < BaseResolver + include ResolvesSnippets + + ERROR_MESSAGE = 'Filtering by both an author and a project is not supported' + + alias_method :user, :object + + argument :author_id, GraphQL::ID_TYPE, + required: false, + description: 'The ID of an author' + + argument :project_id, GraphQL::ID_TYPE, + required: false, + description: 'The ID of a project' + + argument :type, Types::Snippets::TypeEnum, + required: false, + description: 'The type of snippet' + + argument :explore, + GraphQL::BOOLEAN_TYPE, + required: false, + description: 'Explore personal snippets' + + def resolve(**args) + if args[:author_id].present? && args[:project_id].present? + raise Gitlab::Graphql::Errors::ArgumentError, ERROR_MESSAGE + end + + super + end + + private + + def snippet_finder_params(args) + super + .merge(author: resolve_gid(args[:author_id], :author), + project: resolve_gid(args[:project_id], :project), + explore: args[:explore]) + end + end +end diff --git a/app/graphql/resolvers/todo_resolver.rb b/app/graphql/resolvers/todo_resolver.rb index 38a4539f34a..cff65321dc0 100644 --- a/app/graphql/resolvers/todo_resolver.rb +++ b/app/graphql/resolvers/todo_resolver.rb @@ -38,53 +38,15 @@ module Resolvers private - # TODO: Support multiple queries for e.g. state and type on TodosFinder: - # - # https://gitlab.com/gitlab-org/gitlab/merge_requests/18487 - # https://gitlab.com/gitlab-org/gitlab/merge_requests/18518 - # - # As soon as these MR's are merged, we can refactor this to query by - # multiple contents. - # def todo_finder_params(args) { - state: first_state(args), - type: first_type(args), - group_id: first_group_id(args), - author_id: first_author_id(args), - action_id: first_action(args), - project_id: first_project(args) + state: args[:state], + type: args[:type], + group_id: args[:group_id], + author_id: args[:author_id], + action_id: args[:action], + project_id: args[:project_id] } end - - def first_project(args) - first_query_field(args, :project_id) - end - - def first_action(args) - first_query_field(args, :action) - end - - def first_author_id(args) - first_query_field(args, :author_id) - end - - def first_group_id(args) - first_query_field(args, :group_id) - end - - def first_state(args) - first_query_field(args, :state) - end - - def first_type(args) - first_query_field(args, :type) - end - - def first_query_field(query, field) - return unless query.key?(field) - - query[field].first if query[field].respond_to?(:first) - end end end diff --git a/app/graphql/resolvers/users/snippets_resolver.rb b/app/graphql/resolvers/users/snippets_resolver.rb new file mode 100644 index 00000000000..d757640b5ff --- /dev/null +++ b/app/graphql/resolvers/users/snippets_resolver.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Resolvers + module Users + class SnippetsResolver < BaseResolver + include ResolvesSnippets + + alias_method :user, :object + + argument :type, Types::Snippets::TypeEnum, + required: false, + description: 'The type of snippet' + + private + + def snippet_finder_params(args) + super.merge(author: user) + end + end + end +end diff --git a/app/graphql/types/diff_refs_type.rb b/app/graphql/types/diff_refs_type.rb index 33a5780cd68..03d080d784b 100644 --- a/app/graphql/types/diff_refs_type.rb +++ b/app/graphql/types/diff_refs_type.rb @@ -6,9 +6,12 @@ module Types 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' + field :head_sha, GraphQL::STRING_TYPE, null: false, + description: 'SHA of the HEAD at the time the comment was made' + field :base_sha, GraphQL::STRING_TYPE, null: false, + description: 'Merge base of the branch the comment was made on' + field :start_sha, GraphQL::STRING_TYPE, null: false, + description: 'SHA of the branch being compared against' end # rubocop: enable Graphql/AuthorizeTypes end diff --git a/app/graphql/types/error_tracking/sentry_detailed_error_type.rb b/app/graphql/types/error_tracking/sentry_detailed_error_type.rb new file mode 100644 index 00000000000..c680f387a9a --- /dev/null +++ b/app/graphql/types/error_tracking/sentry_detailed_error_type.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +module Types + module ErrorTracking + class SentryDetailedErrorType < ::Types::BaseObject + graphql_name 'SentryDetailedError' + + present_using SentryDetailedErrorPresenter + + authorize :read_sentry_issue + + field :id, GraphQL::ID_TYPE, + null: false, + description: "ID (global ID) of the error" + field :sentry_id, GraphQL::STRING_TYPE, + method: :id, + null: false, + description: "ID (Sentry ID) of the error" + field :title, GraphQL::STRING_TYPE, + null: false, + description: "Title of the error" + field :type, GraphQL::STRING_TYPE, + null: false, + description: "Type of the error" + field :user_count, GraphQL::INT_TYPE, + null: false, + description: "Count of users affected by the error" + field :count, GraphQL::INT_TYPE, + null: false, + description: "Count of occurrences" + field :first_seen, Types::TimeType, + null: false, + description: "Timestamp when the error was first seen" + field :last_seen, Types::TimeType, + null: false, + description: "Timestamp when the error was last seen" + field :message, GraphQL::STRING_TYPE, + null: true, + description: "Sentry metadata message of the error" + field :culprit, GraphQL::STRING_TYPE, + null: false, + description: "Culprit of the error" + field :external_url, GraphQL::STRING_TYPE, + null: false, + description: "External URL of the error" + field :sentry_project_id, GraphQL::ID_TYPE, + method: :project_id, + null: false, + description: "ID of the project (Sentry project)" + field :sentry_project_name, GraphQL::STRING_TYPE, + method: :project_name, + null: false, + description: "Name of the project affected by the error" + field :sentry_project_slug, GraphQL::STRING_TYPE, + method: :project_slug, + null: false, + description: "Slug of the project affected by the error" + field :short_id, GraphQL::STRING_TYPE, + null: false, + description: "Short ID (Sentry ID) of the error" + field :status, Types::ErrorTracking::SentryErrorStatusEnum, + null: false, + description: "Status of the error" + field :frequency, [Types::ErrorTracking::SentryErrorFrequencyType], + null: false, + description: "Last 24hr stats of the error" + field :first_release_last_commit, GraphQL::STRING_TYPE, + null: true, + description: "Commit the error was first seen" + field :last_release_last_commit, GraphQL::STRING_TYPE, + null: true, + description: "Commit the error was last seen" + field :first_release_short_version, GraphQL::STRING_TYPE, + null: true, + description: "Release version the error was first seen" + field :last_release_short_version, GraphQL::STRING_TYPE, + null: true, + description: "Release version the error was last seen" + + def first_seen + DateTime.parse(object.first_seen) + end + + def last_seen + DateTime.parse(object.last_seen) + end + + def project_id + Gitlab::GlobalId.build(model_name: 'Project', id: object.project_id).to_s + end + end + end +end diff --git a/app/graphql/types/error_tracking/sentry_error_frequency_type.rb b/app/graphql/types/error_tracking/sentry_error_frequency_type.rb new file mode 100644 index 00000000000..a44ca0684b6 --- /dev/null +++ b/app/graphql/types/error_tracking/sentry_error_frequency_type.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Types + module ErrorTracking + # rubocop: disable Graphql/AuthorizeTypes + class SentryErrorFrequencyType < ::Types::BaseObject + graphql_name 'SentryErrorFrequency' + + field :time, Types::TimeType, + null: false, + description: "Time the error frequency stats were recorded" + field :count, GraphQL::INT_TYPE, + null: false, + description: "Count of errors received since the previously recorded time" + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/graphql/types/error_tracking/sentry_error_status_enum.rb b/app/graphql/types/error_tracking/sentry_error_status_enum.rb new file mode 100644 index 00000000000..df68eef4f3c --- /dev/null +++ b/app/graphql/types/error_tracking/sentry_error_status_enum.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Types + module ErrorTracking + class SentryErrorStatusEnum < ::Types::BaseEnum + graphql_name 'SentryErrorStatus' + description 'State of a Sentry error' + + value 'RESOLVED', value: 'resolved', description: 'Error has been resolved' + value 'RESOLVED_IN_NEXT_RELEASE', value: 'resolvedInNextRelease', description: 'Error has been ignored until next release' + value 'UNRESOLVED', value: 'unresolved', description: 'Error is unresolved' + value 'IGNORED', value: 'ignored', description: 'Error has been ignored' + end + end +end diff --git a/app/graphql/types/issuable_sort_enum.rb b/app/graphql/types/issuable_sort_enum.rb index 932e90c2d22..9fb1249d582 100644 --- a/app/graphql/types/issuable_sort_enum.rb +++ b/app/graphql/types/issuable_sort_enum.rb @@ -1,10 +1,8 @@ # frozen_string_literal: true module Types - # rubocop: disable Graphql/AuthorizeTypes class IssuableSortEnum < SortEnum graphql_name 'IssuableSort' description 'Values for sorting issuables' end - # rubocop: enable Graphql/AuthorizeTypes end diff --git a/app/graphql/types/issue_sort_enum.rb b/app/graphql/types/issue_sort_enum.rb index 48ff5819286..c8d8f3ef079 100644 --- a/app/graphql/types/issue_sort_enum.rb +++ b/app/graphql/types/issue_sort_enum.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Types - # rubocop: disable Graphql/AuthorizeTypes class IssueSortEnum < IssuableSortEnum graphql_name 'IssueSort' description 'Values for sorting issues' @@ -10,5 +9,6 @@ module Types value 'DUE_DATE_DESC', 'Due date by descending order', value: 'due_date_desc' value 'RELATIVE_POSITION_ASC', 'Relative position by ascending order', value: 'relative_position_asc' end - # rubocop: enable Graphql/AuthorizeTypes end + +Types::IssueSortEnum.prepend_if_ee('::EE::Types::IssueSortEnum') diff --git a/app/graphql/types/issue_state_enum.rb b/app/graphql/types/issue_state_enum.rb index 70c34fbe491..6521407fc9d 100644 --- a/app/graphql/types/issue_state_enum.rb +++ b/app/graphql/types/issue_state_enum.rb @@ -1,11 +1,8 @@ # frozen_string_literal: true module Types - # rubocop: disable Graphql/AuthorizeTypes - # This is a BaseEnum through IssuableEnum, so it does not need authorization class IssueStateEnum < IssuableStateEnum graphql_name 'IssueState' description 'State of a GitLab issue' end - # rubocop: enable Graphql/AuthorizeTypes end diff --git a/app/graphql/types/label_type.rb b/app/graphql/types/label_type.rb index d0bcf2068b7..738a00ad616 100644 --- a/app/graphql/types/label_type.rb +++ b/app/graphql/types/label_type.rb @@ -9,7 +9,7 @@ module Types field :id, GraphQL::ID_TYPE, null: false, description: 'Label ID' field :description, GraphQL::STRING_TYPE, null: true, - description: 'Description of the label (markdown rendered as HTML for caching)' + description: 'Description of the label (Markdown rendered as HTML for caching)' markdown_field :description_html, null: true field :title, GraphQL::STRING_TYPE, null: false, description: 'Content of the label' diff --git a/app/graphql/types/merge_request_state_enum.rb b/app/graphql/types/merge_request_state_enum.rb index 37c890a3c8d..92f52726ab3 100644 --- a/app/graphql/types/merge_request_state_enum.rb +++ b/app/graphql/types/merge_request_state_enum.rb @@ -1,13 +1,10 @@ # frozen_string_literal: true module Types - # rubocop: disable Graphql/AuthorizeTypes - # This is a BaseEnum through IssuableEnum, so it does not need authorization class MergeRequestStateEnum < IssuableStateEnum graphql_name 'MergeRequestState' description 'State of a GitLab merge request' value 'merged' end - # rubocop: enable Graphql/AuthorizeTypes end diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index 278a95fe3ca..0da95b367d8 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -20,7 +20,7 @@ module Types description: 'Title of the merge request' markdown_field :title_html, null: true field :description, GraphQL::STRING_TYPE, null: true, - description: 'Description of the merge request (markdown rendered as HTML for caching)' + description: 'Description of the merge request (Markdown rendered as HTML for caching)' markdown_field :description_html, null: true field :state, MergeRequestStateEnum, null: false, description: 'State of the merge request' diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index b3c7c162bb3..0a9c0143945 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -9,6 +9,8 @@ module Types mount_mutation Mutations::AwardEmojis::Add mount_mutation Mutations::AwardEmojis::Remove mount_mutation Mutations::AwardEmojis::Toggle + mount_mutation Mutations::Issues::SetConfidential + mount_mutation Mutations::Issues::SetDueDate mount_mutation Mutations::MergeRequests::SetLabels mount_mutation Mutations::MergeRequests::SetLocked mount_mutation Mutations::MergeRequests::SetMilestone @@ -21,6 +23,12 @@ module Types mount_mutation Mutations::Notes::Update mount_mutation Mutations::Notes::Destroy mount_mutation Mutations::Todos::MarkDone + mount_mutation Mutations::Todos::Restore + mount_mutation Mutations::Todos::MarkAllDone + mount_mutation Mutations::Snippets::Destroy + mount_mutation Mutations::Snippets::Update + mount_mutation Mutations::Snippets::Create + mount_mutation Mutations::Snippets::MarkAsSpam end end diff --git a/app/graphql/types/notes/diff_position_type.rb b/app/graphql/types/notes/diff_position_type.rb index cab8c750dc0..654562da0a7 100644 --- a/app/graphql/types/notes/diff_position_type.rb +++ b/app/graphql/types/notes/diff_position_type.rb @@ -7,36 +7,38 @@ module Types class DiffPositionType < BaseObject graphql_name 'DiffPosition' - field :diff_refs, Types::DiffRefsType, null: false # rubocop:disable Graphql/Descriptions + field :diff_refs, Types::DiffRefsType, null: false, + description: 'Information about the branch, HEAD, and base at the time of commenting' field :file_path, GraphQL::STRING_TYPE, null: false, - description: "The path of the file that was changed" + description: 'Path of the file that was changed' field :old_path, GraphQL::STRING_TYPE, null: true, - description: "The path of the file on the start sha." + description: 'Path of the file on the start SHA' field :new_path, GraphQL::STRING_TYPE, null: true, - description: "The path of the file on the head sha." - field :position_type, Types::Notes::PositionTypeEnum, null: false # rubocop:disable Graphql/Descriptions + description: 'Path of the file on the HEAD SHA' + field :position_type, Types::Notes::PositionTypeEnum, null: false, + description: 'Type of file the position refers to' # Fields for text positions field :old_line, GraphQL::INT_TYPE, null: true, - description: "The line on start sha that was changed", + description: 'Line on start SHA that was changed', resolve: -> (position, _args, _ctx) { position.old_line if position.on_text? } field :new_line, GraphQL::INT_TYPE, null: true, - description: "The line on head sha that was changed", + description: 'Line on HEAD SHA that was changed', resolve: -> (position, _args, _ctx) { position.new_line if position.on_text? } # Fields for image positions field :x, GraphQL::INT_TYPE, null: true, - description: "The X postion on which the comment was made", + description: 'X position on which the comment was made', resolve: -> (position, _args, _ctx) { position.x if position.on_image? } field :y, GraphQL::INT_TYPE, null: true, - description: "The Y position on which the comment was made", + description: 'Y position on which the comment was made', resolve: -> (position, _args, _ctx) { position.y if position.on_image? } field :width, GraphQL::INT_TYPE, null: true, - description: "The total width of the image", + description: 'Total width of the image', resolve: -> (position, _args, _ctx) { position.width if position.on_image? } field :height, GraphQL::INT_TYPE, null: true, - description: "The total height of the image", + description: 'Total height of the image', resolve: -> (position, _args, _ctx) { position.height if position.on_image? } end # rubocop: enable Graphql/AuthorizeTypes diff --git a/app/graphql/types/notes/discussion_type.rb b/app/graphql/types/notes/discussion_type.rb index ab87f8280ac..74a233e9d26 100644 --- a/app/graphql/types/notes/discussion_type.rb +++ b/app/graphql/types/notes/discussion_type.rb @@ -7,10 +7,14 @@ module Types authorize :read_note - field :id, GraphQL::ID_TYPE, null: false # rubocop:disable Graphql/Descriptions - field :reply_id, GraphQL::ID_TYPE, null: false, description: 'The ID used to reply to this discussion' - field :created_at, Types::TimeType, null: false # rubocop:disable Graphql/Descriptions - field :notes, Types::Notes::NoteType.connection_type, null: false, description: "All notes in the discussion" + field :id, GraphQL::ID_TYPE, null: false, + description: "ID of this discussion" + field :reply_id, GraphQL::ID_TYPE, null: false, + description: 'ID used to reply to this discussion' + field :created_at, Types::TimeType, null: false, + description: "Timestamp of the discussion's creation" + 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, diff --git a/app/graphql/types/notes/note_type.rb b/app/graphql/types/notes/note_type.rb index 4edf6ed90f7..b60fc96bd03 100644 --- a/app/graphql/types/notes/note_type.rb +++ b/app/graphql/types/notes/note_type.rb @@ -9,40 +9,48 @@ module Types expose_permissions Types::PermissionTypes::Note - field :id, GraphQL::ID_TYPE, null: false # rubocop:disable Graphql/Descriptions + field :id, GraphQL::ID_TYPE, null: false, + description: 'ID of the note' field :project, Types::ProjectType, null: true, - description: "The project this note is associated to", + description: 'Project associated with the note', resolve: -> (note, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, note.project_id).find } field :author, Types::UserType, null: false, - description: "The user who wrote this note", + description: 'User who wrote this note', resolve: -> (note, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, note.author_id).find } field :resolved_by, Types::UserType, null: true, - description: "The user that resolved the discussion", + description: 'User that resolved the discussion', resolve: -> (note, _args, _context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, note.resolved_by_id).find } field :system, GraphQL::BOOLEAN_TYPE, null: false, - description: "Whether or not this note was created by the system or by a user" + description: 'Indicates whether this note was created by the system or by a user' field :body, GraphQL::STRING_TYPE, null: false, method: :note, - description: "The content note itself" + description: 'Content of the note' markdown_field :body_html, null: true, method: :note - field :created_at, Types::TimeType, null: false # rubocop:disable Graphql/Descriptions - field :updated_at, Types::TimeType, null: false # rubocop:disable Graphql/Descriptions - field :discussion, Types::Notes::DiscussionType, null: true, description: "The discussion this note is a part of" - field :resolvable, GraphQL::BOOLEAN_TYPE, null: false, method: :resolvable? # rubocop:disable Graphql/Descriptions - field :resolved_at, Types::TimeType, null: true, description: "The time the discussion was resolved" - field :position, Types::Notes::DiffPositionType, null: true, description: "The position of this note on a diff" + field :created_at, Types::TimeType, null: false, + description: 'Timestamp of the note creation' + field :updated_at, Types::TimeType, null: false, + description: "Timestamp of the note's last activity" + field :discussion, Types::Notes::DiscussionType, null: true, + description: 'The discussion this note is a part of' + field :resolvable, GraphQL::BOOLEAN_TYPE, null: false, + description: 'Indicates if this note can be resolved. That is, if it is a resolvable discussion or simply a standalone note', + method: :resolvable? + field :resolved_at, Types::TimeType, null: true, + description: "Timestamp of the note's resolution" + field :position, Types::Notes::DiffPositionType, null: true, + description: 'The position of this note on a diff' end end end diff --git a/app/graphql/types/notes/noteable_type.rb b/app/graphql/types/notes/noteable_type.rb index ab4a170b123..2ac66452841 100644 --- a/app/graphql/types/notes/noteable_type.rb +++ b/app/graphql/types/notes/noteable_type.rb @@ -15,6 +15,8 @@ module Types Types::IssueType when MergeRequest Types::MergeRequestType + when Snippet + Types::SnippetType else raise "Unknown GraphQL type for #{object}" end diff --git a/app/graphql/types/permission_types/project.rb b/app/graphql/types/permission_types/project.rb index 3a6ba371154..2879dbd2b5c 100644 --- a/app/graphql/types/permission_types/project.rb +++ b/app/graphql/types/permission_types/project.rb @@ -10,13 +10,19 @@ module Types :remove_pages, :read_project, :create_merge_request_in, :read_wiki, :read_project_member, :create_issue, :upload_file, :read_cycle_analytics, :download_code, :download_wiki_code, - :fork_project, :create_project_snippet, :read_commit_status, + :fork_project, :read_commit_status, :request_access, :create_pipeline, :create_pipeline_schedule, :create_merge_request_from, :create_wiki, :push_code, :create_deployment, :push_to_delete_protected_branch, :admin_wiki, :admin_project, :update_pages, :admin_remote_mirror, :create_label, :update_wiki, :destroy_wiki, :create_pages, :destroy_pages, :read_pages_content, :admin_operations + + permission_field :create_snippet + + def create_snippet + Ability.allowed?(context[:current_user], :create_project_snippet, object) + end end end end diff --git a/app/graphql/types/permission_types/snippet.rb b/app/graphql/types/permission_types/snippet.rb new file mode 100644 index 00000000000..0fc13c60983 --- /dev/null +++ b/app/graphql/types/permission_types/snippet.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Types + module PermissionTypes + class Snippet < BasePermissionType + graphql_name 'SnippetPermissions' + + abilities :create_note, :award_emoji + + permission_field :read_snippet, method: :can_read_snippet? + permission_field :update_snippet, method: :can_update_snippet? + permission_field :admin_snippet, method: :can_admin_snippet? + permission_field :report_snippet, method: :can_report_as_spam? + end + end +end diff --git a/app/graphql/types/permission_types/user.rb b/app/graphql/types/permission_types/user.rb new file mode 100644 index 00000000000..dba4de2dacc --- /dev/null +++ b/app/graphql/types/permission_types/user.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Types + module PermissionTypes + class User < BasePermissionType + graphql_name 'UserPermissions' + + permission_field :create_snippet + + def create_snippet + Ability.allowed?(context[:current_user], :create_personal_snippet) + end + end + end +end diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index 73255021119..bd80ad7ff74 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -145,5 +145,19 @@ module Types null: true, description: 'Build pipelines of the project', resolver: Resolvers::ProjectPipelinesResolver + + field :sentry_detailed_error, + Types::ErrorTracking::SentryDetailedErrorType, + null: true, + description: 'Detailed version of a Sentry error on the project', + resolver: Resolvers::ErrorTracking::SentryDetailedErrorResolver + + field :snippets, + Types::SnippetType.connection_type, + null: true, + description: 'Snippets of the project', + resolver: Resolvers::Projects::SnippetsResolver end end + +Types::ProjectType.prepend_if_ee('::EE::Types::ProjectType') diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index 996bf225976..199a6226c6d 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -29,6 +29,14 @@ module Types resolver: Resolvers::MetadataResolver, description: 'Metadata about GitLab' - field :echo, GraphQL::STRING_TYPE, null: false, resolver: Resolvers::EchoResolver # rubocop:disable Graphql/Descriptions + field :snippets, + Types::SnippetType.connection_type, + null: true, + resolver: Resolvers::SnippetsResolver, + description: 'Find Snippets visible to the current user' + + field :echo, GraphQL::STRING_TYPE, null: false, + description: 'Text to echo back', + resolver: Resolvers::EchoResolver end end diff --git a/app/graphql/types/root_storage_statistics_type.rb b/app/graphql/types/root_storage_statistics_type.rb index a7498ee0a2e..3c471df072d 100644 --- a/app/graphql/types/root_storage_statistics_type.rb +++ b/app/graphql/types/root_storage_statistics_type.rb @@ -7,7 +7,7 @@ module Types authorize :read_statistics field :storage_size, GraphQL::INT_TYPE, null: false, description: 'The total storage in bytes' - field :repository_size, GraphQL::INT_TYPE, null: false, description: 'The git repository size in bytes' + field :repository_size, GraphQL::INT_TYPE, null: false, description: 'The Git repository size in bytes' field :lfs_objects_size, GraphQL::INT_TYPE, null: false, description: 'The LFS objects size in bytes' field :build_artifacts_size, GraphQL::INT_TYPE, null: false, description: 'The CI artifacts size in bytes' field :packages_size, GraphQL::INT_TYPE, null: false, description: 'The packages size in bytes' diff --git a/app/graphql/types/snippet_type.rb b/app/graphql/types/snippet_type.rb new file mode 100644 index 00000000000..3f780528945 --- /dev/null +++ b/app/graphql/types/snippet_type.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +module Types + class SnippetType < BaseObject + graphql_name 'Snippet' + description 'Represents a snippet entry' + + implements(Types::Notes::NoteableType) + + present_using SnippetPresenter + + authorize :read_snippet + + expose_permissions Types::PermissionTypes::Snippet + + field :id, GraphQL::ID_TYPE, + description: 'Id of the snippet', + null: false + + field :title, GraphQL::STRING_TYPE, + description: 'Title of the snippet', + null: false + + field :project, Types::ProjectType, + description: 'The project the snippet is associated with', + null: true, + authorize: :read_project, + resolve: -> (snippet, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, snippet.project_id).find } + + field :author, Types::UserType, + description: 'The owner of the snippet', + null: false, + resolve: -> (snippet, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, snippet.author_id).find } + + field :file_name, GraphQL::STRING_TYPE, + description: 'File Name of the snippet', + null: true + + field :content, GraphQL::STRING_TYPE, + description: 'Content of the snippet', + null: false + + field :description, GraphQL::STRING_TYPE, + description: 'Description of the snippet', + null: true + + field :visibility_level, Types::VisibilityLevelsEnum, + description: 'Visibility Level of the snippet', + null: false + + field :created_at, Types::TimeType, + description: 'Timestamp this snippet was created', + null: false + + field :updated_at, Types::TimeType, + description: 'Timestamp this snippet was updated', + null: false + + field :web_url, type: GraphQL::STRING_TYPE, + description: 'Web URL of the snippet', + null: false + + field :raw_url, type: GraphQL::STRING_TYPE, + description: 'Raw URL of the snippet', + null: false + + markdown_field :description_html, null: true, method: :description + end +end diff --git a/app/graphql/types/snippets/type_enum.rb b/app/graphql/types/snippets/type_enum.rb new file mode 100644 index 00000000000..243f05359db --- /dev/null +++ b/app/graphql/types/snippets/type_enum.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Types + module Snippets + class TypeEnum < BaseEnum + value 'personal', value: 'personal' + value 'project', value: 'project' + end + end +end diff --git a/app/graphql/types/snippets/visibility_scopes_enum.rb b/app/graphql/types/snippets/visibility_scopes_enum.rb new file mode 100644 index 00000000000..5488e05b95d --- /dev/null +++ b/app/graphql/types/snippets/visibility_scopes_enum.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Types + module Snippets + class VisibilityScopesEnum < BaseEnum + value 'private', value: 'are_private' + value 'internal', value: 'are_internal' + value 'public', value: 'are_public' + end + end +end diff --git a/app/graphql/types/user_type.rb b/app/graphql/types/user_type.rb index b45c7893e75..3943c891335 100644 --- a/app/graphql/types/user_type.rb +++ b/app/graphql/types/user_type.rb @@ -8,6 +8,8 @@ module Types present_using UserPresenter + expose_permissions Types::PermissionTypes::User + field :name, GraphQL::STRING_TYPE, null: false, description: 'Human-readable name of the user' field :username, GraphQL::STRING_TYPE, null: false, @@ -19,5 +21,11 @@ module Types field :todos, Types::TodoType.connection_type, null: false, resolver: Resolvers::TodoResolver, description: 'Todos of the user' + + field :snippets, + Types::SnippetType.connection_type, + null: true, + description: 'Snippets authored by the user', + resolver: Resolvers::Users::SnippetsResolver end end diff --git a/app/graphql/types/visibility_levels_enum.rb b/app/graphql/types/visibility_levels_enum.rb new file mode 100644 index 00000000000..d5ace24455e --- /dev/null +++ b/app/graphql/types/visibility_levels_enum.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Types + class VisibilityLevelsEnum < BaseEnum + Gitlab::VisibilityLevel.string_options.each do |name, int_value| + value name.downcase, value: int_value + end + end +end |