Skip to content
Snippets Groups Projects
Commit 86d48f26 authored by Mario Celi's avatar Mario Celi :two:
Browse files

Merge branch '377514-refactor-issues-resolver' into 'master'

Refactor parent issues resolver

See merge request !102655



Merged-by: default avatarMario Celi <mcelicalderon@gitlab.com>
Approved-by: default avatarEugenia Grieff <egrieff@gitlab.com>
parents 62773989 0042e23e
No related branches found
No related tags found
No related merge requests found
Pipeline #686043968 passed
Pipeline: GitLab

#686046421

    Showing
    with 556 additions and 355 deletions
    # frozen_string_literal: true
    module Resolvers
    class BaseIssuesResolver < BaseResolver
    prepend IssueResolverArguments
    argument :sort, Types::IssueSortEnum,
    description: 'Sort issues by this criteria.',
    required: false,
    default_value: :created_desc
    argument :state, Types::IssuableStateEnum,
    required: false,
    description: 'Current state of this issue.'
    # see app/graphql/types/issue_connection.rb
    type 'Types::IssueConnection', null: true
    NON_STABLE_CURSOR_SORTS = %i[priority_asc priority_desc
    popularity_asc popularity_desc
    label_priority_asc label_priority_desc
    milestone_due_asc milestone_due_desc
    escalation_status_asc escalation_status_desc].freeze
    def continue_issue_resolve(parent, finder, **args)
    issues = Gitlab::Graphql::Loaders::IssuableLoader.new(parent, finder).batching_find_all { |q| apply_lookahead(q) }
    if non_stable_cursor_sort?(args[:sort])
    # Certain complex sorts are not supported by the stable cursor pagination yet.
    # In these cases, we use offset pagination, so we return the correct connection.
    offset_pagination(issues)
    else
    issues
    end
    end
    private
    def unconditional_includes
    [
    {
    project: [:project_feature, :group]
    },
    :author
    ]
    end
    def preloads
    {
    alert_management_alert: [:alert_management_alert],
    assignees: [:assignees],
    participants: Issue.participant_includes,
    timelogs: [:timelogs],
    customer_relations_contacts: { customer_relations_contacts: [:group] },
    escalation_status: [:incident_management_issuable_escalation_status]
    }
    end
    def non_stable_cursor_sort?(sort)
    NON_STABLE_CURSOR_SORTS.include?(sort)
    end
    end
    end
    Resolvers::BaseIssuesResolver.prepend_mod_with('Resolvers::BaseIssuesResolver')
    # frozen_string_literal: true
    module IssueResolverArguments
    extend ActiveSupport::Concern
    prepended do
    include SearchArguments
    include LooksAhead
    argument :iid, GraphQL::Types::String,
    required: false,
    description: 'IID of the issue. For example, "1".'
    argument :iids, [GraphQL::Types::String],
    required: false,
    description: 'List of IIDs of issues. For example, `["1", "2"]`.'
    argument :label_name, [GraphQL::Types::String, null: true],
    required: false,
    description: 'Labels applied to this issue.'
    argument :milestone_title, [GraphQL::Types::String, null: true],
    required: false,
    description: 'Milestone applied to this issue.'
    argument :author_username, GraphQL::Types::String,
    required: false,
    description: 'Username of the author of the issue.'
    argument :assignee_username, GraphQL::Types::String,
    required: false,
    description: 'Username of a user assigned to the issue.',
    deprecated: { reason: 'Use `assigneeUsernames`', milestone: '13.11' }
    argument :assignee_usernames, [GraphQL::Types::String],
    required: false,
    description: 'Usernames of users assigned to the issue.'
    argument :assignee_id, GraphQL::Types::String,
    required: false,
    description: 'ID of a user assigned to the issues. Wildcard values "NONE" and "ANY" are supported.'
    argument :created_before, Types::TimeType,
    required: false,
    description: 'Issues created before this date.'
    argument :created_after, Types::TimeType,
    required: false,
    description: 'Issues created after this date.'
    argument :updated_before, Types::TimeType,
    required: false,
    description: 'Issues updated before this date.'
    argument :updated_after, Types::TimeType,
    required: false,
    description: 'Issues updated after this date.'
    argument :closed_before, Types::TimeType,
    required: false,
    description: 'Issues closed before this date.'
    argument :closed_after, Types::TimeType,
    required: false,
    description: 'Issues closed after this date.'
    argument :types, [Types::IssueTypeEnum],
    as: :issue_types,
    description: 'Filter issues by the given issue types.',
    required: false
    argument :milestone_wildcard_id, ::Types::MilestoneWildcardIdEnum,
    required: false,
    description: 'Filter issues by milestone ID wildcard.'
    argument :my_reaction_emoji, GraphQL::Types::String,
    required: false,
    description: 'Filter by reaction emoji applied by the current user. Wildcard values "NONE" and "ANY" are supported.'
    argument :confidential,
    GraphQL::Types::Boolean,
    required: false,
    description: 'Filter for confidential issues. If "false", excludes confidential issues. If "true", returns only confidential issues.'
    argument :not, Types::Issues::NegatedIssueFilterInputType,
    description: 'Negated arguments.',
    required: false
    argument :or, Types::Issues::UnionedIssueFilterInputType,
    description: 'List of arguments with inclusive OR.',
    required: false
    argument :crm_contact_id, GraphQL::Types::String,
    required: false,
    description: 'ID of a contact assigned to the issues.'
    argument :crm_organization_id, GraphQL::Types::String,
    required: false,
    description: 'ID of an organization assigned to the issues.'
    end
    def resolve_with_lookahead(**args)
    return Issue.none if resource_parent.nil?
    finder = IssuesFinder.new(current_user, prepare_finder_params(args))
    continue_issue_resolve(resource_parent, finder, **args)
    end
    def ready?(**args)
    if args[:or].present? && ::Feature.disabled?(:or_issuable_queries, resource_parent)
    raise ::Gitlab::Graphql::Errors::ArgumentError, "'or' arguments are only allowed when the `or_issuable_queries` feature flag is enabled."
    end
    args[:not] = args[:not].to_h if args[:not]
    args[:or] = args[:or].to_h if args[:or]
    params_not_mutually_exclusive(args, mutually_exclusive_assignee_username_args)
    params_not_mutually_exclusive(args, mutually_exclusive_milestone_args)
    params_not_mutually_exclusive(args.fetch(:not, {}), mutually_exclusive_milestone_args)
    params_not_mutually_exclusive(args, mutually_exclusive_release_tag_args)
    super
    end
    class_methods do
    def resolver_complexity(args, child_complexity:)
    complexity = super
    complexity += 2 if args[:labelName]
    complexity
    end
    def accept_release_tag
    argument :release_tag, [GraphQL::Types::String],
    required: false,
    description: "Release tag associated with the issue's milestone."
    argument :release_tag_wildcard_id, Types::ReleaseTagWildcardIdEnum,
    required: false,
    description: 'Filter issues by release tag ID wildcard.'
    end
    end
    private
    def prepare_finder_params(args)
    params = super(args)
    params[:not] = params[:not].to_h if params[:not]
    params[:or] = params[:or].to_h if params[:or]
    params[:iids] ||= [params.delete(:iid)].compact if params[:iid]
    params[:attempt_project_search_optimizations] = true if params[:search].present?
    prepare_author_username_params(params)
    prepare_assignee_username_params(params)
    prepare_release_tag_params(params)
    params
    end
    def prepare_release_tag_params(args)
    release_tag_wildcard = args.delete(:release_tag_wildcard_id)
    return if release_tag_wildcard.blank?
    args[:release_tag] ||= release_tag_wildcard
    end
    def prepare_author_username_params(args)
    args[:or][:author_username] = args[:or].delete(:author_usernames) if args.dig(:or, :author_usernames).present?
    end
    def prepare_assignee_username_params(args)
    args[:assignee_username] = args.delete(:assignee_usernames) if args[:assignee_usernames].present?
    args[:not][:assignee_username] = args[:not].delete(:assignee_usernames) if args.dig(:not, :assignee_usernames).present?
    args[:or][:assignee_username] = args[:or].delete(:assignee_usernames) if args.dig(:or, :assignee_usernames).present?
    end
    def mutually_exclusive_release_tag_args
    [:release_tag, :release_tag_wildcard_id]
    end
    def mutually_exclusive_milestone_args
    [:milestone_title, :milestone_wildcard_id]
    end
    def mutually_exclusive_assignee_username_args
    [:assignee_usernames, :assignee_username]
    end
    def params_not_mutually_exclusive(args, mutually_exclusive_args)
    if args.slice(*mutually_exclusive_args).compact.size > 1
    arg_str = mutually_exclusive_args.map { |x| x.to_s.camelize(:lower) }.join(', ')
    raise ::Gitlab::Graphql::Errors::ArgumentError, "only one of [#{arg_str}] arguments is allowed at the same time."
    end
    end
    def resource_parent
    # The project could have been loaded in batch by `BatchLoader`.
    # At this point we need the `id` of the project to query for issues, so
    # make sure it's loaded and not `nil` before continuing.
    strong_memoize(:resource_parent) do
    object.respond_to?(:sync) ? object.sync : object
    end
    end
    end
    # frozen_string_literal: true
    module Issues
    module LookAheadPreloads
    extend ActiveSupport::Concern
    prepended do
    include ::LooksAhead
    end
    private
    def unconditional_includes
    [
    {
    project: [:project_feature, :group]
    },
    :author
    ]
    end
    def preloads
    {
    alert_management_alert: [:alert_management_alert],
    assignees: [:assignees],
    participants: Issue.participant_includes,
    timelogs: [:timelogs],
    customer_relations_contacts: { customer_relations_contacts: [:group] },
    escalation_status: [:incident_management_issuable_escalation_status]
    }
    end
    end
    end
    Issues::LookAheadPreloads.prepend_mod
    # frozen_string_literal: true
    module Issues
    module SortArguments
    extend ActiveSupport::Concern
    NON_STABLE_CURSOR_SORTS = %i[priority_asc priority_desc
    popularity_asc popularity_desc
    label_priority_asc label_priority_desc
    milestone_due_asc milestone_due_desc
    escalation_status_asc escalation_status_desc].freeze
    included do
    argument :sort, Types::IssueSortEnum,
    description: 'Sort issues by this criteria.',
    required: false,
    default_value: :created_desc
    end
    private
    def non_stable_cursor_sort?(sort)
    NON_STABLE_CURSOR_SORTS.include?(sort)
    end
    end
    end
    ......@@ -46,9 +46,17 @@ def prepare_finder_params(args)
    def prepare_search_params(args)
    return args unless args[:search].present?
    args[:in] = args[:in].join(',') if args[:in].present?
    set_search_optimization_param(args)
    args
    end
    def set_search_optimization_param(args)
    return args unless respond_to?(:resource_parent, true) && resource_parent.present?
    parent_type = resource_parent.is_a?(Project) ? :project : :group
    args[:"attempt_#{parent_type}_search_optimizations"] = true
    args[:in] = args[:in].join(',') if args[:in].present?
    args
    end
    ......
    ......@@ -2,7 +2,7 @@
    # rubocop:disable Graphql/ResolverType (inherited from BaseIssuesResolver)
    module Resolvers
    class GroupIssuesResolver < BaseIssuesResolver
    class GroupIssuesResolver < Issues::BaseParentResolver
    def self.issuable_collection_name
    'issues'
    end
    ......
    # frozen_string_literal: true
    module Resolvers
    class IssueStatusCountsResolver < BaseResolver
    prepend IssueResolverArguments
    class IssueStatusCountsResolver < Issues::BaseResolver
    type Types::IssueStatusCountsType, null: true
    accept_release_tag
    extras [:lookahead]
    def resolve(**args)
    return Issue.none if resource_parent.nil?
    finder = IssuesFinder.new(current_user, prepare_finder_params(args))
    finder.parent_param = resource_parent
    Gitlab::IssuablesCountForState.new(finder, resource_parent)
    end
    private
    def continue_issue_resolve(parent, finder, **args)
    finder.parent_param = parent
    apply_lookahead(Gitlab::IssuablesCountForState.new(finder, parent))
    def resource_parent
    # The project could have been loaded in batch by `BatchLoader`.
    # At this point we need the `id` of the project to query for issues, so
    # make sure it's loaded and not `nil` before continuing.
    strong_memoize(:resource_parent) do
    object.respond_to?(:sync) ? object.sync : object
    end
    end
    end
    end
    # frozen_string_literal: true
    module Resolvers
    module Issues
    class BaseParentResolver < Issues::BaseResolver
    prepend ::Issues::LookAheadPreloads
    include ::Issues::SortArguments
    argument :state, Types::IssuableStateEnum,
    required: false,
    description: 'Current state of this issue.'
    # see app/graphql/types/issue_connection.rb
    type 'Types::IssueConnection', null: true
    def resolve_with_lookahead(**args)
    return Issue.none if resource_parent.nil?
    finder = IssuesFinder.new(current_user, prepare_finder_params(args))
    issues = Gitlab::Graphql::Loaders::IssuableLoader.new(resource_parent, finder).batching_find_all do |q|
    apply_lookahead(q)
    end
    if non_stable_cursor_sort?(args[:sort])
    # Certain complex sorts are not supported by the stable cursor pagination yet.
    # In these cases, we use offset pagination, so we return the correct connection.
    offset_pagination(issues)
    else
    issues
    end
    end
    private
    def resource_parent
    # The project could have been loaded in batch by `BatchLoader`.
    # At this point we need the `id` of the project to query for issues, so
    # make sure it's loaded and not `nil` before continuing.
    strong_memoize(:resource_parent) do
    object.respond_to?(:sync) ? object.sync : object
    end
    end
    end
    end
    end
    Resolvers::Issues::BaseParentResolver.prepend_mod
    # frozen_string_literal: true
    module Resolvers
    module Issues
    # rubocop:disable Graphql/ResolverType
    class BaseResolver < Resolvers::BaseResolver
    include SearchArguments
    argument :assignee_id, GraphQL::Types::String,
    required: false,
    description: 'ID of a user assigned to the issues. Wildcard values "NONE" and "ANY" are supported.'
    argument :assignee_username, GraphQL::Types::String,
    required: false,
    description: 'Username of a user assigned to the issue.',
    deprecated: { reason: 'Use `assigneeUsernames`', milestone: '13.11' }
    argument :assignee_usernames, [GraphQL::Types::String],
    required: false,
    description: 'Usernames of users assigned to the issue.'
    argument :author_username, GraphQL::Types::String,
    required: false,
    description: 'Username of the author of the issue.'
    argument :closed_after, Types::TimeType,
    required: false,
    description: 'Issues closed after this date.'
    argument :closed_before, Types::TimeType,
    required: false,
    description: 'Issues closed before this date.'
    argument :confidential,
    GraphQL::Types::Boolean,
    required: false,
    description: 'Filter for confidential issues. If "false", excludes confidential issues.' \
    ' If "true", returns only confidential issues.'
    argument :created_after, Types::TimeType,
    required: false,
    description: 'Issues created after this date.'
    argument :created_before, Types::TimeType,
    required: false,
    description: 'Issues created before this date.'
    argument :crm_contact_id, GraphQL::Types::String,
    required: false,
    description: 'ID of a contact assigned to the issues.'
    argument :crm_organization_id, GraphQL::Types::String,
    required: false,
    description: 'ID of an organization assigned to the issues.'
    argument :iid, GraphQL::Types::String,
    required: false,
    description: 'IID of the issue. For example, "1".'
    argument :iids, [GraphQL::Types::String],
    required: false,
    description: 'List of IIDs of issues. For example, `["1", "2"]`.'
    argument :label_name, [GraphQL::Types::String, { null: true }],
    required: false,
    description: 'Labels applied to this issue.'
    argument :milestone_title, [GraphQL::Types::String, { null: true }],
    required: false,
    description: 'Milestone applied to this issue.'
    argument :milestone_wildcard_id, ::Types::MilestoneWildcardIdEnum,
    required: false,
    description: 'Filter issues by milestone ID wildcard.'
    argument :my_reaction_emoji, GraphQL::Types::String,
    required: false,
    description: 'Filter by reaction emoji applied by the current user.' \
    ' Wildcard values "NONE" and "ANY" are supported.'
    argument :not, Types::Issues::NegatedIssueFilterInputType,
    description: 'Negated arguments.',
    required: false
    argument :or, Types::Issues::UnionedIssueFilterInputType,
    description: 'List of arguments with inclusive OR.',
    required: false
    argument :types, [Types::IssueTypeEnum],
    as: :issue_types,
    description: 'Filter issues by the given issue types.',
    required: false
    argument :updated_after, Types::TimeType,
    required: false,
    description: 'Issues updated after this date.'
    argument :updated_before, Types::TimeType,
    required: false,
    description: 'Issues updated before this date.'
    class << self
    def resolver_complexity(args, child_complexity:)
    complexity = super
    complexity += 2 if args[:labelName]
    complexity
    end
    def accept_release_tag
    argument :release_tag, [GraphQL::Types::String],
    required: false,
    description: "Release tag associated with the issue's milestone."
    argument :release_tag_wildcard_id, Types::ReleaseTagWildcardIdEnum,
    required: false,
    description: 'Filter issues by release tag ID wildcard.'
    end
    end
    def ready?(**args)
    if args[:or].present? && ::Feature.disabled?(:or_issuable_queries, resource_parent)
    raise ::Gitlab::Graphql::Errors::ArgumentError,
    "'or' arguments are only allowed when the `or_issuable_queries` feature flag is enabled."
    end
    args[:not] = args[:not].to_h if args[:not]
    args[:or] = args[:or].to_h if args[:or]
    params_not_mutually_exclusive(args, mutually_exclusive_assignee_username_args)
    params_not_mutually_exclusive(args, mutually_exclusive_milestone_args)
    params_not_mutually_exclusive(args.fetch(:not, {}), mutually_exclusive_milestone_args)
    params_not_mutually_exclusive(args, mutually_exclusive_release_tag_args)
    super
    end
    private
    def prepare_finder_params(args)
    params = super(args)
    params[:not] = params[:not].to_h if params[:not]
    params[:or] = params[:or].to_h if params[:or]
    params[:iids] ||= [params.delete(:iid)].compact if params[:iid]
    prepare_author_username_params(params)
    prepare_assignee_username_params(params)
    prepare_release_tag_params(params)
    params
    end
    def prepare_release_tag_params(args)
    release_tag_wildcard = args.delete(:release_tag_wildcard_id)
    return if release_tag_wildcard.blank?
    args[:release_tag] ||= release_tag_wildcard
    end
    def prepare_author_username_params(args)
    args[:or][:author_username] = args[:or].delete(:author_usernames) if args.dig(:or, :author_usernames).present?
    end
    def prepare_assignee_username_params(args)
    args[:assignee_username] = args.delete(:assignee_usernames) if args[:assignee_usernames].present?
    if args.dig(:or, :assignee_usernames).present?
    args[:or][:assignee_username] = args[:or].delete(:assignee_usernames)
    end
    return unless args.dig(:not, :assignee_usernames).present?
    args[:not][:assignee_username] = args[:not].delete(:assignee_usernames)
    end
    def mutually_exclusive_release_tag_args
    [:release_tag, :release_tag_wildcard_id]
    end
    def mutually_exclusive_milestone_args
    [:milestone_title, :milestone_wildcard_id]
    end
    def mutually_exclusive_assignee_username_args
    [:assignee_usernames, :assignee_username]
    end
    def params_not_mutually_exclusive(args, mutually_exclusive_args)
    return unless args.slice(*mutually_exclusive_args).compact.size > 1
    arg_str = mutually_exclusive_args.map { |x| x.to_s.camelize(:lower) }.join(', ')
    raise ::Gitlab::Graphql::Errors::ArgumentError,
    "only one of [#{arg_str}] arguments is allowed at the same time."
    end
    end
    # rubocop:enable Graphql/ResolverType
    end
    end
    Resolvers::Issues::BaseResolver.prepend_mod
    ......@@ -2,7 +2,7 @@
    # rubocop:disable Graphql/ResolverType (inherited from BaseIssuesResolver)
    module Resolvers
    class IssuesResolver < BaseIssuesResolver
    class IssuesResolver < Issues::BaseParentResolver
    accept_release_tag
    end
    end
    ......@@ -241,7 +241,6 @@ class ProjectType < BaseObject
    Types::IssueStatusCountsType,
    null: true,
    description: 'Counts of issues by status for the project.',
    extras: [:lookahead],
    resolver: Resolvers::IssueStatusCountsResolver
    field :milestones, Types::MilestoneType.connection_type,
    ......
    ......@@ -16933,9 +16933,14 @@ Returns [`IssueStatusCountsType`](#issuestatuscountstype).
    | <a id="projectissuestatuscountscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before this date. |
    | <a id="projectissuestatuscountscrmcontactid"></a>`crmContactId` | [`String`](#string) | ID of a contact assigned to the issues. |
    | <a id="projectissuestatuscountscrmorganizationid"></a>`crmOrganizationId` | [`String`](#string) | ID of an organization assigned to the issues. |
    | <a id="projectissuestatuscountsepicid"></a>`epicId` | [`String`](#string) | ID of an epic associated with the issues, "none" and "any" values are supported. |
    | <a id="projectissuestatuscountshealthstatusfilter"></a>`healthStatusFilter` | [`HealthStatusFilter`](#healthstatusfilter) | Health status of the issue, "none" and "any" values are supported. |
    | <a id="projectissuestatuscountsiid"></a>`iid` | [`String`](#string) | IID of the issue. For example, "1". |
    | <a id="projectissuestatuscountsiids"></a>`iids` | [`[String!]`](#string) | List of IIDs of issues. For example, `["1", "2"]`. |
    | <a id="projectissuestatuscountsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
    | <a id="projectissuestatuscountsincludesubepics"></a>`includeSubepics` | [`Boolean`](#boolean) | Whether to include subepics when filtering issues by epicId. |
    | <a id="projectissuestatuscountsiterationid"></a>`iterationId` | [`[ID]`](#id) | List of iteration Global IDs applied to the issue. |
    | <a id="projectissuestatuscountsiterationwildcardid"></a>`iterationWildcardId` | [`IterationWildcardId`](#iterationwildcardid) | Filter by iteration ID wildcard. |
    | <a id="projectissuestatuscountslabelname"></a>`labelName` | [`[String]`](#string) | Labels applied to this issue. |
    | <a id="projectissuestatuscountsmilestonetitle"></a>`milestoneTitle` | [`[String]`](#string) | Milestone applied to this issue. |
    | <a id="projectissuestatuscountsmilestonewildcardid"></a>`milestoneWildcardId` | [`MilestoneWildcardId`](#milestonewildcardid) | Filter issues by milestone ID wildcard. |
    ......@@ -16948,6 +16953,7 @@ Returns [`IssueStatusCountsType`](#issuestatuscountstype).
    | <a id="projectissuestatuscountstypes"></a>`types` | [`[IssueType!]`](#issuetype) | Filter issues by the given issue types. |
    | <a id="projectissuestatuscountsupdatedafter"></a>`updatedAfter` | [`Time`](#time) | Issues updated after this date. |
    | <a id="projectissuestatuscountsupdatedbefore"></a>`updatedBefore` | [`Time`](#time) | Issues updated before this date. |
    | <a id="projectissuestatuscountsweight"></a>`weight` | [`String`](#string) | Weight applied to the issue, "none" and "any" values are supported. |
     
    ##### `Project.issues`
     
    # frozen_string_literal: true
    module EE
    module Resolvers
    module BaseIssuesResolver
    extend ActiveSupport::Concern
    extend ::Gitlab::Utils::Override
    prepended do
    argument :iteration_id, [::GraphQL::Types::ID, null: true],
    required: false,
    description: 'List of iteration Global IDs applied to the issue.'
    argument :iteration_wildcard_id, ::Types::IterationWildcardIdEnum,
    required: false,
    description: 'Filter by iteration ID wildcard.'
    argument :epic_id, GraphQL::Types::String,
    required: false,
    description: 'ID of an epic associated with the issues, "none" and "any" values are supported.'
    argument :include_subepics, GraphQL::Types::Boolean,
    required: false,
    description: 'Whether to include subepics when filtering issues by epicId.'
    argument :weight, GraphQL::Types::String,
    required: false,
    description: 'Weight applied to the issue, "none" and "any" values are supported.'
    argument :health_status, ::Types::HealthStatusEnum,
    required: false,
    deprecated: { reason: 'Use `healthStatusFilter`', milestone: '15.4' },
    description: 'Health status of the issue.'
    argument :health_status_filter, ::Types::HealthStatusFilterEnum,
    required: false,
    description: 'Health status of the issue, "none" and "any" values are supported.'
    end
    override :resolve_with_lookahead
    def resolve_with_lookahead(**args)
    args[:not] = args[:not].to_h if args[:not]
    args[:iteration_id] = iteration_ids_from_args(args) if args[:iteration_id].present?
    args[:not][:iteration_id] = iteration_ids_from_args(args[:not]) if args.dig(:not, :iteration_id).present?
    prepare_iteration_wildcard_params(args)
    prepare_health_status_params(args)
    super
    end
    def ready?(**args)
    args[:not] = args[:not].to_h if args[:not]
    if iteration_params_not_mutually_exclusive?(args) || iteration_params_not_mutually_exclusive?(args.fetch(:not, {}))
    arg_str = mutually_exclusive_iteration_args.map { |x| x.to_s.camelize(:lower) }.join(', ')
    raise ::Gitlab::Graphql::Errors::ArgumentError, "only one of [#{arg_str}] arguments is allowed at the same time."
    end
    super
    end
    private
    # Originally accepted a raw model id. Now accept a gid, but allow a raw id
    # for backward compatibility
    def iteration_ids_from_args(args)
    args[:iteration_id].map do |id|
    ::GitlabSchema.parse_gid(id, expected_type: ::Iteration).model_id
    rescue ::Gitlab::Graphql::Errors::ArgumentError
    id
    end
    end
    def prepare_iteration_wildcard_params(args)
    args[:iteration_id] = args.delete(:iteration_wildcard_id) if args[:iteration_wildcard_id].present?
    args[:not][:iteration_id] = args[:not].delete(:iteration_wildcard_id) if args.dig(:not, :iteration_wildcard_id).present?
    end
    def prepare_health_status_params(args)
    # health_status argument is deprecated, use health_status_filter instead
    args[:health_status] = args.delete(:health_status_filter) if args[:health_status_filter].present?
    end
    def iteration_params_not_mutually_exclusive?(args)
    args.slice(*mutually_exclusive_iteration_args).compact.size > 1
    end
    def mutually_exclusive_iteration_args
    [:iteration_id, :iteration_wildcard_id]
    end
    override :preloads
    def preloads
    super.merge(
    {
    sla_due_at: [:issuable_sla],
    metric_images: [:metric_images]
    }
    )
    end
    end
    end
    end
    # frozen_string_literal: true
    module EE
    module Resolvers
    module Issues
    module BaseParentResolver
    extend ActiveSupport::Concern
    extend ::Gitlab::Utils::Override
    prepended do
    argument :health_status, ::Types::HealthStatusEnum,
    required: false,
    deprecated: { reason: 'Use `healthStatusFilter`', milestone: '15.4' },
    description: 'Health status of the issue.'
    end
    end
    end
    end
    end
    # frozen_string_literal: true
    module EE
    module Resolvers
    module Issues
    module BaseResolver
    extend ActiveSupport::Concern
    extend ::Gitlab::Utils::Override
    prepended do
    argument :iteration_id, [::GraphQL::Types::ID, { null: true }],
    required: false,
    description: 'List of iteration Global IDs applied to the issue.'
    argument :iteration_wildcard_id, ::Types::IterationWildcardIdEnum,
    required: false,
    description: 'Filter by iteration ID wildcard.'
    argument :epic_id, GraphQL::Types::String,
    required: false,
    description: 'ID of an epic associated with the issues, "none" and "any" values are supported.'
    argument :include_subepics, GraphQL::Types::Boolean,
    required: false,
    description: 'Whether to include subepics when filtering issues by epicId.'
    argument :weight, GraphQL::Types::String,
    required: false,
    description: 'Weight applied to the issue, "none" and "any" values are supported.'
    argument :health_status_filter, ::Types::HealthStatusFilterEnum,
    required: false,
    description: 'Health status of the issue, "none" and "any" values are supported.'
    end
    def ready?(**args)
    args[:not] = args[:not].to_h if args[:not]
    if iteration_params_not_mutually_exclusive?(args) ||
    iteration_params_not_mutually_exclusive?(args.fetch(:not, {}))
    arg_str = mutually_exclusive_iteration_args.map { |x| x.to_s.camelize(:lower) }.join(', ')
    raise ::Gitlab::Graphql::Errors::ArgumentError,
    "only one of [#{arg_str}] arguments is allowed at the same time."
    end
    super
    end
    private
    override :prepare_finder_params
    def prepare_finder_params(args)
    args[:not] = args[:not].to_h if args[:not]
    args[:iteration_id] = iteration_ids_from_args(args) if args[:iteration_id].present?
    args[:not][:iteration_id] = iteration_ids_from_args(args[:not]) if args.dig(:not, :iteration_id).present?
    prepare_iteration_wildcard_params(args)
    prepare_health_status_params(args)
    super
    end
    # Originally accepted a raw model id. Now accept a gid, but allow a raw id
    # for backward compatibility
    def iteration_ids_from_args(args)
    args[:iteration_id].map do |id|
    ::GitlabSchema.parse_gid(id, expected_type: ::Iteration).model_id
    rescue ::Gitlab::Graphql::Errors::ArgumentError
    id
    end
    end
    def prepare_iteration_wildcard_params(args)
    args[:iteration_id] = args.delete(:iteration_wildcard_id) if args[:iteration_wildcard_id].present?
    return unless args.dig(:not, :iteration_wildcard_id).present?
    args[:not][:iteration_id] = args[:not].delete(:iteration_wildcard_id)
    end
    def prepare_health_status_params(args)
    # health_status argument is deprecated, use health_status_filter instead
    args[:health_status] = args.delete(:health_status_filter) if args[:health_status_filter].present?
    end
    def iteration_params_not_mutually_exclusive?(args)
    args.slice(*mutually_exclusive_iteration_args).compact.size > 1
    end
    def mutually_exclusive_iteration_args
    [:iteration_id, :iteration_wildcard_id]
    end
    end
    end
    end
    end
    # frozen_string_literal: true
    module EE
    module Issues
    module LookAheadPreloads
    extend ActiveSupport::Concern
    extend ::Gitlab::Utils::Override
    private
    override :preloads
    def preloads
    super.merge(
    {
    sla_due_at: [:issuable_sla],
    metric_images: [:metric_images]
    }
    )
    end
    end
    end
    end
    # frozen_string_literal: true
    require 'spec_helper'
    RSpec.describe 'getting Issue counts by status' do
    include GraphqlHelpers
    let_it_be(:group) { create(:group) }
    let_it_be(:iteration) { create(:iteration, group: group) }
    let_it_be(:epic) { create(:epic, group: group) }
    let_it_be(:project) { create(:project, :repository, group: group) }
    let_it_be(:current_user) { create(:user) }
    let_it_be(:issue_opened1) { create(:issue, project: project, weight: 3, epic: epic) }
    let_it_be(:issue_opened2) { create(:issue, project: project, iteration: iteration) }
    let_it_be(:issue_closed) { create(:issue, :closed, project: project) }
    let_it_be(:other_project_issue) { create(:issue) }
    let(:params) { {} }
    let(:fields) do
    <<~QUERY
    #{all_graphql_fields_for('IssueStatusCountsType'.classify)}
    QUERY
    end
    let(:query) do
    graphql_query_for(
    'project',
    { 'fullPath' => project.full_path },
    query_graphql_field('issueStatusCounts', params, fields)
    )
    end
    context 'with issue count data' do
    let(:issue_counts) { graphql_data.dig('project', 'issueStatusCounts') }
    context 'with project permissions' do
    before do
    project.add_developer(current_user)
    post_graphql(query, current_user: current_user)
    end
    it_behaves_like 'a working graphql query'
    it 'returns the correct counts for each status' do
    expect(issue_counts).to eq(
    'all' => 3,
    'opened' => 2,
    'closed' => 1
    )
    end
    context 'when filters are provided' do
    context 'when filtering by weight' do
    let(:params) { { 'weight' => '3' } }
    it 'returns the correct counts for each status' do
    expect(issue_counts).to eq(
    'all' => 1,
    'opened' => 1,
    'closed' => 0
    )
    end
    end
    context 'when filtering by iteration' do
    let(:params) { { 'iterationId' => iteration.to_gid.to_s } }
    it 'returns the correct counts for each status' do
    expect(issue_counts).to eq(
    'all' => 1,
    'opened' => 1,
    'closed' => 0
    )
    end
    end
    context 'when filtering by epic' do
    let(:params) { { 'epicId' => epic.id.to_s, 'includeSubepics' => true } }
    it 'returns the correct counts for each status' do
    expect(issue_counts).to eq(
    'all' => 1,
    'opened' => 1,
    'closed' => 0
    )
    end
    end
    context 'when filtering by health status' do
    let(:params) { { 'healthStatusFilter' => :ANY } }
    it 'returns the correct counts for each status' do
    expect(issue_counts).to eq(
    'all' => 0,
    'opened' => 0,
    'closed' => 0
    )
    end
    end
    end
    end
    end
    end
    0% Loading or .
    You are about to add 0 people to the discussion. Proceed with caution.
    Finish editing this message first!
    Please register or to comment