Commit 8d5d9384 authored by George Koltsov's avatar George Koltsov

Add Event type to GraphQL and events to epics

  - Add EventType to GraphQL
  - Add events connection to epics
parent 2425e9de
# frozen_string_literal: true
module Types
class EventActionEnum < BaseEnum
graphql_name 'EventAction'
description 'Event action'
::Event.actions.keys.each do |target_type|
value target_type.upcase, value: target_type, description: "#{target_type.titleize} action"
end
end
end
# frozen_string_literal: true
module Types
class EventType < BaseObject
graphql_name 'Event'
description 'Representing an event'
present_using EventPresenter
authorize :read_event
field :id, GraphQL::ID_TYPE,
description: 'ID of the event.',
null: false
field :author, Types::UserType,
description: 'Author of this event.',
null: false
field :action, Types::EventActionEnum,
description: 'Action of the event.',
null: false
field :created_at, Types::TimeType,
description: 'When this event was created.',
null: false
field :updated_at, Types::TimeType,
description: 'When this event was updated.',
null: false
def author
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.author_id).find
end
end
end
# frozen_string_literal: true
module Types
module EventableType
include Types::BaseInterface
field :events, Types::EventType.connection_type, null: true, description: 'A list of events associated with the object.'
end
end
# frozen_string_literal: true
class EventPolicy < BasePolicy # rubocop:disable Gitlab/NamespacedClass
condition(:visible_to_user) do
subject.visible_to_user?(user)
end
rule { visible_to_user }.enable :read_event
end
...@@ -1639,7 +1639,7 @@ type BoardEdge { ...@@ -1639,7 +1639,7 @@ type BoardEdge {
""" """
Represents an epic on an issue board Represents an epic on an issue board
""" """
type BoardEpic implements CurrentUserTodos & Noteable { type BoardEpic implements CurrentUserTodos & Eventable & Noteable {
""" """
Author of the epic. Author of the epic.
""" """
...@@ -1878,6 +1878,31 @@ type BoardEpic implements CurrentUserTodos & Noteable { ...@@ -1878,6 +1878,31 @@ type BoardEpic implements CurrentUserTodos & Noteable {
""" """
dueDateIsFixed: Boolean dueDateIsFixed: Boolean
"""
A list of events associated with the object.
"""
events(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): EventConnection
""" """
Group to which the epic belongs. Group to which the epic belongs.
""" """
...@@ -8682,7 +8707,7 @@ type EnvironmentsCanaryIngressUpdatePayload { ...@@ -8682,7 +8707,7 @@ type EnvironmentsCanaryIngressUpdatePayload {
""" """
Represents an epic Represents an epic
""" """
type Epic implements CurrentUserTodos & Noteable { type Epic implements CurrentUserTodos & Eventable & Noteable {
""" """
Author of the epic. Author of the epic.
""" """
...@@ -8921,6 +8946,31 @@ type Epic implements CurrentUserTodos & Noteable { ...@@ -8921,6 +8946,31 @@ type Epic implements CurrentUserTodos & Noteable {
""" """
dueDateIsFixed: Boolean dueDateIsFixed: Boolean
"""
A list of events associated with the object.
"""
events(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): EventConnection
""" """
Group to which the epic belongs. Group to which the epic belongs.
""" """
...@@ -10251,6 +10301,168 @@ enum EpicWildcardId { ...@@ -10251,6 +10301,168 @@ enum EpicWildcardId {
NONE NONE
} }
"""
Representing an event
"""
type Event {
"""
Action of the event.
"""
action: EventAction!
"""
Author of this event.
"""
author: User!
"""
When this event was created.
"""
createdAt: Time!
"""
ID of the event.
"""
id: ID!
"""
When this event was updated.
"""
updatedAt: Time!
}
"""
Event action
"""
enum EventAction {
"""
Approved action
"""
APPROVED
"""
Archived action
"""
ARCHIVED
"""
Closed action
"""
CLOSED
"""
Commented action
"""
COMMENTED
"""
Created action
"""
CREATED
"""
Destroyed action
"""
DESTROYED
"""
Expired action
"""
EXPIRED
"""
Joined action
"""
JOINED
"""
Left action
"""
LEFT
"""
Merged action
"""
MERGED
"""
Pushed action
"""
PUSHED
"""
Reopened action
"""
REOPENED
"""
Updated action
"""
UPDATED
}
"""
The connection type for Event.
"""
type EventConnection {
"""
A list of edges.
"""
edges: [EventEdge]
"""
A list of nodes.
"""
nodes: [Event]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type EventEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: Event
}
interface Eventable {
"""
A list of events associated with the object.
"""
events(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): EventConnection
}
""" """
Autogenerated input type of ExportRequirements Autogenerated input type of ExportRequirements
""" """
......
...@@ -4855,6 +4855,59 @@ ...@@ -4855,6 +4855,59 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "events",
"description": "A list of events associated with the object.",
"args": [
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "EventConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "group", "name": "group",
"description": "Group to which the epic belongs.", "description": "Group to which the epic belongs.",
...@@ -5516,6 +5569,11 @@ ...@@ -5516,6 +5569,11 @@
"kind": "INTERFACE", "kind": "INTERFACE",
"name": "CurrentUserTodos", "name": "CurrentUserTodos",
"ofType": null "ofType": null
},
{
"kind": "INTERFACE",
"name": "Eventable",
"ofType": null
} }
], ],
"enumValues": null, "enumValues": null,
...@@ -24566,6 +24624,59 @@ ...@@ -24566,6 +24624,59 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "events",
"description": "A list of events associated with the object.",
"args": [
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "EventConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "group", "name": "group",
"description": "Group to which the epic belongs.", "description": "Group to which the epic belongs.",
...@@ -25213,6 +25324,11 @@ ...@@ -25213,6 +25324,11 @@
"kind": "INTERFACE", "kind": "INTERFACE",
"name": "CurrentUserTodos", "name": "CurrentUserTodos",
"ofType": null "ofType": null
},
{
"kind": "INTERFACE",
"name": "Eventable",
"ofType": null
} }
], ],
"enumValues": null, "enumValues": null,
...@@ -28276,6 +28392,385 @@ ...@@ -28276,6 +28392,385 @@
], ],
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "Event",
"description": "Representing an event",
"fields": [
{
"name": "action",
"description": "Action of the event.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "EventAction",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "author",
"description": "Author of this event.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "User",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "createdAt",
"description": "When this event was created.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": "ID of the event.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updatedAt",
"description": "When this event was updated.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "EventAction",
"description": "Event action",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "CREATED",
"description": "Created action",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "UPDATED",
"description": "Updated action",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "CLOSED",
"description": "Closed action",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "REOPENED",
"description": "Reopened action",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "PUSHED",
"description": "Pushed action",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "COMMENTED",
"description": "Commented action",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "MERGED",
"description": "Merged action",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "JOINED",
"description": "Joined action",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "LEFT",
"description": "Left action",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "DESTROYED",
"description": "Destroyed action",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "EXPIRED",
"description": "Expired action",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "APPROVED",
"description": "Approved action",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "ARCHIVED",
"description": "Archived action",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "EventConnection",
"description": "The connection type for Event.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "EventEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Event",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pageInfo",
"description": "Information to aid in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "PageInfo",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "EventEdge",
"description": "An edge in a connection.",
"fields": [
{
"name": "cursor",
"description": "A cursor for use in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "node",
"description": "The item at the end of the edge.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Event",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INTERFACE",
"name": "Eventable",
"description": null,
"fields": [
{
"name": "events",
"description": "A list of events associated with the object.",
"args": [
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "EventConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": [
{
"kind": "OBJECT",
"name": "BoardEpic",
"ofType": null
},
{
"kind": "OBJECT",
"name": "Epic",
"ofType": null
}
]
},
{ {
"kind": "INPUT_OBJECT", "kind": "INPUT_OBJECT",
"name": "ExportRequirementsInput", "name": "ExportRequirementsInput",
...@@ -309,6 +309,7 @@ Represents an epic on an issue board. ...@@ -309,6 +309,7 @@ Represents an epic on an issue board.
| `dueDateFixed` | Time | Fixed due date of the epic. | | `dueDateFixed` | Time | Fixed due date of the epic. |
| `dueDateFromMilestones` | Time | Inherited due date of the epic from milestones. | | `dueDateFromMilestones` | Time | Inherited due date of the epic from milestones. |
| `dueDateIsFixed` | Boolean | Indicates if the due date has been manually set. | | `dueDateIsFixed` | Boolean | Indicates if the due date has been manually set. |
| `events` | EventConnection | A list of events associated with the object. |
| `group` | Group! | Group to which the epic belongs. | | `group` | Group! | Group to which the epic belongs. |
| `hasChildren` | Boolean! | Indicates if the epic has children. | | `hasChildren` | Boolean! | Indicates if the epic has children. |
| `hasIssues` | Boolean! | Indicates if the epic has direct issues. | | `hasIssues` | Boolean! | Indicates if the epic has direct issues. |
...@@ -1467,6 +1468,7 @@ Represents an epic. ...@@ -1467,6 +1468,7 @@ Represents an epic.
| `dueDateFixed` | Time | Fixed due date of the epic. | | `dueDateFixed` | Time | Fixed due date of the epic. |
| `dueDateFromMilestones` | Time | Inherited due date of the epic from milestones. | | `dueDateFromMilestones` | Time | Inherited due date of the epic from milestones. |
| `dueDateIsFixed` | Boolean | Indicates if the due date has been manually set. | | `dueDateIsFixed` | Boolean | Indicates if the due date has been manually set. |
| `events` | EventConnection | A list of events associated with the object. |
| `group` | Group! | Group to which the epic belongs. | | `group` | Group! | Group to which the epic belongs. |
| `hasChildren` | Boolean! | Indicates if the epic has children. | | `hasChildren` | Boolean! | Indicates if the epic has children. |
| `hasIssues` | Boolean! | Indicates if the epic has direct issues. | | `hasIssues` | Boolean! | Indicates if the epic has direct issues. |
...@@ -1678,6 +1680,18 @@ Autogenerated return type of EpicTreeReorder. ...@@ -1678,6 +1680,18 @@ Autogenerated return type of EpicTreeReorder.
| `clientMutationId` | String | A unique identifier for the client performing the mutation. | | `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. |
### Event
Representing an event.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `action` | EventAction! | Action of the event. |
| `author` | User! | Author of this event. |
| `createdAt` | Time! | When this event was created. |
| `id` | ID! | ID of the event. |
| `updatedAt` | Time! | When this event was updated. |
### ExportRequirementsPayload ### ExportRequirementsPayload
Autogenerated return type of ExportRequirements. Autogenerated return type of ExportRequirements.
...@@ -4879,6 +4893,26 @@ Epic ID wildcard values. ...@@ -4879,6 +4893,26 @@ Epic ID wildcard values.
| `ANY` | Any epic is assigned | | `ANY` | Any epic is assigned |
| `NONE` | No epic is assigned | | `NONE` | No epic is assigned |
### EventAction
Event action.
| Value | Description |
| ----- | ----------- |
| `APPROVED` | Approved action |
| `ARCHIVED` | Archived action |
| `CLOSED` | Closed action |
| `COMMENTED` | Commented action |
| `CREATED` | Created action |
| `DESTROYED` | Destroyed action |
| `EXPIRED` | Expired action |
| `JOINED` | Joined action |
| `LEFT` | Left action |
| `MERGED` | Merged action |
| `PUSHED` | Pushed action |
| `REOPENED` | Reopened action |
| `UPDATED` | Updated action |
### GroupMemberRelation ### GroupMemberRelation
Group member relation. Group member relation.
......
...@@ -78,7 +78,8 @@ module Resolvers ...@@ -78,7 +78,8 @@ module Resolvers
def preloads def preloads
{ {
parent: [:parent] parent: [:parent],
events: { events: [:target] }
} }
end end
......
...@@ -15,6 +15,7 @@ module Types ...@@ -15,6 +15,7 @@ module Types
implements(Types::Notes::NoteableType) implements(Types::Notes::NoteableType)
implements(Types::CurrentUserTodos) implements(Types::CurrentUserTodos)
implements(Types::EventableType)
field :id, GraphQL::ID_TYPE, null: false, field :id, GraphQL::ID_TYPE, null: false,
description: 'ID of the epic.' description: 'ID of the epic.'
......
---
title: Add Event type to GraphQL and events to epics
merge_request: 53152
author:
type: changed
...@@ -13,7 +13,7 @@ RSpec.describe GitlabSchema.types['Epic'] do ...@@ -13,7 +13,7 @@ RSpec.describe GitlabSchema.types['Epic'] do
notes discussions relative_position subscribed participants notes discussions relative_position subscribed participants
descendant_counts descendant_weight_sum upvotes downvotes descendant_counts descendant_weight_sum upvotes downvotes
user_notes_count user_discussions_count health_status current_user_todos user_notes_count user_discussions_count health_status current_user_todos
award_emoji award_emoji events
] ]
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe EventPolicy do
let(:user) { create(:user) }
let(:event) { create(:event, :created, target: create(:epic, group: group)) }
subject { described_class.new(user, event) }
before do
stub_licensed_features(epics: true)
end
context 'when the user cannot read the epic' do
let(:group) { create(:group, :private) }
it { expect_disallowed(:read_event) }
end
context 'when the user can read the epic' do
let(:group) { create(:group, :public) }
it { expect_allowed(:read_event) }
end
end
...@@ -83,6 +83,49 @@ RSpec.describe 'getting epics information' do ...@@ -83,6 +83,49 @@ RSpec.describe 'getting epics information' do
end end
end end
context 'query for epics with events' do
let_it_be(:epic) { create(:epic, group: group) }
it 'can lookahead to prevent N+1 queries' do
create_list(:event, 10, :created, target: epic, group: group)
control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
query_epics_with_events(1)
end.count
events = graphql_dig_at(graphql_data, :group, :epics, :nodes, :events, :nodes)
expect(events.count).to eq(1)
expect do
query_epics_with_events(10)
end.not_to exceed_all_query_limit(control_count)
data = graphql_data(fresh_response_data)
events = graphql_dig_at(data, :group, :epics, :nodes, :events, :nodes)
expect(events.count).to eq(10)
end
end
def query_epics_with_events(number)
epics_field = <<~NODE
epics {
nodes {
id
events(first: #{number}) {
nodes {
id
}
}
}
}
NODE
post_graphql(
graphql_query_for('group', { 'fullPath' => group.full_path }, epics_field),
current_user: user
)
end
def epics_query(group, field, value) def epics_query(group, field, value)
epics_query_by_hash(group, field => value) epics_query_by_hash(group, field => value)
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::EventType do
specify { expect(described_class.graphql_name).to eq('Event') }
specify { expect(described_class).to require_graphql_authorizations(:read_event) }
specify { expect(described_class).to have_graphql_fields(:id, :author, :action, :created_at, :updated_at) }
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::EventableType do
it 'exposes events field' do
expect(described_class).to have_graphql_fields(:events)
end
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