Commit 45fbc742 authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Add GraphQL subscription for assignee updates

This will be used for updating sidebar assignees in real time
parent 7955bc37
......@@ -18,6 +18,12 @@ module Subscriptions
private
def unauthorized!
unsubscribe if context.query.subscription_update?
raise GraphQL::ExecutionError, 'Unauthorized subscription'
end
def current_user
context[:current_user]
end
......
# frozen_string_literal: true
module Subscriptions
class IssuableUpdated < BaseSubscription
include Gitlab::Graphql::Laziness
payload_type Types::IssuableType
argument :issuable_id, Types::GlobalIDType[Issuable],
required: true,
description: 'ID of the issuable.'
def subscribe(issuable_id:)
nil
end
def authorized?(issuable_id:)
# TODO: remove this check when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
raise Gitlab::Graphql::Errors::ArgumentError, 'Invalid IssuableID' unless issuable_id.is_a?(GlobalID)
issuable = force(GitlabSchema.find_by_gid(issuable_id))
unauthorized! unless issuable && Ability.allowed?(current_user, :"read_#{issuable.to_ability_name}", issuable)
true
end
end
end
# frozen_string_literal: true
module Types
class IssuableType < BaseUnion
graphql_name 'Issuable'
description 'Represents an issuable.'
possible_types Types::IssueType, Types::MergeRequestType
def self.resolve_type(object, context)
case object
when Issue
Types::IssueType
when MergeRequest
Types::MergeRequestType
else
raise 'Unsupported issuable type'
end
end
end
end
Types::IssuableType.prepend_if_ee('::EE::Types::IssuableType')
# frozen_string_literal: true
module Types
class SubscriptionType < ::Types::BaseObject
graphql_name 'Subscription'
field :issuable_assignees_updated, subscription: Subscriptions::IssuableUpdated, null: true,
description: 'Triggered when the assignees of an issuable are updated.'
end
end
......@@ -11795,6 +11795,22 @@ Represents the Geo sync and verification state of a snippet repository.
| <a id="submoduletype"></a>`type` | [`EntryType!`](#entrytype) | Type of tree entry. |
| <a id="submoduleweburl"></a>`webUrl` | [`String`](#string) | Web URL for the sub-module. |
### `Subscription`
#### Fields with arguments
##### `Subscription.issuableAssigneesUpdated`
Triggered when the assignees of an issuable are updated.
Returns [`Issuable`](#issuable).
###### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="subscriptionissuableassigneesupdatedissuableid"></a>`issuableId` | [`IssuableID!`](#issuableid) | ID of the issuable. |
### `TaskCompletionStatus`
Completion status of tasks.
......@@ -14158,6 +14174,12 @@ An example `IncidentManagementOncallRotationID` is: `"gid://gitlab/IncidentManag
Represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.
### `IssuableID`
A `IssuableID` is a global ID. It is encoded as a string.
An example `IssuableID` is: `"gid://gitlab/Issuable/1"`.
### `IssueID`
A `IssueID` is a global ID. It is encoded as a string.
......@@ -14367,6 +14389,16 @@ abstract types.
### Unions
#### `Issuable`
Represents an issuable.
One of:
- [`Epic`](#epic)
- [`Issue`](#issue)
- [`MergeRequest`](#mergerequest)
#### `PackageMetadata`
Represents metadata associated with a Package.
......
# frozen_string_literal: true
module EE
module Types
module IssuableType
extend ActiveSupport::Concern
prepended do
possible_types ::Types::EpicType
end
class_methods do
def resolve_type(object, context)
case object
when ::Epic
::Types::EpicType
else
super
end
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['Issuable'] do
it 'returns possible types' do
expect(described_class.possible_types).to include(Types::EpicType)
end
describe '.resolve_type' do
it 'resolves epics' do
expect(described_class.resolve_type(build(:epic), {})).to eq(Types::EpicType)
end
end
end
......@@ -8,7 +8,7 @@ module RuboCop
'https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#type-authorization'
# We want to exclude our own basetypes and scalars
ALLOWED_TYPES = %w[BaseEnum BaseScalar BasePermissionType MutationType
ALLOWED_TYPES = %w[BaseEnum BaseScalar BasePermissionType MutationType SubscriptionType
QueryType GraphQL::Schema BaseUnion BaseInputObject].freeze
def_node_search :authorize?, <<~PATTERN
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Subscriptions::IssuableUpdated do
include GraphqlHelpers
it { expect(described_class).to have_graphql_arguments(:issuable_id) }
it { expect(described_class.payload_type).to eq(Types::IssuableType) }
describe '#resolve' do
let_it_be(:unauthorized_user) { create(:user) }
let_it_be(:issue) { create(:issue) }
let(:current_user) { issue.author }
let(:issuable_id) { issue.to_gid }
subject { resolver.resolve_with_support(issuable_id: issuable_id) }
context 'initial subscription' do
let(:resolver) { resolver_instance(described_class, ctx: { current_user: current_user }, subscription_update: false) }
it 'returns nil' do
expect(subject).to eq(nil)
end
context 'when user is unauthorized' do
let(:current_user) { unauthorized_user }
it 'raises an exception' do
expect { subject }.to raise_error(GraphQL::ExecutionError)
end
end
context 'when issue does not exist' do
let(:issuable_id) { GlobalID.parse("gid://gitlab/Issue/#{non_existing_record_id}") }
it 'raises an exception' do
expect { subject }.to raise_error(GraphQL::ExecutionError)
end
end
context 'when a GraphQL::ID_TYPE is provided' do
let(:issuable_id) { issue.to_gid.to_s }
it 'raises an exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
end
end
end
context 'subscription updates' do
let(:resolver) { resolver_instance(described_class, obj: issue, ctx: { current_user: current_user }, subscription_update: true) }
it 'returns the resolved object' do
expect(subject).to eq(issue)
end
context 'when user is unauthorized' do
let(:current_user) { unauthorized_user }
it 'unsubscribes the user' do
expect { subject }.to throw_symbol(:graphql_subscription_unsubscribed)
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['Issuable'] do
it 'returns possible types' do
expect(described_class.possible_types).to include(Types::IssueType, Types::MergeRequestType)
end
describe '.resolve_type' do
it 'resolves issues' do
expect(described_class.resolve_type(build(:issue), {})).to eq(Types::IssueType)
end
it 'resolves merge requests' do
expect(described_class.resolve_type(build(:merge_request), {})).to eq(Types::MergeRequestType)
end
it 'raises an error for invalid types' do
expect { described_class.resolve_type(build(:user), {}) }.to raise_error 'Unsupported issuable type'
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['Subscription'] do
it 'has the expected fields' do
expected_fields = %i[
issuable_assignees_updated
]
expect(described_class).to have_graphql_fields(*expected_fields).only
end
end
......@@ -142,9 +142,9 @@ module GraphqlHelpers
Class.new(::Types::BaseObject) { graphql_name name }
end
def resolver_instance(resolver_class, obj: nil, ctx: {}, field: nil, schema: GitlabSchema)
def resolver_instance(resolver_class, obj: nil, ctx: {}, field: nil, schema: GitlabSchema, subscription_update: false)
if ctx.is_a?(Hash)
q = double('Query', schema: schema)
q = double('Query', schema: schema, subscription_update?: subscription_update)
ctx = GraphQL::Query::Context.new(query: q, object: obj, values: ctx)
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment