Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/graphql/resolvers')
-rw-r--r--app/graphql/resolvers/alert_management/alert_resolver.rb42
-rw-r--r--app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb4
-rw-r--r--app/graphql/resolvers/alert_management_alert_resolver.rb31
-rw-r--r--app/graphql/resolvers/assigned_merge_requests_resolver.rb9
-rw-r--r--app/graphql/resolvers/authored_merge_requests_resolver.rb9
-rw-r--r--app/graphql/resolvers/base_resolver.rb33
-rw-r--r--app/graphql/resolvers/concerns/looks_ahead.rb52
-rw-r--r--app/graphql/resolvers/concerns/resolves_merge_requests.rb67
-rw-r--r--app/graphql/resolvers/concerns/resolves_project.rb15
-rw-r--r--app/graphql/resolvers/full_path_resolver.rb7
-rw-r--r--app/graphql/resolvers/merge_request_resolver.rb18
-rw-r--r--app/graphql/resolvers/merge_requests_resolver.rb52
-rw-r--r--app/graphql/resolvers/project_members_resolver.rb21
-rw-r--r--app/graphql/resolvers/project_pipeline_resolver.rb17
-rw-r--r--app/graphql/resolvers/projects/jira_imports_resolver.rb2
-rw-r--r--app/graphql/resolvers/projects/jira_projects_resolver.rb76
-rw-r--r--app/graphql/resolvers/user_merge_requests_resolver.rb68
-rw-r--r--app/graphql/resolvers/user_resolver.rb43
-rw-r--r--app/graphql/resolvers/users_resolver.rb57
19 files changed, 548 insertions, 75 deletions
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