diff options
Diffstat (limited to 'app/graphql/resolvers')
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 |