diff options
Diffstat (limited to 'app/graphql')
94 files changed, 1655 insertions, 163 deletions
diff --git a/app/graphql/mutations/alert_management/alerts/set_assignees.rb b/app/graphql/mutations/alert_management/alerts/set_assignees.rb new file mode 100644 index 00000000000..1e0c9fdeeaf --- /dev/null +++ b/app/graphql/mutations/alert_management/alerts/set_assignees.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module Mutations + module AlertManagement + module Alerts + class SetAssignees < Base + graphql_name 'AlertSetAssignees' + + argument :assignee_usernames, + [GraphQL::STRING_TYPE], + required: true, + description: 'The usernames to assign to the alert. Replaces existing assignees by default.' + + argument :operation_mode, + Types::MutationOperationModeEnum, + required: false, + description: 'The operation to perform. Defaults to REPLACE.' + + def resolve(args) + alert = authorized_find!(project_path: args[:project_path], iid: args[:iid]) + result = set_assignees(alert, args[:assignee_usernames], args[:operation_mode]) + + prepare_response(result) + end + + private + + def set_assignees(alert, assignee_usernames, operation_mode) + operation_mode ||= Types::MutationOperationModeEnum.enum[:replace] + + original_assignees = alert.assignees + target_users = find_target_users(assignee_usernames) + + assignees = case Types::MutationOperationModeEnum.enum.key(operation_mode).to_sym + when :replace then target_users.uniq + when :append then (original_assignees + target_users).uniq + when :remove then (original_assignees - target_users) + end + + ::AlertManagement::Alerts::UpdateService.new(alert, current_user, assignees: assignees).execute + end + + def find_target_users(assignee_usernames) + UsersFinder.new(current_user, username: assignee_usernames).execute + end + + def prepare_response(result) + { + alert: result.payload[:alert], + errors: result.error? ? [result.message] : [] + } + end + end + end + end +end diff --git a/app/graphql/mutations/alert_management/base.rb b/app/graphql/mutations/alert_management/base.rb index ca2057d4845..7fcca63db51 100644 --- a/app/graphql/mutations/alert_management/base.rb +++ b/app/graphql/mutations/alert_management/base.rb @@ -3,7 +3,7 @@ module Mutations module AlertManagement class Base < BaseMutation - include Mutations::ResolvesProject + include ResolvesProject argument :project_path, GraphQL::ID_TYPE, required: true, @@ -32,7 +32,7 @@ module Mutations return unless project - resolver = Resolvers::AlertManagementAlertResolver.single.new(object: project, context: context, field: nil) + resolver = Resolvers::AlertManagement::AlertResolver.single.new(object: project, context: context, field: nil) resolver.resolve(iid: iid) end end diff --git a/app/graphql/mutations/alert_management/update_alert_status.rb b/app/graphql/mutations/alert_management/update_alert_status.rb index e73a591378a..d820124d26f 100644 --- a/app/graphql/mutations/alert_management/update_alert_status.rb +++ b/app/graphql/mutations/alert_management/update_alert_status.rb @@ -27,7 +27,7 @@ module Mutations def prepare_response(result) { alert: result.payload[:alert], - errors: result.error? ? [result.message] : [] + errors: result.errors } end end diff --git a/app/graphql/mutations/base_mutation.rb b/app/graphql/mutations/base_mutation.rb index 30510cfab50..33f3f33a440 100644 --- a/app/graphql/mutations/base_mutation.rb +++ b/app/graphql/mutations/base_mutation.rb @@ -9,7 +9,7 @@ module Mutations field :errors, [GraphQL::STRING_TYPE], null: false, - description: "Errors encountered during execution of the mutation." + description: 'Errors encountered during execution of the mutation.' def current_user context[:current_user] diff --git a/app/graphql/mutations/branches/create.rb b/app/graphql/mutations/branches/create.rb index 127d5447d0a..214fead2e80 100644 --- a/app/graphql/mutations/branches/create.rb +++ b/app/graphql/mutations/branches/create.rb @@ -3,7 +3,7 @@ module Mutations module Branches class Create < BaseMutation - include Mutations::ResolvesProject + include ResolvesProject graphql_name 'CreateBranch' diff --git a/app/graphql/mutations/commits/create.rb b/app/graphql/mutations/commits/create.rb new file mode 100644 index 00000000000..9ed1bb819c8 --- /dev/null +++ b/app/graphql/mutations/commits/create.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module Mutations + module Commits + class Create < BaseMutation + include ResolvesProject + + graphql_name 'CommitCreate' + + argument :project_path, GraphQL::ID_TYPE, + required: true, + description: 'Project full path the branch is associated with' + + argument :branch, GraphQL::STRING_TYPE, + required: true, + description: 'Name of the branch' + + argument :message, + GraphQL::STRING_TYPE, + required: true, + description: copy_field_description(Types::CommitType, :message) + + argument :actions, + [Types::CommitActionType], + required: true, + description: 'Array of action hashes to commit as a batch' + + field :commit, + Types::CommitType, + null: true, + description: 'The commit after mutation' + + authorize :push_code + + def resolve(project_path:, branch:, message:, actions:) + project = authorized_find!(full_path: project_path) + + attributes = { + commit_message: message, + branch_name: branch, + start_branch: branch, + actions: actions.map { |action| action.to_h } + } + + result = ::Files::MultiService.new(project, current_user, attributes).execute + + { + commit: (project.repository.commit(result[:result]) if result[:status] == :success), + errors: Array.wrap(result[:message]) + } + end + + private + + def find_object(full_path:) + resolve_project(full_path: full_path) + end + end + end +end diff --git a/app/graphql/mutations/concerns/mutations/resolves_issuable.rb b/app/graphql/mutations/concerns/mutations/resolves_issuable.rb index d63cc27a450..13a56f2e709 100644 --- a/app/graphql/mutations/concerns/mutations/resolves_issuable.rb +++ b/app/graphql/mutations/concerns/mutations/resolves_issuable.rb @@ -3,12 +3,22 @@ module Mutations module ResolvesIssuable extend ActiveSupport::Concern - include Mutations::ResolvesProject + + included do + include ResolvesProject + end def resolve_issuable(type:, parent_path:, iid:) parent = resolve_issuable_parent(type, parent_path) + key = type == :merge_request ? :iids : :iid + args = { key => iid.to_s } + + resolver = issuable_resolver(type, parent, context) + ready, early_return = resolver.ready?(**args) + + return early_return unless ready - issuable_resolver(type, parent, context).resolve(iid: iid.to_s) + resolver.resolve(**args) end private @@ -22,7 +32,7 @@ module Mutations def resolve_issuable_parent(type, parent_path) return unless type == :issue || type == :merge_request - resolve_project(full_path: parent_path) + resolve_project(full_path: parent_path) if parent_path.present? end end end diff --git a/app/graphql/mutations/concerns/mutations/resolves_project.rb b/app/graphql/mutations/concerns/mutations/resolves_project.rb deleted file mode 100644 index e223e3edd94..00000000000 --- a/app/graphql/mutations/concerns/mutations/resolves_project.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -module Mutations - module ResolvesProject - extend ActiveSupport::Concern - - def resolve_project(full_path:) - project_resolver.resolve(full_path: full_path) - end - - def project_resolver - Resolvers::ProjectResolver.new(object: nil, context: context, field: nil) - end - end -end diff --git a/app/graphql/mutations/container_expiration_policies/update.rb b/app/graphql/mutations/container_expiration_policies/update.rb new file mode 100644 index 00000000000..c210571c6ca --- /dev/null +++ b/app/graphql/mutations/container_expiration_policies/update.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module Mutations + module ContainerExpirationPolicies + class Update < Mutations::BaseMutation + include ResolvesProject + + graphql_name 'UpdateContainerExpirationPolicy' + + authorize :destroy_container_image + + argument :project_path, + GraphQL::ID_TYPE, + required: true, + description: 'The project path where the container expiration policy is located' + + argument :enabled, + GraphQL::BOOLEAN_TYPE, + required: false, + description: copy_field_description(Types::ContainerExpirationPolicyType, :enabled) + + argument :cadence, + Types::ContainerExpirationPolicyCadenceEnum, + required: false, + description: copy_field_description(Types::ContainerExpirationPolicyType, :cadence) + + argument :older_than, + Types::ContainerExpirationPolicyOlderThanEnum, + required: false, + description: copy_field_description(Types::ContainerExpirationPolicyType, :older_than) + + argument :keep_n, + Types::ContainerExpirationPolicyKeepEnum, + required: false, + description: copy_field_description(Types::ContainerExpirationPolicyType, :keep_n) + + field :container_expiration_policy, + Types::ContainerExpirationPolicyType, + null: true, + description: 'The container expiration policy after mutation' + + def resolve(project_path:, **args) + project = authorized_find!(full_path: project_path) + + result = ::ContainerExpirationPolicies::UpdateService + .new(container: project, current_user: current_user, params: args) + .execute + + { + container_expiration_policy: result.payload[:container_expiration_policy], + errors: result.errors + } + end + + private + + def find_object(full_path:) + resolve_project(full_path: full_path) + end + end + end +end diff --git a/app/graphql/mutations/discussions/toggle_resolve.rb b/app/graphql/mutations/discussions/toggle_resolve.rb new file mode 100644 index 00000000000..41fd22c6b55 --- /dev/null +++ b/app/graphql/mutations/discussions/toggle_resolve.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +module Mutations + module Discussions + class ToggleResolve < BaseMutation + graphql_name 'DiscussionToggleResolve' + + description 'Toggles the resolved state of a discussion' + + argument :id, + GraphQL::ID_TYPE, + required: true, + description: 'The global id of the discussion' + + argument :resolve, + GraphQL::BOOLEAN_TYPE, + required: true, + description: 'Will resolve the discussion when true, and unresolve the discussion when false' + + field :discussion, + Types::Notes::DiscussionType, + null: true, + description: 'The discussion after mutation' + + def resolve(id:, resolve:) + discussion = authorized_find_discussion!(id: id) + errors = [] + + begin + if resolve + resolve!(discussion) + else + unresolve!(discussion) + end + rescue ActiveRecord::RecordNotSaved + errors << "Discussion failed to be #{'un' unless resolve}resolved" + end + + { + discussion: discussion, + errors: errors + } + end + + private + + # `Discussion` permissions are checked through `Discussion#can_resolve?`, + # so we use this method of checking permissions rather than by defining + # an `authorize` permission and calling `authorized_find!`. + def authorized_find_discussion!(id:) + find_object(id: id).tap do |discussion| + raise_resource_not_available_error! unless discussion&.can_resolve?(current_user) + end + end + + def find_object(id:) + GitlabSchema.object_from_id(id, expected_type: ::Discussion) + end + + def resolve!(discussion) + ::Discussions::ResolveService.new( + discussion.project, + current_user, + one_or_more_discussions: discussion + ).execute + end + + def unresolve!(discussion) + discussion.unresolve! + end + end + end +end diff --git a/app/graphql/mutations/issues/set_confidential.rb b/app/graphql/mutations/issues/set_confidential.rb index 0fff5518665..75befddc261 100644 --- a/app/graphql/mutations/issues/set_confidential.rb +++ b/app/graphql/mutations/issues/set_confidential.rb @@ -19,7 +19,7 @@ module Mutations { issue: issue, - errors: issue.errors.full_messages + errors: errors_on_object(issue) } end end diff --git a/app/graphql/mutations/issues/set_due_date.rb b/app/graphql/mutations/issues/set_due_date.rb index 1855c6f053b..effd863c541 100644 --- a/app/graphql/mutations/issues/set_due_date.rb +++ b/app/graphql/mutations/issues/set_due_date.rb @@ -19,7 +19,7 @@ module Mutations { issue: issue, - errors: issue.errors.full_messages + errors: errors_on_object(issue) } end end diff --git a/app/graphql/mutations/issues/update.rb b/app/graphql/mutations/issues/update.rb index 3710144fff5..7f6d9b0f988 100644 --- a/app/graphql/mutations/issues/update.rb +++ b/app/graphql/mutations/issues/update.rb @@ -33,7 +33,7 @@ module Mutations { issue: issue, - errors: issue.errors.full_messages + errors: errors_on_object(issue) } end end diff --git a/app/graphql/mutations/jira_import/import_users.rb b/app/graphql/mutations/jira_import/import_users.rb new file mode 100644 index 00000000000..c7225e1a99c --- /dev/null +++ b/app/graphql/mutations/jira_import/import_users.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Mutations + module JiraImport + class ImportUsers < BaseMutation + include ResolvesProject + + graphql_name 'JiraImportUsers' + + field :jira_users, + [Types::JiraUserType], + null: true, + description: 'Users returned from Jira, matched by email and name if possible.' + + argument :project_path, GraphQL::ID_TYPE, + required: true, + description: 'The project to import the Jira users into' + argument :start_at, GraphQL::INT_TYPE, + required: false, + description: 'The index of the record the import should started at, default 0 (50 records returned)' + + def resolve(project_path:, start_at:) + project = authorized_find!(full_path: project_path) + + service_response = ::JiraImport::UsersImporter.new(context[:current_user], project, start_at).execute + + { + jira_users: service_response.payload, + errors: service_response.errors + } + end + + private + + def find_object(full_path:) + resolve_project(full_path: full_path) + end + + def authorized_resource?(project) + Ability.allowed?(context[:current_user], :admin_project, project) + end + end + end +end diff --git a/app/graphql/mutations/jira_import/start.rb b/app/graphql/mutations/jira_import/start.rb index 6b80c9f8ca4..3df26d33711 100644 --- a/app/graphql/mutations/jira_import/start.rb +++ b/app/graphql/mutations/jira_import/start.rb @@ -3,7 +3,7 @@ module Mutations module JiraImport class Start < BaseMutation - include Mutations::ResolvesProject + include ResolvesProject graphql_name 'JiraImportStart' @@ -23,29 +23,21 @@ module Mutations description: 'Project name of the importer Jira project' def resolve(project_path:, jira_project_key:) - project = find_project!(project_path: project_path) - - raise_resource_not_available_error! unless project + project = authorized_find!(full_path: project_path) service_response = ::JiraImport::StartImportService .new(context[:current_user], project, jira_project_key) .execute jira_import = service_response.success? ? service_response.payload[:import_data] : nil - errors = service_response.error? ? [service_response.message] : [] + { jira_import: jira_import, - errors: errors + errors: service_response.errors } end private - def find_project!(project_path:) - return unless project_path.present? - - authorized_find!(full_path: project_path) - end - def find_object(full_path:) resolve_project(full_path: full_path) end diff --git a/app/graphql/mutations/merge_requests/create.rb b/app/graphql/mutations/merge_requests/create.rb new file mode 100644 index 00000000000..e210987f259 --- /dev/null +++ b/app/graphql/mutations/merge_requests/create.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module Mutations + module MergeRequests + class Create < BaseMutation + include ResolvesProject + + graphql_name 'MergeRequestCreate' + + argument :project_path, GraphQL::ID_TYPE, + required: true, + description: 'Project full path the merge request is associated with' + + argument :title, GraphQL::STRING_TYPE, + required: true, + description: copy_field_description(Types::MergeRequestType, :title) + + argument :source_branch, GraphQL::STRING_TYPE, + required: true, + description: copy_field_description(Types::MergeRequestType, :source_branch) + + argument :target_branch, GraphQL::STRING_TYPE, + required: true, + description: copy_field_description(Types::MergeRequestType, :target_branch) + + argument :description, GraphQL::STRING_TYPE, + required: false, + description: copy_field_description(Types::MergeRequestType, :description) + + field :merge_request, + Types::MergeRequestType, + null: true, + description: 'The merge request after mutation' + + authorize :create_merge_request_from + + def resolve(project_path:, title:, source_branch:, target_branch:, description: nil) + project = authorized_find!(full_path: project_path) + + attributes = { + title: title, + source_branch: source_branch, + target_branch: target_branch, + author_id: current_user.id, + description: description + } + + merge_request = ::MergeRequests::CreateService.new(project, current_user, attributes).execute + + { + merge_request: merge_request.valid? ? merge_request : nil, + errors: errors_on_object(merge_request) + } + end + + private + + def find_object(full_path:) + resolve_project(full_path: full_path) + end + end + end +end diff --git a/app/graphql/mutations/merge_requests/set_assignees.rb b/app/graphql/mutations/merge_requests/set_assignees.rb index 8f0025f0a58..de244b62d0f 100644 --- a/app/graphql/mutations/merge_requests/set_assignees.rb +++ b/app/graphql/mutations/merge_requests/set_assignees.rb @@ -40,7 +40,7 @@ module Mutations { merge_request: merge_request, - errors: merge_request.errors.full_messages + errors: errors_on_object(merge_request) } end end diff --git a/app/graphql/mutations/merge_requests/set_labels.rb b/app/graphql/mutations/merge_requests/set_labels.rb index 71f7a353bc9..c1e45808593 100644 --- a/app/graphql/mutations/merge_requests/set_labels.rb +++ b/app/graphql/mutations/merge_requests/set_labels.rb @@ -24,8 +24,9 @@ module Mutations project = merge_request.project label_ids = label_ids + .map { |gid| GlobalID.parse(gid) } .select(&method(:label_descendant?)) - .map { |gid| GlobalID.parse(gid).model_id } # MergeRequests::UpdateService expects integers + .map(&:model_id) # MergeRequests::UpdateService expects integers attribute_name = case operation_mode when Types::MutationOperationModeEnum.enum[:append] @@ -41,12 +42,12 @@ module Mutations { merge_request: merge_request, - errors: merge_request.errors.full_messages + errors: errors_on_object(merge_request) } end def label_descendant?(gid) - GlobalID.parse(gid)&.model_class&.ancestors&.include?(Label) + gid&.model_class&.ancestors&.include?(Label) end end end diff --git a/app/graphql/mutations/merge_requests/set_locked.rb b/app/graphql/mutations/merge_requests/set_locked.rb index 09aaa0b39aa..c49d5186a03 100644 --- a/app/graphql/mutations/merge_requests/set_locked.rb +++ b/app/graphql/mutations/merge_requests/set_locked.rb @@ -21,7 +21,7 @@ module Mutations { merge_request: merge_request, - errors: merge_request.errors.full_messages + errors: errors_on_object(merge_request) } end end diff --git a/app/graphql/mutations/merge_requests/set_milestone.rb b/app/graphql/mutations/merge_requests/set_milestone.rb index 707d6677952..b3412dd9ed2 100644 --- a/app/graphql/mutations/merge_requests/set_milestone.rb +++ b/app/graphql/mutations/merge_requests/set_milestone.rb @@ -22,7 +22,7 @@ module Mutations { merge_request: merge_request, - errors: merge_request.errors.full_messages + errors: errors_on_object(merge_request) } end end diff --git a/app/graphql/mutations/merge_requests/set_subscription.rb b/app/graphql/mutations/merge_requests/set_subscription.rb index 86750152775..1535481ab37 100644 --- a/app/graphql/mutations/merge_requests/set_subscription.rb +++ b/app/graphql/mutations/merge_requests/set_subscription.rb @@ -18,7 +18,7 @@ module Mutations { merge_request: merge_request, - errors: merge_request.errors.full_messages + errors: errors_on_object(merge_request) } end end diff --git a/app/graphql/mutations/merge_requests/set_wip.rb b/app/graphql/mutations/merge_requests/set_wip.rb index a2aa0c84ee4..5d2077c12f2 100644 --- a/app/graphql/mutations/merge_requests/set_wip.rb +++ b/app/graphql/mutations/merge_requests/set_wip.rb @@ -21,7 +21,7 @@ module Mutations { merge_request: merge_request, - errors: merge_request.errors.full_messages + errors: errors_on_object(merge_request) } end diff --git a/app/graphql/mutations/metrics/dashboard/annotations/base.rb b/app/graphql/mutations/metrics/dashboard/annotations/base.rb new file mode 100644 index 00000000000..3126267da64 --- /dev/null +++ b/app/graphql/mutations/metrics/dashboard/annotations/base.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Mutations + module Metrics + module Dashboard + module Annotations + class Base < BaseMutation + private + + # This method is defined here in order to be used by `authorized_find!` in the subclasses. + def find_object(id:) + GitlabSchema.object_from_id(id) + end + end + end + end + end +end diff --git a/app/graphql/mutations/metrics/dashboard/annotations/delete.rb b/app/graphql/mutations/metrics/dashboard/annotations/delete.rb new file mode 100644 index 00000000000..fb828ba0e2f --- /dev/null +++ b/app/graphql/mutations/metrics/dashboard/annotations/delete.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Mutations + module Metrics + module Dashboard + module Annotations + class Delete < Base + graphql_name 'DeleteAnnotation' + + authorize :delete_metrics_dashboard_annotation + + argument :id, + GraphQL::ID_TYPE, + required: true, + description: 'The global id of the annotation to delete' + + def resolve(id:) + annotation = authorized_find!(id: id) + + result = ::Metrics::Dashboard::Annotations::DeleteService.new(context[:current_user], annotation).execute + + errors = Array.wrap(result[:message]) + + { + errors: errors + } + end + end + end + end + end +end diff --git a/app/graphql/mutations/snippets/create.rb b/app/graphql/mutations/snippets/create.rb index 6fc223fbee7..e1022358c09 100644 --- a/app/graphql/mutations/snippets/create.rb +++ b/app/graphql/mutations/snippets/create.rb @@ -3,7 +3,7 @@ module Mutations module Snippets class Create < BaseMutation - include Mutations::ResolvesProject + include ResolvesProject graphql_name 'CreateSnippet' @@ -60,7 +60,7 @@ module Mutations snippet = service_response.payload[:snippet] { - snippet: snippet.valid? ? snippet : nil, + snippet: service_response.success? ? snippet : nil, errors: errors_on_object(snippet) } end diff --git a/app/graphql/mutations/todos/mark_all_done.rb b/app/graphql/mutations/todos/mark_all_done.rb index 5694985717c..d30d1bcbcf0 100644 --- a/app/graphql/mutations/todos/mark_all_done.rb +++ b/app/graphql/mutations/todos/mark_all_done.rb @@ -28,7 +28,9 @@ module Mutations def mark_all_todos_done return [] unless current_user - TodoService.new.mark_all_todos_as_done_by_user(current_user) + todos = TodosFinder.new(current_user).execute + + TodoService.new.resolve_todos(todos, current_user, resolved_by_action: :api_all_done) end end end diff --git a/app/graphql/mutations/todos/mark_done.rb b/app/graphql/mutations/todos/mark_done.rb index d738e387c43..748e02d8782 100644 --- a/app/graphql/mutations/todos/mark_done.rb +++ b/app/graphql/mutations/todos/mark_done.rb @@ -30,7 +30,7 @@ module Mutations private def mark_done(todo) - TodoService.new.mark_todo_as_done(todo, current_user) + TodoService.new.resolve_todo(todo, current_user, resolved_by_action: :api_done) end end end diff --git a/app/graphql/mutations/todos/restore.rb b/app/graphql/mutations/todos/restore.rb index c4597bd84a2..a0a1772db0a 100644 --- a/app/graphql/mutations/todos/restore.rb +++ b/app/graphql/mutations/todos/restore.rb @@ -18,7 +18,7 @@ module Mutations def resolve(id:) todo = authorized_find!(id: id) - restore(todo.id) if todo.done? + restore(todo) { todo: todo.reset, @@ -28,8 +28,8 @@ module Mutations private - def restore(id) - TodoService.new.mark_todos_as_pending_by_ids([id], current_user) + def restore(todo) + TodoService.new.restore_todo(todo, current_user) end end end diff --git a/app/graphql/mutations/todos/restore_many.rb b/app/graphql/mutations/todos/restore_many.rb index 8a6265207cd..e95651b232f 100644 --- a/app/graphql/mutations/todos/restore_many.rb +++ b/app/graphql/mutations/todos/restore_many.rb @@ -68,7 +68,7 @@ module Mutations end def restore(todos) - TodoService.new.mark_todos_as_pending(todos, current_user) + TodoService.new.restore_todos(todos, current_user) end end end diff --git a/app/graphql/resolvers/alert_management/alert_resolver.rb b/app/graphql/resolvers/alert_management/alert_resolver.rb new file mode 100644 index 00000000000..71a7615685a --- /dev/null +++ b/app/graphql/resolvers/alert_management/alert_resolver.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Resolvers + module AlertManagement + class AlertResolver < BaseResolver + include LooksAhead + + argument :iid, GraphQL::STRING_TYPE, + required: false, + description: 'IID of the alert. For example, "1"' + + argument :statuses, [Types::AlertManagement::StatusEnum], + as: :status, + required: false, + description: 'Alerts with the specified statues. For example, [TRIGGERED]' + + argument :sort, Types::AlertManagement::AlertSortEnum, + description: 'Sort alerts by this criteria', + required: false + + argument :search, GraphQL::STRING_TYPE, + description: 'Search criteria for filtering alerts. This will search on title, description, service, monitoring_tool.', + required: false + + type Types::AlertManagement::AlertType, null: true + + def resolve_with_lookahead(**args) + parent = object.respond_to?(:sync) ? object.sync : object + return ::AlertManagement::Alert.none if parent.nil? + + apply_lookahead(::AlertManagement::AlertsFinder.new(context[:current_user], parent, args).execute) + end + + def preloads + { + assignees: [:assignees], + notes: [:ordered_notes, { ordered_notes: [:system_note_metadata, :project, :noteable] }] + } + end + end + end +end diff --git a/app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb b/app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb index 7f4346632ca..a45de21002f 100644 --- a/app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb +++ b/app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb @@ -5,6 +5,10 @@ module Resolvers class AlertStatusCountsResolver < BaseResolver type Types::AlertManagement::AlertStatusCountsType, null: true + argument :search, GraphQL::STRING_TYPE, + description: 'Search criteria for filtering alerts. This will search on title, description, service, monitoring_tool.', + required: false + def resolve(**args) ::Gitlab::AlertManagement::AlertStatusCounts.new(context[:current_user], object, args) end diff --git a/app/graphql/resolvers/alert_management_alert_resolver.rb b/app/graphql/resolvers/alert_management_alert_resolver.rb deleted file mode 100644 index 51ebbb96476..00000000000 --- a/app/graphql/resolvers/alert_management_alert_resolver.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -module Resolvers - class AlertManagementAlertResolver < BaseResolver - argument :iid, GraphQL::STRING_TYPE, - required: false, - description: 'IID of the alert. For example, "1"' - - argument :statuses, [Types::AlertManagement::StatusEnum], - as: :status, - required: false, - description: 'Alerts with the specified statues. For example, [TRIGGERED]' - - argument :sort, Types::AlertManagement::AlertSortEnum, - description: 'Sort alerts by this criteria', - required: false - - argument :search, GraphQL::STRING_TYPE, - description: 'Search criteria for filtering alerts. This will search on title, description, service, monitoring_tool.', - required: false - - type Types::AlertManagement::AlertType, null: true - - def resolve(**args) - parent = object.respond_to?(:sync) ? object.sync : object - return ::AlertManagement::Alert.none if parent.nil? - - ::AlertManagement::AlertsFinder.new(context[:current_user], parent, args).execute - end - end -end diff --git a/app/graphql/resolvers/assigned_merge_requests_resolver.rb b/app/graphql/resolvers/assigned_merge_requests_resolver.rb new file mode 100644 index 00000000000..fa08b142a7e --- /dev/null +++ b/app/graphql/resolvers/assigned_merge_requests_resolver.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Resolvers + class AssignedMergeRequestsResolver < UserMergeRequestsResolver + def user_role + :assignee + end + end +end diff --git a/app/graphql/resolvers/authored_merge_requests_resolver.rb b/app/graphql/resolvers/authored_merge_requests_resolver.rb new file mode 100644 index 00000000000..e19bc9e8715 --- /dev/null +++ b/app/graphql/resolvers/authored_merge_requests_resolver.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Resolvers + class AuthoredMergeRequestsResolver < UserMergeRequestsResolver + def user_role + :author + end + end +end diff --git a/app/graphql/resolvers/base_resolver.rb b/app/graphql/resolvers/base_resolver.rb index cf0642930ad..7daff68c069 100644 --- a/app/graphql/resolvers/base_resolver.rb +++ b/app/graphql/resolvers/base_resolver.rb @@ -3,27 +3,33 @@ module Resolvers class BaseResolver < GraphQL::Schema::Resolver extend ::Gitlab::Utils::Override + include ::Gitlab::Utils::StrongMemoize def self.single @single ||= Class.new(self) do + def ready?(**args) + ready, early_return = super + [ready, select_result(early_return)] + end + def resolve(**args) - super.first + select_result(super) end def single? true end + + def select_result(results) + results&.first + end end end def self.last - @last ||= Class.new(self) do - def resolve(**args) - super.last - end - - def single? - true + @last ||= Class.new(self.single) do + def select_result(results) + results&.last end end end @@ -59,6 +65,17 @@ module Resolvers end end + def synchronized_object + strong_memoize(:synchronized_object) do + case object + when BatchLoader::GraphQL + object.sync + else + object + end + end + end + def single? false end diff --git a/app/graphql/resolvers/concerns/looks_ahead.rb b/app/graphql/resolvers/concerns/looks_ahead.rb new file mode 100644 index 00000000000..becc6debd33 --- /dev/null +++ b/app/graphql/resolvers/concerns/looks_ahead.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module LooksAhead + extend ActiveSupport::Concern + + FEATURE_FLAG = :graphql_lookahead_support + + included do + attr_accessor :lookahead + end + + def resolve(**args) + self.lookahead = args.delete(:lookahead) + + resolve_with_lookahead(**args) + end + + def apply_lookahead(query) + return query unless Feature.enabled?(FEATURE_FLAG) + + selection = node_selection + + includes = preloads.each.flat_map do |name, requirements| + selection&.selects?(name) ? requirements : [] + end + preloads = (unconditional_includes + includes).uniq + + return query if preloads.empty? + + query.preload(*preloads) # rubocop: disable CodeReuse/ActiveRecord + end + + private + + def unconditional_includes + [] + end + + def preloads + {} + end + + def node_selection + return unless lookahead + + if lookahead.selects?(:nodes) + lookahead.selection(:nodes) + elsif lookahead.selects?(:edges) + lookahead.selection(:edges).selection(:nodes) + end + end +end diff --git a/app/graphql/resolvers/concerns/resolves_merge_requests.rb b/app/graphql/resolvers/concerns/resolves_merge_requests.rb new file mode 100644 index 00000000000..a2140728a27 --- /dev/null +++ b/app/graphql/resolvers/concerns/resolves_merge_requests.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +# Mixin for resolving merge requests. All arguments must be in forms +# that `MergeRequestsFinder` can handle, so you may need to use aliasing. +module ResolvesMergeRequests + extend ActiveSupport::Concern + include LooksAhead + + included do + type Types::MergeRequestType, null: true + end + + def resolve_with_lookahead(**args) + args[:iids] = Array.wrap(args[:iids]) if args[:iids] + args.compact! + + if project && args.keys == [:iids] + batch_load_merge_requests(args[:iids]) + else + args[:project_id] ||= project + + apply_lookahead(MergeRequestsFinder.new(current_user, args).execute) + end.then(&(single? ? :first : :itself)) + end + + def ready?(**args) + return early_return if no_results_possible?(args) + + super + end + + def early_return + [false, single? ? nil : MergeRequest.none] + end + + private + + def batch_load_merge_requests(iids) + iids.map { |iid| batch_load(iid) }.select(&:itself) # .compact doesn't work on BatchLoader + end + + # rubocop: disable CodeReuse/ActiveRecord + def batch_load(iid) + BatchLoader::GraphQL.for(iid.to_s).batch(key: project) do |iids, loader, args| + query = args[:key].merge_requests.where(iid: iids) + + apply_lookahead(query).each do |mr| + loader.call(mr.iid.to_s, mr) + end + end + end + # rubocop: enable CodeReuse/ActiveRecord + + def unconditional_includes + [:target_project] + end + + def preloads + { + assignees: [:assignees], + labels: [:labels], + author: [:author], + milestone: [:milestone], + head_pipeline: [:merge_request_diff, { head_pipeline: [:merge_request] }] + } + end +end diff --git a/app/graphql/resolvers/concerns/resolves_project.rb b/app/graphql/resolvers/concerns/resolves_project.rb new file mode 100644 index 00000000000..3c5ce3dab01 --- /dev/null +++ b/app/graphql/resolvers/concerns/resolves_project.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module ResolvesProject + def resolve_project(full_path: nil, project_id: nil) + unless full_path.present? ^ project_id.present? + raise ::Gitlab::Graphql::Errors::ArgumentError, 'Incompatible arguments: projectId, projectPath.' + end + + if full_path.present? + ::Gitlab::Graphql::Loaders::FullPathModelLoader.new(Project, full_path).find + else + ::GitlabSchema.object_from_id(project_id, expected_type: Project) + end + end +end diff --git a/app/graphql/resolvers/full_path_resolver.rb b/app/graphql/resolvers/full_path_resolver.rb index 46d3360baae..cbb0bf998a6 100644 --- a/app/graphql/resolvers/full_path_resolver.rb +++ b/app/graphql/resolvers/full_path_resolver.rb @@ -11,12 +11,7 @@ module Resolvers end def model_by_full_path(model, full_path) - BatchLoader::GraphQL.for(full_path).batch(key: model) do |full_paths, loader, args| - # `with_route` avoids an N+1 calculating full_path - args[:key].where_full_path_in(full_paths).with_route.each do |model_instance| - loader.call(model_instance.full_path, model_instance) - end - end + ::Gitlab::Graphql::Loaders::FullPathModelLoader.new(model, full_path).find end end end diff --git a/app/graphql/resolvers/merge_request_resolver.rb b/app/graphql/resolvers/merge_request_resolver.rb new file mode 100644 index 00000000000..a47a128ea32 --- /dev/null +++ b/app/graphql/resolvers/merge_request_resolver.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Resolvers + class MergeRequestResolver < BaseResolver.single + include ResolvesMergeRequests + + alias_method :project, :synchronized_object + + argument :iid, GraphQL::STRING_TYPE, + required: true, + as: :iids, + description: 'IID of the merge request, for example `1`' + + def no_results_possible?(args) + project.nil? + end + end +end diff --git a/app/graphql/resolvers/merge_requests_resolver.rb b/app/graphql/resolvers/merge_requests_resolver.rb index 25121dce005..3aa52341eec 100644 --- a/app/graphql/resolvers/merge_requests_resolver.rb +++ b/app/graphql/resolvers/merge_requests_resolver.rb @@ -2,47 +2,43 @@ module Resolvers class MergeRequestsResolver < BaseResolver - argument :iid, GraphQL::STRING_TYPE, - required: false, - description: 'IID of the merge request, for example `1`' + include ResolvesMergeRequests + + alias_method :project, :synchronized_object argument :iids, [GraphQL::STRING_TYPE], required: false, description: 'Array of IIDs of merge requests, for example `[1, 2]`' - type Types::MergeRequestType, null: true + argument :source_branches, [GraphQL::STRING_TYPE], + required: false, + as: :source_branch, + description: 'Array of source branch names. All resolved merge requests will have one of these branches as their source.' - alias_method :project, :object + argument :target_branches, [GraphQL::STRING_TYPE], + required: false, + as: :target_branch, + description: 'Array of target branch names. All resolved merge requests will have one of these branches as their target.' - def resolve(**args) - project = object.respond_to?(:sync) ? object.sync : object - return MergeRequest.none if project.nil? + argument :state, ::Types::MergeRequestStateEnum, + required: false, + description: 'A merge request state. If provided, all resolved merge requests will have this state.' - args[:iids] ||= [args[:iid]].compact + argument :labels, [GraphQL::STRING_TYPE], + required: false, + as: :label_name, + description: 'Array of label names. All resolved merge requests will have all of these labels.' - if args[:iids].any? - batch_load_merge_requests(args[:iids]) - else - args[:project_id] = project.id - - MergeRequestsFinder.new(context[:current_user], args).execute - end + def self.single + ::Resolvers::MergeRequestResolver end - def batch_load_merge_requests(iids) - iids.map { |iid| batch_load(iid) }.select(&:itself) # .compact doesn't work on BatchLoader + def no_results_possible?(args) + project.nil? || some_argument_is_empty?(args) end - # rubocop: disable CodeReuse/ActiveRecord - def batch_load(iid) - BatchLoader::GraphQL.for(iid.to_s).batch(key: project) do |iids, loader, args| - arg_key = args[:key].respond_to?(:sync) ? args[:key].sync : args[:key] - - arg_key.merge_requests.where(iid: iids).each do |mr| - loader.call(mr.iid.to_s, mr) - end - end + def some_argument_is_empty?(args) + args.values.any? { |v| v.is_a?(Array) && v.empty? } end - # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/graphql/resolvers/project_members_resolver.rb b/app/graphql/resolvers/project_members_resolver.rb new file mode 100644 index 00000000000..3846531762e --- /dev/null +++ b/app/graphql/resolvers/project_members_resolver.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Resolvers + class ProjectMembersResolver < BaseResolver + argument :search, GraphQL::STRING_TYPE, + required: false, + description: 'Search query' + + type Types::ProjectMemberType, null: true + + alias_method :project, :object + + def resolve(**args) + return Member.none unless project.present? + + MembersFinder + .new(project, context[:current_user], params: args) + .execute + end + end +end diff --git a/app/graphql/resolvers/project_pipeline_resolver.rb b/app/graphql/resolvers/project_pipeline_resolver.rb new file mode 100644 index 00000000000..5bafe3dd140 --- /dev/null +++ b/app/graphql/resolvers/project_pipeline_resolver.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Resolvers + class ProjectPipelineResolver < BaseResolver + alias_method :project, :object + + argument :iid, GraphQL::ID_TYPE, + required: true, + description: 'IID of the Pipeline, e.g., "1"' + + def resolve(iid:) + BatchLoader::GraphQL.for(iid).batch(key: project) do |iids, loader, args| + args[:key].ci_pipelines.for_iid(iids).each { |pl| loader.call(pl.iid.to_s, pl) } + end + end + end +end diff --git a/app/graphql/resolvers/projects/jira_imports_resolver.rb b/app/graphql/resolvers/projects/jira_imports_resolver.rb index 25361c068d9..aa9b7139f38 100644 --- a/app/graphql/resolvers/projects/jira_imports_resolver.rb +++ b/app/graphql/resolvers/projects/jira_imports_resolver.rb @@ -14,8 +14,6 @@ module Resolvers end def authorized_resource?(project) - return false unless project.jira_issues_import_feature_flag_enabled? - context[:current_user].present? && Ability.allowed?(context[:current_user], :read_project, project) end end diff --git a/app/graphql/resolvers/projects/jira_projects_resolver.rb b/app/graphql/resolvers/projects/jira_projects_resolver.rb new file mode 100644 index 00000000000..a8c3768df41 --- /dev/null +++ b/app/graphql/resolvers/projects/jira_projects_resolver.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +module Resolvers + module Projects + class JiraProjectsResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + + argument :name, + GraphQL::STRING_TYPE, + required: false, + description: 'Project name or key' + + def resolve(name: nil, **args) + authorize!(project) + + response, start_cursor, end_cursor = jira_projects(name: name, **compute_pagination_params(args)) + end_cursor = nil if !!response.payload[:is_last] + + if response.success? + Gitlab::Graphql::ExternallyPaginatedArray.new(start_cursor, end_cursor, *response.payload[:projects]) + else + raise Gitlab::Graphql::Errors::BaseError, response.message + end + end + + def authorized_resource?(project) + Ability.allowed?(context[:current_user], :admin_project, project) + end + + private + + alias_method :jira_service, :object + + def project + jira_service&.project + end + + def compute_pagination_params(params) + after_cursor = Base64.decode64(params[:after].to_s) + before_cursor = Base64.decode64(params[:before].to_s) + + # differentiate between 0 cursor and nil or invalid cursor that decodes into zero. + after_index = after_cursor.to_i == 0 && after_cursor != "0" ? nil : after_cursor.to_i + before_index = before_cursor.to_i == 0 && before_cursor != "0" ? nil : before_cursor.to_i + + if after_index.present? && before_index.present? + if after_index >= before_index + { start_at: 0, limit: 0 } + else + { start_at: after_index + 1, limit: before_index - after_index - 1 } + end + elsif after_index.present? + { start_at: after_index + 1, limit: nil } + elsif before_index.present? + { start_at: 0, limit: before_index - 1 } + else + { start_at: 0, limit: nil } + end + end + + def jira_projects(name:, start_at:, limit:) + args = { query: name, start_at: start_at, limit: limit }.compact + + response = Jira::Requests::Projects.new(project.jira_service, args).execute + + return [response, nil, nil] if response.error? + + projects = response.payload[:projects] + start_cursor = start_at == 0 ? nil : Base64.encode64((start_at - 1).to_s) + end_cursor = Base64.encode64((start_at + projects.size - 1).to_s) + + [response, start_cursor, end_cursor] + end + end + end +end diff --git a/app/graphql/resolvers/user_merge_requests_resolver.rb b/app/graphql/resolvers/user_merge_requests_resolver.rb new file mode 100644 index 00000000000..b0d6e159f73 --- /dev/null +++ b/app/graphql/resolvers/user_merge_requests_resolver.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module Resolvers + class UserMergeRequestsResolver < MergeRequestsResolver + include ResolvesProject + + argument :project_path, GraphQL::STRING_TYPE, + required: false, + description: 'The full-path of the project the authored merge requests should be in. Incompatible with projectId.' + + argument :project_id, GraphQL::ID_TYPE, + required: false, + description: 'The global ID of the project the authored merge requests should be in. Incompatible with projectPath.' + + attr_reader :project + alias_method :user, :synchronized_object + + def ready?(project_id: nil, project_path: nil, **args) + return early_return unless can_read_profile? + + if project_id || project_path + load_project(project_path, project_id) + return early_return unless can_read_project? + elsif args[:iids].present? + raise ::Gitlab::Graphql::Errors::ArgumentError, + 'iids requires projectPath or projectId' + end + + super(**args) + end + + def resolve(**args) + prepare_args(args) + key = :"#{user_role}_id" + super(key => user.id, **args) + end + + def user_role + raise NotImplementedError + end + + private + + def can_read_profile? + Ability.allowed?(current_user, :read_user_profile, user) + end + + def can_read_project? + Ability.allowed?(current_user, :read_merge_request, project) + end + + def load_project(project_path, project_id) + @project = resolve_project(full_path: project_path, project_id: project_id) + @project = @project.sync if @project.respond_to?(:sync) + end + + def no_results_possible?(args) + some_argument_is_empty?(args) + end + + # These arguments are handled in load_project, and should not be passed to + # the finder directly. + def prepare_args(args) + args.delete(:project_id) + args.delete(:project_path) + end + end +end diff --git a/app/graphql/resolvers/user_resolver.rb b/app/graphql/resolvers/user_resolver.rb new file mode 100644 index 00000000000..a34cecba491 --- /dev/null +++ b/app/graphql/resolvers/user_resolver.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Resolvers + class UserResolver < BaseResolver + description 'Retrieve a single user' + + type Types::UserType, null: true + + argument :id, GraphQL::ID_TYPE, + required: false, + description: 'ID of the User' + + argument :username, GraphQL::STRING_TYPE, + required: false, + description: 'Username of the User' + + def ready?(id: nil, username: nil) + unless id.present? ^ username.present? + raise Gitlab::Graphql::Errors::ArgumentError, 'Provide either a single username or id' + end + + super + end + + def resolve(id: nil, username: nil) + if id + GitlabSchema.object_from_id(id, expected_type: User) + else + batch_load(username) + end + end + + private + + def batch_load(username) + BatchLoader::GraphQL.for(username).batch do |usernames, loader| + User.by_username(usernames).each do |user| + loader.call(user.username, user) + end + end + end + end +end diff --git a/app/graphql/resolvers/users_resolver.rb b/app/graphql/resolvers/users_resolver.rb new file mode 100644 index 00000000000..110a283b42e --- /dev/null +++ b/app/graphql/resolvers/users_resolver.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module Resolvers + class UsersResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + + description 'Find Users' + + argument :ids, [GraphQL::ID_TYPE], + required: false, + description: 'List of user Global IDs' + + argument :usernames, [GraphQL::STRING_TYPE], required: false, + description: 'List of usernames' + + argument :sort, Types::SortEnum, + description: 'Sort users by this criteria', + required: false, + default_value: 'created_desc' + + def resolve(ids: nil, usernames: nil, sort: nil) + authorize! + + ::UsersFinder.new(context[:current_user], finder_params(ids, usernames, sort)).execute + end + + def ready?(**args) + args = { ids: nil, usernames: nil }.merge!(args) + + return super if args.values.compact.blank? + + if args.values.all? + raise Gitlab::Graphql::Errors::ArgumentError, 'Provide either a list of usernames or ids' + end + + super + end + + def authorize! + Ability.allowed?(context[:current_user], :read_users_list) || raise_resource_not_available_error! + end + + private + + def finder_params(ids, usernames, sort) + params = {} + params[:sort] = sort if sort + params[:username] = usernames if usernames + params[:id] = parse_gids(ids) if ids + params + end + + def parse_gids(gids) + gids.map { |gid| GitlabSchema.parse_gid(gid, expected_type: ::User).model_id } + end + end +end diff --git a/app/graphql/types/access_level_enum.rb b/app/graphql/types/access_level_enum.rb new file mode 100644 index 00000000000..6754d3d28ce --- /dev/null +++ b/app/graphql/types/access_level_enum.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Types + class AccessLevelEnum < BaseEnum + graphql_name 'AccessLevelEnum' + description 'Access level to a resource' + + value 'NO_ACCESS', value: Gitlab::Access::NO_ACCESS + value 'GUEST', value: Gitlab::Access::GUEST + value 'REPORTER', value: Gitlab::Access::REPORTER + value 'DEVELOPER', value: Gitlab::Access::DEVELOPER + value 'MAINTAINER', value: Gitlab::Access::MAINTAINER + value 'OWNER', value: Gitlab::Access::OWNER + end +end diff --git a/app/graphql/types/access_level_type.rb b/app/graphql/types/access_level_type.rb new file mode 100644 index 00000000000..c7f915f5038 --- /dev/null +++ b/app/graphql/types/access_level_type.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true +# rubocop:disable Graphql/AuthorizeTypes + +module Types + class AccessLevelType < Types::BaseObject + graphql_name 'AccessLevel' + description 'Represents the access level of a relationship between a User and object that it is related to' + + field :integer_value, GraphQL::INT_TYPE, null: true, + description: 'Integer representation of access level', + method: :to_i + + field :string_value, Types::AccessLevelEnum, null: true, + description: 'String representation of access level', + method: :to_i + end +end diff --git a/app/graphql/types/alert_management/alert_sort_enum.rb b/app/graphql/types/alert_management/alert_sort_enum.rb index e6d38af8170..3faac9ce53c 100644 --- a/app/graphql/types/alert_management/alert_sort_enum.rb +++ b/app/graphql/types/alert_management/alert_sort_enum.rb @@ -6,16 +6,16 @@ module Types graphql_name 'AlertManagementAlertSort' description 'Values for sorting alerts' - value 'START_TIME_ASC', 'Start time by ascending order', value: :start_time_asc - value 'START_TIME_DESC', 'Start time by descending order', value: :start_time_desc - value 'END_TIME_ASC', 'End time by ascending order', value: :end_time_asc - value 'END_TIME_DESC', 'End time by descending order', value: :end_time_desc + value 'STARTED_AT_ASC', 'Start time by ascending order', value: :started_at_asc + value 'STARTED_AT_DESC', 'Start time by descending order', value: :started_at_desc + value 'ENDED_AT_ASC', 'End time by ascending order', value: :ended_at_asc + value 'ENDED_AT_DESC', 'End time by descending order', value: :ended_at_desc value 'CREATED_TIME_ASC', 'Created time by ascending order', value: :created_at_asc value 'CREATED_TIME_DESC', 'Created time by descending order', value: :created_at_desc value 'UPDATED_TIME_ASC', 'Created time by ascending order', value: :updated_at_asc value 'UPDATED_TIME_DESC', 'Created time by descending order', value: :updated_at_desc - value 'EVENTS_COUNT_ASC', 'Events count by ascending order', value: :events_count_asc - value 'EVENTS_COUNT_DESC', 'Events count by descending order', value: :events_count_desc + value 'EVENT_COUNT_ASC', 'Events count by ascending order', value: :event_count_asc + value 'EVENT_COUNT_DESC', 'Events count by descending order', value: :event_count_desc value 'SEVERITY_ASC', 'Severity by ascending order', value: :severity_asc value 'SEVERITY_DESC', 'Severity by descending order', value: :severity_desc value 'STATUS_ASC', 'Status by ascending order', value: :status_asc diff --git a/app/graphql/types/alert_management/alert_type.rb b/app/graphql/types/alert_management/alert_type.rb index a766fb3236d..8215ccb152c 100644 --- a/app/graphql/types/alert_management/alert_type.rb +++ b/app/graphql/types/alert_management/alert_type.rb @@ -6,6 +6,8 @@ module Types graphql_name 'AlertManagementAlert' description "Describes an alert from the project's Alert Management" + implements(Types::Notes::NoteableType) + authorize :read_alert_management_alert field :iid, @@ -83,6 +85,15 @@ module Types Types::TimeType, null: true, description: 'Timestamp the alert was last updated' + + field :assignees, + Types::UserType.connection_type, + null: true, + description: 'Assignees of the alert' + + def notes + object.ordered_notes + end end end end diff --git a/app/graphql/types/base_object.rb b/app/graphql/types/base_object.rb index dad16898ba6..70e665f8fc3 100644 --- a/app/graphql/types/base_object.rb +++ b/app/graphql/types/base_object.rb @@ -12,5 +12,9 @@ module Types def id GitlabSchema.id_from_object(object) end + + def current_user + context[:current_user] + end end end diff --git a/app/graphql/types/board_type.rb b/app/graphql/types/board_type.rb index c0be782ed1e..f5dc9e08427 100644 --- a/app/graphql/types/board_type.rb +++ b/app/graphql/types/board_type.rb @@ -15,7 +15,7 @@ module Types field :lists, Types::BoardListType.connection_type, null: true, - description: 'Lists of the project board', + description: 'Lists of the board', resolver: Resolvers::BoardListsResolver, extras: [:lookahead] end diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb index d77b2a2ba32..32050766e5b 100644 --- a/app/graphql/types/ci/pipeline_type.rb +++ b/app/graphql/types/ci/pipeline_type.rb @@ -42,3 +42,5 @@ module Types end end end + +Types::Ci::PipelineType.prepend_if_ee('::EE::Types::Ci::PipelineType') diff --git a/app/graphql/types/commit_action_mode_enum.rb b/app/graphql/types/commit_action_mode_enum.rb new file mode 100644 index 00000000000..77658a85b51 --- /dev/null +++ b/app/graphql/types/commit_action_mode_enum.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Types + class CommitActionModeEnum < BaseEnum + graphql_name 'CommitActionMode' + description 'Mode of a commit action' + + value 'CREATE', description: 'Create command', value: :create + value 'DELETE', description: 'Delete command', value: :delete + value 'MOVE', description: 'Move command', value: :move + value 'UPDATE', description: 'Update command', value: :update + value 'CHMOD', description: 'Chmod command', value: :chmod + end +end diff --git a/app/graphql/types/commit_action_type.rb b/app/graphql/types/commit_action_type.rb new file mode 100644 index 00000000000..7674abb11eb --- /dev/null +++ b/app/graphql/types/commit_action_type.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Types + # rubocop: disable Graphql/AuthorizeTypes + class CommitActionType < BaseInputObject + argument :action, type: Types::CommitActionModeEnum, required: true, + description: 'The action to perform, create, delete, move, update, chmod' + argument :file_path, type: GraphQL::STRING_TYPE, required: true, + description: 'Full path to the file' + argument :content, type: GraphQL::STRING_TYPE, required: false, + description: 'Content of the file' + argument :previous_path, type: GraphQL::STRING_TYPE, required: false, + description: 'Original full path to the file being moved' + argument :last_commit_id, type: GraphQL::STRING_TYPE, required: false, + description: 'Last known file commit ID' + argument :execute_filemode, type: GraphQL::BOOLEAN_TYPE, required: false, + description: 'Enables/disables the execute flag on the file' + argument :encoding, type: Types::CommitEncodingEnum, required: false, + description: 'Encoding of the file. Default is text' + end + # rubocop: enable Graphql/AuthorizeTypes +end diff --git a/app/graphql/types/commit_encoding_enum.rb b/app/graphql/types/commit_encoding_enum.rb new file mode 100644 index 00000000000..0ea89b82db7 --- /dev/null +++ b/app/graphql/types/commit_encoding_enum.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Types + class CommitEncodingEnum < BaseEnum + graphql_name 'CommitEncoding' + + value 'TEXT', description: 'Text encoding', value: :text + value 'BASE64', description: 'Base64 encoding', value: :base64 + end +end diff --git a/app/graphql/types/container_expiration_policy_cadence_enum.rb b/app/graphql/types/container_expiration_policy_cadence_enum.rb new file mode 100644 index 00000000000..bb8bdf2197b --- /dev/null +++ b/app/graphql/types/container_expiration_policy_cadence_enum.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Types + class ContainerExpirationPolicyCadenceEnum < BaseEnum + OPTIONS_MAPPING = { + '1d': 'EVERY_DAY', + '7d': 'EVERY_WEEK', + '14d': 'EVERY_TWO_WEEKS', + '1month': 'EVERY_MONTH', + '3month': 'EVERY_THREE_MONTHS' + }.freeze + + ::ContainerExpirationPolicy.cadence_options.each do |option, description| + value OPTIONS_MAPPING[option], description, value: option.to_s + end + end +end diff --git a/app/graphql/types/container_expiration_policy_keep_enum.rb b/app/graphql/types/container_expiration_policy_keep_enum.rb new file mode 100644 index 00000000000..7632df61092 --- /dev/null +++ b/app/graphql/types/container_expiration_policy_keep_enum.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Types + class ContainerExpirationPolicyKeepEnum < BaseEnum + OPTIONS_MAPPING = { + 1 => 'ONE_TAG', + 5 => 'FIVE_TAGS', + 10 => 'TEN_TAGS', + 25 => 'TWENTY_FIVE_TAGS', + 50 => 'FIFTY_TAGS', + 100 => 'ONE_HUNDRED_TAGS' + }.freeze + + ::ContainerExpirationPolicy.keep_n_options.each do |option, description| + value OPTIONS_MAPPING[option], description, value: option + end + end +end diff --git a/app/graphql/types/container_expiration_policy_older_than_enum.rb b/app/graphql/types/container_expiration_policy_older_than_enum.rb new file mode 100644 index 00000000000..da70534b0d7 --- /dev/null +++ b/app/graphql/types/container_expiration_policy_older_than_enum.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Types + class ContainerExpirationPolicyOlderThanEnum < BaseEnum + OPTIONS_MAPPING = { + '7d': 'SEVEN_DAYS', + '14d': 'FOURTEEN_DAYS', + '30d': 'THIRTY_DAYS', + '90d': 'NINETY_DAYS' + }.freeze + + ::ContainerExpirationPolicy.older_than_options.each do |option, description| + value OPTIONS_MAPPING[option], description, value: option.to_s + end + end +end diff --git a/app/graphql/types/container_expiration_policy_type.rb b/app/graphql/types/container_expiration_policy_type.rb new file mode 100644 index 00000000000..da53dbcbd39 --- /dev/null +++ b/app/graphql/types/container_expiration_policy_type.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Types + class ContainerExpirationPolicyType < BaseObject + graphql_name 'ContainerExpirationPolicy' + + description 'A tag expiration policy designed to keep only the images that matter most' + + authorize :destroy_container_image + + field :created_at, Types::TimeType, null: false, description: 'Timestamp of when the container expiration policy was created' + field :updated_at, Types::TimeType, null: false, description: 'Timestamp of when the container expiration policy was updated' + field :enabled, GraphQL::BOOLEAN_TYPE, null: false, description: 'Indicates whether this container expiration policy is enabled' + field :older_than, Types::ContainerExpirationPolicyOlderThanEnum, null: true, description: 'Tags older that this will expire' + field :cadence, Types::ContainerExpirationPolicyCadenceEnum, null: false, description: 'This container expiration policy schedule' + field :keep_n, Types::ContainerExpirationPolicyKeepEnum, null: true, description: 'Number of tags to retain' + field :name_regex, GraphQL::STRING_TYPE, null: true, description: 'Tags with names matching this regex pattern will expire' + field :name_regex_keep, GraphQL::STRING_TYPE, null: true, description: 'Tags with names matching this regex pattern will be preserved' + field :next_run_at, Types::TimeType, null: true, description: 'Next time that this container expiration policy will get executed' + end +end diff --git a/app/graphql/types/evidence_type.rb b/app/graphql/types/evidence_type.rb new file mode 100644 index 00000000000..a2fc9953c67 --- /dev/null +++ b/app/graphql/types/evidence_type.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Types + class EvidenceType < BaseObject + graphql_name 'ReleaseEvidence' + description 'Evidence for a release' + + authorize :download_code + + present_using Releases::EvidencePresenter + + field :id, GraphQL::ID_TYPE, null: false, + description: 'ID of the evidence' + field :sha, GraphQL::STRING_TYPE, null: true, + description: 'SHA1 ID of the evidence hash' + field :filepath, GraphQL::STRING_TYPE, null: true, + description: 'URL from where the evidence can be downloaded' + field :collected_at, Types::TimeType, null: true, + description: 'Timestamp when the evidence was collected' + end +end diff --git a/app/graphql/types/group_member_type.rb b/app/graphql/types/group_member_type.rb new file mode 100644 index 00000000000..ffffa3247db --- /dev/null +++ b/app/graphql/types/group_member_type.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Types + class GroupMemberType < BaseObject + expose_permissions Types::PermissionTypes::Group + authorize :read_group + + implements MemberInterface + + graphql_name 'GroupMember' + description 'Represents a Group Member' + + field :group, Types::GroupType, null: true, + description: 'Group that a User is a member of', + resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, obj.source_id).find } + end +end diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb index 20b4c66ba95..fd7d9a9ba3d 100644 --- a/app/graphql/types/group_type.rb +++ b/app/graphql/types/group_type.rb @@ -65,6 +65,45 @@ module Types null: true, description: 'A single board of the group', resolver: Resolvers::BoardsResolver.single + + field :label, + Types::LabelType, + null: true, + description: 'A label available on this group' do + argument :title, GraphQL::STRING_TYPE, + required: true, + description: 'Title of the label' + end + + def label(title:) + BatchLoader::GraphQL.for(title).batch(key: group) do |titles, loader, args| + LabelsFinder + .new(current_user, group: args[:key], title: titles) + .execute + .each { |label| loader.call(label.title, label) } + end + end + + field :labels, + Types::LabelType.connection_type, + null: true, + description: 'Labels available on this group' do + argument :search_term, GraphQL::STRING_TYPE, + required: false, + description: 'A search term to find labels with' + end + + def labels(search_term: nil) + LabelsFinder + .new(current_user, group: group, search: search_term) + .execute + end + + private + + def group + object.respond_to?(:sync) ? object.sync : object + end end end diff --git a/app/graphql/types/jira_import_type.rb b/app/graphql/types/jira_import_type.rb index 4a124566ffb..cf58a53b40d 100644 --- a/app/graphql/types/jira_import_type.rb +++ b/app/graphql/types/jira_import_type.rb @@ -15,6 +15,12 @@ module Types description: 'User that started the Jira import' field :jira_project_key, GraphQL::STRING_TYPE, null: false, description: 'Project key for the imported Jira project' + field :imported_issues_count, GraphQL::INT_TYPE, null: false, + description: 'Count of issues that were successfully imported' + field :failed_to_import_count, GraphQL::INT_TYPE, null: false, + description: 'Count of issues that failed to import' + field :total_issue_count, GraphQL::INT_TYPE, null: false, + description: 'Total count of issues that were attempted to import' end # rubocop: enable Graphql/AuthorizeTypes end diff --git a/app/graphql/types/jira_user_type.rb b/app/graphql/types/jira_user_type.rb new file mode 100644 index 00000000000..8aa21ce669b --- /dev/null +++ b/app/graphql/types/jira_user_type.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Types + # rubocop: disable Graphql/AuthorizeTypes + # Authorization is at project level for owners or admins on mutation level + class JiraUserType < BaseObject + graphql_name 'JiraUser' + + field :jira_account_id, GraphQL::STRING_TYPE, null: false, + description: 'Account id of the Jira user' + field :jira_display_name, GraphQL::STRING_TYPE, null: false, + description: 'Display name of the Jira user' + field :jira_email, GraphQL::STRING_TYPE, null: true, + description: 'Email of the Jira user, returned only for users with public emails' + field :gitlab_id, GraphQL::INT_TYPE, null: true, + description: 'Id of the matched GitLab user' + end + # rubocop: enable Graphql/AuthorizeTypes +end diff --git a/app/graphql/types/member_interface.rb b/app/graphql/types/member_interface.rb new file mode 100644 index 00000000000..976836221bc --- /dev/null +++ b/app/graphql/types/member_interface.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Types + module MemberInterface + include BaseInterface + + field :access_level, Types::AccessLevelType, null: true, + description: 'GitLab::Access level' + + field :created_by, Types::UserType, null: true, + description: 'User that authorized membership' + + field :created_at, Types::TimeType, null: true, + description: 'Date and time the membership was created' + + field :updated_at, Types::TimeType, null: true, + description: 'Date and time the membership was last updated' + + field :expires_at, Types::TimeType, null: true, + description: 'Date and time the membership expires' + end +end diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index cd4c6b4d46a..cb4ff7ea0c5 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -28,6 +28,8 @@ module Types description: 'Timestamp of when the merge request was created' field :updated_at, Types::TimeType, null: false, description: 'Timestamp of when the merge request was last updated' + field :merged_at, Types::TimeType, null: true, complexity: 5, + description: 'Timestamp of when the merge request was merged, null if not merged' field :source_project, Types::ProjectType, null: true, description: 'Source project of the merge request' field :target_project, Types::ProjectType, null: false, @@ -81,8 +83,14 @@ module Types description: 'Default merge commit message of the merge request' field :merge_ongoing, GraphQL::BOOLEAN_TYPE, method: :merge_ongoing?, null: false, description: 'Indicates if a merge is currently occurring' - field :source_branch_exists, GraphQL::BOOLEAN_TYPE, method: :source_branch_exists?, null: false, + field :source_branch_exists, GraphQL::BOOLEAN_TYPE, + null: false, calls_gitaly: true, + method: :source_branch_exists?, description: 'Indicates if the source branch of the merge request exists' + field :target_branch_exists, GraphQL::BOOLEAN_TYPE, + null: false, calls_gitaly: true, + method: :target_branch_exists?, + description: 'Indicates if the target branch of the merge request exists' field :mergeable_discussions_state, GraphQL::BOOLEAN_TYPE, null: true, description: 'Indicates if all discussions in the merge request have been resolved, allowing the merge request to be merged' field :web_url, GraphQL::STRING_TYPE, null: true, @@ -103,6 +111,8 @@ module Types resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Milestone, obj.milestone_id).find } field :assignees, Types::UserType.connection_type, null: true, complexity: 5, description: 'Assignees of the merge request' + field :author, Types::UserType, null: true, + description: 'User who created this merge request' field :participants, Types::UserType.connection_type, null: true, complexity: 5, description: 'Participants in the merge request' field :subscribed, GraphQL::BOOLEAN_TYPE, method: :subscribed?, null: false, complexity: 5, diff --git a/app/graphql/types/metrics/dashboard_type.rb b/app/graphql/types/metrics/dashboard_type.rb index d684533ff94..bbcce2d9596 100644 --- a/app/graphql/types/metrics/dashboard_type.rb +++ b/app/graphql/types/metrics/dashboard_type.rb @@ -10,6 +10,9 @@ module Types field :path, GraphQL::STRING_TYPE, null: true, description: 'Path to a file with the dashboard definition' + field :schema_validation_warnings, [GraphQL::STRING_TYPE], null: true, + description: 'Dashboard schema validation warnings' + field :annotations, Types::Metrics::Dashboards::AnnotationType.connection_type, null: true, description: 'Annotations added to the dashboard', resolver: Resolvers::Metrics::Dashboards::AnnotationResolver diff --git a/app/graphql/types/milestone_type.rb b/app/graphql/types/milestone_type.rb index 900f8c6f01d..99bd6e819d6 100644 --- a/app/graphql/types/milestone_type.rb +++ b/app/graphql/types/milestone_type.rb @@ -35,5 +35,17 @@ module Types field :updated_at, Types::TimeType, null: false, description: 'Timestamp of last milestone update' + + field :project_milestone, GraphQL::BOOLEAN_TYPE, null: false, + description: 'Indicates if milestone is at project level', + method: :project_milestone? + + field :group_milestone, GraphQL::BOOLEAN_TYPE, null: false, + description: 'Indicates if milestone is at group level', + method: :group_milestone? + + field :subgroup_milestone, GraphQL::BOOLEAN_TYPE, null: false, + description: 'Indicates if milestone is at subgroup level', + method: :subgroup_milestone? end end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index aeff84b83b8..8874c56dfdb 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -9,13 +9,17 @@ module Types mount_mutation Mutations::Admin::SidekiqQueues::DeleteJobs mount_mutation Mutations::AlertManagement::CreateAlertIssue mount_mutation Mutations::AlertManagement::UpdateAlertStatus + mount_mutation Mutations::AlertManagement::Alerts::SetAssignees mount_mutation Mutations::AwardEmojis::Add mount_mutation Mutations::AwardEmojis::Remove mount_mutation Mutations::AwardEmojis::Toggle mount_mutation Mutations::Branches::Create, calls_gitaly: true + mount_mutation Mutations::Commits::Create, calls_gitaly: true + mount_mutation Mutations::Discussions::ToggleResolve mount_mutation Mutations::Issues::SetConfidential mount_mutation Mutations::Issues::SetDueDate mount_mutation Mutations::Issues::Update + mount_mutation Mutations::MergeRequests::Create mount_mutation Mutations::MergeRequests::SetLabels mount_mutation Mutations::MergeRequests::SetLocked mount_mutation Mutations::MergeRequests::SetMilestone @@ -23,6 +27,7 @@ module Types mount_mutation Mutations::MergeRequests::SetWip, calls_gitaly: true mount_mutation Mutations::MergeRequests::SetAssignees mount_mutation Mutations::Metrics::Dashboard::Annotations::Create + mount_mutation Mutations::Metrics::Dashboard::Annotations::Delete 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 @@ -44,8 +49,10 @@ module Types mount_mutation Mutations::Snippets::Create mount_mutation Mutations::Snippets::MarkAsSpam mount_mutation Mutations::JiraImport::Start + mount_mutation Mutations::JiraImport::ImportUsers mount_mutation Mutations::DesignManagement::Upload, calls_gitaly: true mount_mutation Mutations::DesignManagement::Delete, calls_gitaly: true + mount_mutation Mutations::ContainerExpirationPolicies::Update end end diff --git a/app/graphql/types/notes/discussion_type.rb b/app/graphql/types/notes/discussion_type.rb index 74a233e9d26..a51d253097d 100644 --- a/app/graphql/types/notes/discussion_type.rb +++ b/app/graphql/types/notes/discussion_type.rb @@ -7,6 +7,8 @@ module Types authorize :read_note + implements(Types::ResolvableInterface) + field :id, GraphQL::ID_TYPE, null: false, description: "ID of this discussion" field :reply_id, GraphQL::ID_TYPE, null: false, diff --git a/app/graphql/types/notes/note_type.rb b/app/graphql/types/notes/note_type.rb index d48cc868434..8755b4ccad5 100644 --- a/app/graphql/types/notes/note_type.rb +++ b/app/graphql/types/notes/note_type.rb @@ -9,6 +9,8 @@ module Types expose_permissions Types::PermissionTypes::Note + implements(Types::ResolvableInterface) + field :id, GraphQL::ID_TYPE, null: false, description: 'ID of the note' @@ -22,11 +24,6 @@ module Types 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: '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: 'Indicates whether this note was created by the system or by a user' @@ -44,11 +41,6 @@ module Types 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' field :confidential, GraphQL::BOOLEAN_TYPE, null: true, diff --git a/app/graphql/types/notes/noteable_type.rb b/app/graphql/types/notes/noteable_type.rb index 187c9109f8c..3a16d54f9cd 100644 --- a/app/graphql/types/notes/noteable_type.rb +++ b/app/graphql/types/notes/noteable_type.rb @@ -19,6 +19,8 @@ module Types Types::SnippetType when ::DesignManagement::Design Types::DesignManagement::DesignType + when ::AlertManagement::Alert + Types::AlertManagement::AlertType else raise "Unknown GraphQL type for #{object}" end diff --git a/app/graphql/types/permission_types/ci/pipeline.rb b/app/graphql/types/permission_types/ci/pipeline.rb index 73e44a33eba..cfd68380005 100644 --- a/app/graphql/types/permission_types/ci/pipeline.rb +++ b/app/graphql/types/permission_types/ci/pipeline.rb @@ -6,7 +6,8 @@ module Types class Pipeline < BasePermissionType graphql_name 'PipelinePermissions' - abilities :update_pipeline, :admin_pipeline, :destroy_pipeline + abilities :admin_pipeline, :destroy_pipeline + ability_field :update_pipeline, calls_gitaly: true end end end diff --git a/app/graphql/types/permission_types/merge_request.rb b/app/graphql/types/permission_types/merge_request.rb index d877fc177d2..28b7ebd2af6 100644 --- a/app/graphql/types/permission_types/merge_request.rb +++ b/app/graphql/types/permission_types/merge_request.rb @@ -3,6 +3,11 @@ module Types module PermissionTypes class MergeRequest < BasePermissionType + PERMISSION_FIELDS = %i[push_to_source_branch + remove_source_branch + cherry_pick_on_current_merge_request + revert_on_current_merge_request].freeze + present_using MergeRequestPresenter description 'Check permissions for the current user on a merge request' graphql_name 'MergeRequestPermissions' @@ -10,10 +15,9 @@ module Types abilities :read_merge_request, :admin_merge_request, :update_merge_request, :create_note - permission_field :push_to_source_branch, method: :can_push_to_source_branch?, calls_gitaly: true - permission_field :remove_source_branch, method: :can_remove_source_branch?, calls_gitaly: true - permission_field :cherry_pick_on_current_merge_request, method: :can_cherry_pick_on_current_merge_request? - permission_field :revert_on_current_merge_request, method: :can_revert_on_current_merge_request? + PERMISSION_FIELDS.each do |field_name| + permission_field field_name, method: :"can_#{field_name}?", calls_gitaly: true + end end end end diff --git a/app/graphql/types/project_member_type.rb b/app/graphql/types/project_member_type.rb new file mode 100644 index 00000000000..e9ccb51886b --- /dev/null +++ b/app/graphql/types/project_member_type.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Types + class ProjectMemberType < BaseObject + graphql_name 'ProjectMember' + description 'Represents a Project Member' + + expose_permissions Types::PermissionTypes::Project + + implements MemberInterface + + authorize :read_project + + field :id, GraphQL::ID_TYPE, null: false, + description: 'ID of the member' + + field :user, Types::UserType, null: false, + description: 'User that is associated with the member object', + resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, obj.user_id).find } + + field :project, Types::ProjectType, null: true, + description: 'Project that User is a member of', + resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, obj.source_id).find } + end +end diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index 4e438ed2576..bbfb7fc4f20 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -95,6 +95,8 @@ module Types description: 'Status of Jira import background job of the project' field :only_allow_merge_if_pipeline_succeeds, GraphQL::BOOLEAN_TYPE, null: true, description: 'Indicates if merge requests of the project can only be merged with successful jobs' + field :allow_merge_on_skipped_pipeline, GraphQL::BOOLEAN_TYPE, null: true, + description: 'If `only_allow_merge_if_pipeline_succeeds` is true, indicates if merge requests of the project can also be merged with skipped jobs' field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true, description: 'Indicates if users can request member access to the project' field :only_allow_merge_if_all_discussions_are_resolved, GraphQL::BOOLEAN_TYPE, null: true, @@ -125,6 +127,7 @@ module Types Types::MergeRequestType.connection_type, null: true, description: 'Merge requests of the project', + extras: [:lookahead], resolver: Resolvers::MergeRequestsResolver field :merge_request, @@ -139,6 +142,11 @@ module Types description: 'Issues of the project', resolver: Resolvers::IssuesResolver + field :project_members, + Types::ProjectMemberType.connection_type, + description: 'Members of the project', + resolver: Resolvers::ProjectMembersResolver + field :environments, Types::EnvironmentType.connection_type, null: true, @@ -157,6 +165,12 @@ module Types description: 'Build pipelines of the project', resolver: Resolvers::ProjectPipelinesResolver + field :pipeline, + Types::Ci::PipelineType, + null: true, + description: 'Build pipeline of the project', + resolver: Resolvers::ProjectPipelineResolver + field :sentry_detailed_error, Types::ErrorTracking::SentryDetailedErrorType, null: true, @@ -210,13 +224,14 @@ module Types Types::AlertManagement::AlertType.connection_type, null: true, description: 'Alert Management alerts of the project', - resolver: Resolvers::AlertManagementAlertResolver + extras: [:lookahead], + resolver: Resolvers::AlertManagement::AlertResolver field :alert_management_alert, Types::AlertManagement::AlertType, null: true, description: 'A single Alert Management alert of the project', - resolver: Resolvers::AlertManagementAlertResolver.single + resolver: Resolvers::AlertManagement::AlertResolver.single field :alert_management_alert_status_counts, Types::AlertManagement::AlertStatusCountsType, @@ -237,6 +252,50 @@ module Types description: 'A single release of the project', resolver: Resolvers::ReleasesResolver.single, feature_flag: :graphql_release_data + + field :container_expiration_policy, + Types::ContainerExpirationPolicyType, + null: true, + description: 'The container expiration policy of the project' + + field :label, + Types::LabelType, + null: true, + description: 'A label available on this project' do + argument :title, GraphQL::STRING_TYPE, + required: true, + description: 'Title of the label' + end + + def label(title:) + BatchLoader::GraphQL.for(title).batch(key: project) do |titles, loader, args| + LabelsFinder + .new(current_user, project: args[:key], title: titles) + .execute + .each { |label| loader.call(label.title, label) } + end + end + + field :labels, + Types::LabelType.connection_type, + null: true, + description: 'Labels available on this project' do + argument :search_term, GraphQL::STRING_TYPE, + required: false, + description: 'A search term to find labels with' + end + + def labels(search_term: nil) + LabelsFinder + .new(current_user, project: project, search: search_term) + .execute + end + + private + + def project + @project ||= object.respond_to?(:sync) ? object.sync : object + end end end diff --git a/app/graphql/types/projects/service_type.rb b/app/graphql/types/projects/service_type.rb index 55dd828d4b8..4ae7cb77904 100644 --- a/app/graphql/types/projects/service_type.rb +++ b/app/graphql/types/projects/service_type.rb @@ -6,7 +6,7 @@ module Types include Types::BaseInterface graphql_name 'Service' - # TODO: Add all the fields that we want to expose for the project services intergrations + # TODO: Add all the fields that we want to expose for the project services integrations # https://gitlab.com/gitlab-org/gitlab/-/issues/213088 field :type, GraphQL::STRING_TYPE, null: true, description: 'Class name of the service' diff --git a/app/graphql/types/projects/services/jira_project_type.rb b/app/graphql/types/projects/services/jira_project_type.rb new file mode 100644 index 00000000000..ccf9107f398 --- /dev/null +++ b/app/graphql/types/projects/services/jira_project_type.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Types + module Projects + module Services + # rubocop:disable Graphql/AuthorizeTypes + class JiraProjectType < BaseObject + graphql_name 'JiraProject' + + field :key, GraphQL::STRING_TYPE, null: false, + description: 'Key of the Jira project' + field :project_id, GraphQL::INT_TYPE, null: false, + description: 'ID of the Jira project', + method: :id + field :name, GraphQL::STRING_TYPE, null: true, + description: 'Name of the Jira project' + end + # rubocop:enable Graphql/AuthorizeTypes + end + end +end diff --git a/app/graphql/types/projects/services/jira_service_type.rb b/app/graphql/types/projects/services/jira_service_type.rb index 4fd9e61f5a4..e81963f752d 100644 --- a/app/graphql/types/projects/services/jira_service_type.rb +++ b/app/graphql/types/projects/services/jira_service_type.rb @@ -9,9 +9,14 @@ module Types implements(Types::Projects::ServiceType) authorize :admin_project - # This is a placeholder for now for the actuall implementation of the JiraServiceType - # Here we will want to expose a field with jira_projects fetched through Jira Rest API - # MR implementing it https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28190 + + field :projects, + Types::Projects::Services::JiraProjectType.connection_type, + null: true, + connection: false, + extensions: [Gitlab::Graphql::Extensions::ExternallyPaginatedArrayExtension], + description: 'List of Jira projects fetched through Jira REST API', + resolver: Resolvers::Projects::JiraProjectsResolver end end end diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index 70cdcb62bc6..362e4004b73 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -47,10 +47,24 @@ module Types null: false, description: 'Fields related to design management' + field :user, Types::UserType, + null: true, + description: 'Find a user', + resolver: Resolvers::UserResolver + + field :users, Types::UserType.connection_type, + null: true, + description: 'Find users', + resolver: Resolvers::UsersResolver + field :echo, GraphQL::STRING_TYPE, null: false, description: 'Text to echo back', resolver: Resolvers::EchoResolver + field :user, Types::UserType, null: true, + description: 'Find a user on this instance', + resolver: Resolvers::UserResolver + def design_management DesignManagementObject.new(nil) end diff --git a/app/graphql/types/release_assets_type.rb b/app/graphql/types/release_assets_type.rb new file mode 100644 index 00000000000..58ad05b5365 --- /dev/null +++ b/app/graphql/types/release_assets_type.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Types + class ReleaseAssetsType < BaseObject + graphql_name 'ReleaseAssets' + + authorize :read_release + + alias_method :release, :object + + present_using ReleasePresenter + + field :assets_count, GraphQL::INT_TYPE, null: true, + description: 'Number of assets of the release' + field :links, Types::ReleaseLinkType.connection_type, null: true, + description: 'Asset links of the release' + field :sources, Types::ReleaseSourceType.connection_type, null: true, + description: 'Sources of the release' + end +end diff --git a/app/graphql/types/release_link_type.rb b/app/graphql/types/release_link_type.rb new file mode 100644 index 00000000000..070f14a90df --- /dev/null +++ b/app/graphql/types/release_link_type.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Types + class ReleaseLinkType < BaseObject + graphql_name 'ReleaseLink' + + authorize :read_release + + field :id, GraphQL::ID_TYPE, null: false, + description: 'ID of the link' + field :name, GraphQL::STRING_TYPE, null: true, + description: 'Name of the link' + field :url, GraphQL::STRING_TYPE, null: true, + description: 'URL of the link' + field :link_type, Types::ReleaseLinkTypeEnum, null: true, + description: 'Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`' + field :external, GraphQL::BOOLEAN_TYPE, null: true, method: :external?, + description: 'Indicates the link points to an external resource' + end +end diff --git a/app/graphql/types/release_link_type_enum.rb b/app/graphql/types/release_link_type_enum.rb new file mode 100644 index 00000000000..b364855833f --- /dev/null +++ b/app/graphql/types/release_link_type_enum.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Types + class ReleaseLinkTypeEnum < BaseEnum + graphql_name 'ReleaseLinkType' + description 'Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`' + + ::Releases::Link.link_types.keys.each do |link_type| + value link_type.upcase, value: link_type, description: "#{link_type.titleize} link type" + end + end +end diff --git a/app/graphql/types/release_source_type.rb b/app/graphql/types/release_source_type.rb new file mode 100644 index 00000000000..0ec1ad85a39 --- /dev/null +++ b/app/graphql/types/release_source_type.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Types + class ReleaseSourceType < BaseObject + graphql_name 'ReleaseSource' + + authorize :read_release_sources + + field :format, GraphQL::STRING_TYPE, null: true, + description: 'Format of the source' + field :url, GraphQL::STRING_TYPE, null: true, + description: 'Download URL of the source' + end +end diff --git a/app/graphql/types/release_type.rb b/app/graphql/types/release_type.rb index 632351be5d3..3d8e5a93c68 100644 --- a/app/graphql/types/release_type.rb +++ b/app/graphql/types/release_type.rb @@ -23,8 +23,12 @@ module Types description: 'Timestamp of when the release was created' field :released_at, Types::TimeType, null: true, description: 'Timestamp of when the release was released' + field :assets, Types::ReleaseAssetsType, null: true, method: :itself, + description: 'Assets of the release' field :milestones, Types::MilestoneType.connection_type, null: true, description: 'Milestones associated to the release' + field :evidences, Types::EvidenceType.connection_type, null: true, + description: 'Evidence for the release' field :author, Types::UserType, null: true, description: 'User that created the release' diff --git a/app/graphql/types/resolvable_interface.rb b/app/graphql/types/resolvable_interface.rb new file mode 100644 index 00000000000..a39092c70ca --- /dev/null +++ b/app/graphql/types/resolvable_interface.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Types + # This Interface contains fields that are shared between objects that include either + # the `ResolvableNote` or `ResolvableDiscussion` modules. + module ResolvableInterface + include Types::BaseInterface + + field :resolved_by, Types::UserType, + null: true, + description: 'User who resolved the object' + + def resolved_by + return unless object.resolved_by_id + + Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.resolved_by_id).find + end + + field :resolved, GraphQL::BOOLEAN_TYPE, null: false, + description: 'Indicates if the object is resolved', + method: :resolved? + field :resolvable, GraphQL::BOOLEAN_TYPE, null: false, + description: 'Indicates if the object can be resolved', + method: :resolvable? + field :resolved_at, Types::TimeType, null: true, + description: 'Timestamp of when the object was resolved' + end +end diff --git a/app/graphql/types/snippet_type.rb b/app/graphql/types/snippet_type.rb index b23c4f71ffa..73ca3425ded 100644 --- a/app/graphql/types/snippet_type.rb +++ b/app/graphql/types/snippet_type.rb @@ -27,9 +27,12 @@ module Types authorize: :read_project, resolve: -> (snippet, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, snippet.project_id).find } + # Author can be nil in some scenarios. For example, + # when the admin setting restricted visibility + # level is set to public field :author, Types::UserType, description: 'The owner of the snippet', - null: false, + null: true, resolve: -> (snippet, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, snippet.author_id).find } field :file_name, GraphQL::STRING_TYPE, @@ -65,6 +68,11 @@ module Types calls_gitaly: true, null: false + field :blobs, type: [Types::Snippets::BlobType], + description: 'Snippet blobs', + calls_gitaly: true, + null: false + field :ssh_url_to_repo, type: GraphQL::STRING_TYPE, description: 'SSH URL to the snippet repository', calls_gitaly: true, diff --git a/app/graphql/types/snippets/file_input_action_enum.rb b/app/graphql/types/snippets/file_input_action_enum.rb new file mode 100644 index 00000000000..7785853f3a8 --- /dev/null +++ b/app/graphql/types/snippets/file_input_action_enum.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Types + module Snippets + class FileInputActionEnum < BaseEnum + graphql_name 'SnippetFileInputActionEnum' + description 'Type of a snippet file input action' + + value 'create', value: :create + value 'update', value: :update + value 'delete', value: :delete + value 'move', value: :move + end + end +end diff --git a/app/graphql/types/snippets/file_input_type.rb b/app/graphql/types/snippets/file_input_type.rb new file mode 100644 index 00000000000..85a02c8f493 --- /dev/null +++ b/app/graphql/types/snippets/file_input_type.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Types + module Snippets + class FileInputType < BaseInputObject # rubocop:disable Graphql/AuthorizeTypes + graphql_name 'SnippetFileInputType' + description 'Represents an action to perform over a snippet file' + + argument :action, Types::Snippets::FileInputActionEnum, + description: 'Type of input action', + required: true + + argument :previous_path, GraphQL::STRING_TYPE, + description: 'Previous path of the snippet file', + required: false + + argument :file_path, GraphQL::STRING_TYPE, + description: 'Path of the snippet file', + required: true + + argument :content, GraphQL::STRING_TYPE, + description: 'Snippet file content', + required: false + end + end +end diff --git a/app/graphql/types/user_state_enum.rb b/app/graphql/types/user_state_enum.rb new file mode 100644 index 00000000000..d34936b4c48 --- /dev/null +++ b/app/graphql/types/user_state_enum.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Types + class UserStateEnum < BaseEnum + graphql_name 'UserState' + description 'Possible states of a user' + + value 'active', 'The user is active and is able to use the system', value: 'active' + value 'blocked', 'The user has been blocked and is prevented from using the system', value: 'blocked' + value 'deactivated', 'The user is no longer active and is unable to use the system', value: 'deactivated' + end +end diff --git a/app/graphql/types/user_type.rb b/app/graphql/types/user_type.rb index 29a3f5d452f..ab3c84ea539 100644 --- a/app/graphql/types/user_type.rb +++ b/app/graphql/types/user_type.rb @@ -12,12 +12,12 @@ module Types field :id, GraphQL::ID_TYPE, null: false, description: 'ID of the user' - field :name, GraphQL::STRING_TYPE, null: false, - description: 'Human-readable name of the user' - field :state, GraphQL::STRING_TYPE, null: false, - description: 'State of the issue' field :username, GraphQL::STRING_TYPE, null: false, description: 'Username of the user. Unique within this instance of GitLab' + field :name, GraphQL::STRING_TYPE, null: false, + description: 'Human-readable name of the user' + field :state, Types::UserStateEnum, null: false, + description: 'State of the user' field :avatar_url, GraphQL::STRING_TYPE, null: true, description: "URL of the user's avatar" field :web_url, GraphQL::STRING_TYPE, null: false, @@ -25,6 +25,20 @@ module Types field :todos, Types::TodoType.connection_type, null: false, resolver: Resolvers::TodoResolver, description: 'Todos of the user' + field :group_memberships, Types::GroupMemberType.connection_type, null: true, + description: 'Group memberships of the user', + method: :group_members + field :project_memberships, Types::ProjectMemberType.connection_type, null: true, + description: 'Project memberships of the user', + method: :project_members + + # Merge request field: MRs can be either authored or assigned: + field :authored_merge_requests, Types::MergeRequestType.connection_type, null: true, + resolver: Resolvers::AuthoredMergeRequestsResolver, + description: 'Merge Requests authored by the user' + field :assigned_merge_requests, Types::MergeRequestType.connection_type, null: true, + resolver: Resolvers::AssignedMergeRequestsResolver, + description: 'Merge Requests assigned to the user' field :snippets, Types::SnippetType.connection_type, |