Commit 15f790a9 authored by Sean McGivern's avatar Sean McGivern

Merge branch '31914-graphql-todos-query-pd' into 'master'

Query todos with GraphQL

See merge request gitlab-org/gitlab!18218
parents 3de8edb5 13651c40
...@@ -33,6 +33,8 @@ class TodosFinder ...@@ -33,6 +33,8 @@ class TodosFinder
end end
def execute def execute
return Todo.none if current_user.nil?
items = current_user.todos items = current_user.todos
items = by_action_id(items) items = by_action_id(items)
items = by_action(items) items = by_action(items)
......
# frozen_string_literal: true
module Resolvers
class TodoResolver < BaseResolver
type Types::TodoType, null: true
alias_method :user, :object
argument :action, [Types::TodoActionEnum],
required: false,
description: 'The action to be filtered'
argument :author_id, [GraphQL::ID_TYPE],
required: false,
description: 'The ID of an author'
argument :project_id, [GraphQL::ID_TYPE],
required: false,
description: 'The ID of a project'
argument :group_id, [GraphQL::ID_TYPE],
required: false,
description: 'The ID of a group'
argument :state, [Types::TodoStateEnum],
required: false,
description: 'The state of the todo'
argument :type, [Types::TodoTargetEnum],
required: false,
description: 'The type of the todo'
def resolve(**args)
return Todo.none if user != context[:current_user]
TodosFinder.new(user, todo_finder_params(args)).execute
end
private
# TODO: Support multiple queries for e.g. state and type on TodosFinder:
#
# https://gitlab.com/gitlab-org/gitlab/merge_requests/18487
# https://gitlab.com/gitlab-org/gitlab/merge_requests/18518
#
# As soon as these MR's are merged, we can refactor this to query by
# multiple contents.
#
def todo_finder_params(args)
{
state: first_state(args),
type: first_type(args),
group_id: first_group_id(args),
author_id: first_author_id(args),
action_id: first_action(args),
project_id: first_project(args)
}
end
def first_project(args)
first_query_field(args, :project_id)
end
def first_action(args)
first_query_field(args, :action)
end
def first_author_id(args)
first_query_field(args, :author_id)
end
def first_group_id(args)
first_query_field(args, :group_id)
end
def first_state(args)
first_query_field(args, :state)
end
def first_type(args)
first_query_field(args, :type)
end
def first_query_field(query, field)
return unless query.key?(field)
query[field].first if query[field].respond_to?(:first)
end
end
end
...@@ -14,6 +14,11 @@ module Types ...@@ -14,6 +14,11 @@ module Types
resolver: Resolvers::GroupResolver, resolver: Resolvers::GroupResolver,
description: "Find a group" description: "Find a group"
field :current_user, Types::UserType,
null: true,
resolve: -> (_obj, _args, context) { context[:current_user] },
description: "Get information about current user"
field :namespace, Types::NamespaceType, field :namespace, Types::NamespaceType,
null: true, null: true,
resolver: Resolvers::NamespaceResolver, resolver: Resolvers::NamespaceResolver,
......
# frozen_string_literal: true
module Types
class TodoActionEnum < BaseEnum
value 'assigned', value: 1
value 'mentioned', value: 2
value 'build_failed', value: 3
value 'marked', value: 4
value 'approval_required', value: 5
value 'unmergeable', value: 6
value 'directly_addressed', value: 7
end
end
# frozen_string_literal: true
module Types
class TodoStateEnum < BaseEnum
value 'pending'
value 'done'
end
end
# frozen_string_literal: true
module Types
class TodoTargetEnum < BaseEnum
value 'Issue'
value 'MergeRequest'
value 'Epic'
end
end
# frozen_string_literal: true
module Types
class TodoType < BaseObject
graphql_name 'Todo'
description 'Representing a todo entry'
present_using TodoPresenter
authorize :read_todo
field :id, GraphQL::ID_TYPE,
description: 'Id of the todo',
null: false
field :project, Types::ProjectType,
description: 'The project this todo is associated with',
null: true,
authorize: :read_project,
resolve: -> (todo, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, todo.project_id).find }
field :group, Types::GroupType,
description: 'Group this todo is associated with',
null: true,
authorize: :read_group,
resolve: -> (todo, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, todo.group_id).find }
field :author, Types::UserType,
description: 'The owner of this todo',
null: false,
resolve: -> (todo, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, todo.author_id).find }
field :action, Types::TodoActionEnum,
description: 'Action of the todo',
null: false
field :target_type, Types::TodoTargetEnum,
description: 'Target type of the todo',
null: false
field :body, GraphQL::STRING_TYPE,
description: 'Body of the todo',
null: false
field :state, Types::TodoStateEnum,
description: 'State of the todo',
null: false
field :created_at, Types::TimeType,
description: 'Timestamp this todo was created',
null: false
end
end
...@@ -12,5 +12,8 @@ module Types ...@@ -12,5 +12,8 @@ module Types
field :username, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions field :username, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
field :avatar_url, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions field :avatar_url, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
field :web_url, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions field :web_url, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
field :todos, Types::TodoType.connection_type, null: false,
resolver: Resolvers::TodoResolver,
description: 'Todos of this user'
end end
end end
# frozen_string_literal: true
class TodoPolicy < BasePolicy
desc 'User can only read own todos'
condition(:own_todo) do
@user && @subject.user_id == @user.id
end
rule { own_todo }.enable :read_todo
end
# frozen_string_literal: true
class TodoPresenter < Gitlab::View::Presenter::Delegated
include GlobalID::Identification
presents :todo
end
---
title: Add ability to query todos using GraphQL
merge_request: 18218
author:
type: added
...@@ -710,6 +710,20 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph ...@@ -710,6 +710,20 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `count` | Int! | | | `count` | Int! | |
| `completedCount` | Int! | | | `completedCount` | Int! | |
### Todo
| Name | Type | Description |
| --- | ---- | ---------- |
| `id` | ID! | Id of the todo |
| `project` | Project | The project this todo is associated with |
| `group` | Group | Group this todo is associated with |
| `author` | User! | The owner of this todo |
| `action` | TodoActionEnum! | Action of the todo |
| `targetType` | TodoTargetEnum! | Target type of the todo |
| `body` | String! | Body of the todo |
| `state` | TodoStateEnum! | State of the todo |
| `createdAt` | Time! | Timestamp this todo was created |
### ToggleAwardEmojiPayload ### ToggleAwardEmojiPayload
| Name | Type | Description | | Name | Type | Description |
......
...@@ -16,6 +16,10 @@ describe TodosFinder do ...@@ -16,6 +16,10 @@ describe TodosFinder do
end end
describe '#execute' do describe '#execute' do
it 'returns no todos if user is nil' do
expect(described_class.new(nil, {}).execute).to be_empty
end
context 'filtering' do context 'filtering' do
let!(:todo1) { create(:todo, user: user, project: project, target: issue) } let!(:todo1) { create(:todo, user: user, project: project, target: issue) }
let!(:todo2) { create(:todo, user: user, group: group, target: merge_request) } let!(:todo2) { create(:todo, user: user, group: group, target: merge_request) }
......
# frozen_string_literal: true
require 'spec_helper'
describe Resolvers::TodoResolver do
include GraphqlHelpers
describe '#resolve' do
let_it_be(:current_user) { create(:user) }
let_it_be(:user) { create(:user) }
let_it_be(:author1) { create(:user) }
let_it_be(:author2) { create(:user) }
let_it_be(:todo1) { create(:todo, user: user, target_type: 'MergeRequest', state: :pending, action: Todo::MENTIONED, author: author1) }
let_it_be(:todo2) { create(:todo, user: user, state: :done, action: Todo::ASSIGNED, author: author2) }
let_it_be(:todo3) { create(:todo, user: user, state: :pending, action: Todo::ASSIGNED, author: author1) }
it 'calls TodosFinder' do
expect_next_instance_of(TodosFinder) do |finder|
expect(finder).to receive(:execute)
end
resolve_todos
end
context 'when using no filter' do
it 'returns expected todos' do
todos = resolve(described_class, obj: user, args: {}, ctx: { current_user: user })
expect(todos).to contain_exactly(todo1, todo3)
end
end
context 'when using filters' do
# TODO These can be removed as soon as we support filtering for multiple field contents for todos
it 'just uses the first state' do
todos = resolve(described_class, obj: user, args: { state: [:done, :pending] }, ctx: { current_user: user })
expect(todos).to contain_exactly(todo2)
end
it 'just uses the first action' do
todos = resolve(described_class, obj: user, args: { action: [Todo::MENTIONED, Todo::ASSIGNED] }, ctx: { current_user: user })
expect(todos).to contain_exactly(todo1)
end
it 'just uses the first author id' do
# We need a pending todo for now because of TodosFinder's state query
todo4 = create(:todo, user: user, state: :pending, action: Todo::ASSIGNED, author: author2)
todos = resolve(described_class, obj: user, args: { author_id: [author2.id, author1.id] }, ctx: { current_user: user })
expect(todos).to contain_exactly(todo4)
end
it 'just uses the first project id' do
project1 = create(:project)
project2 = create(:project)
create(:todo, project: project1, user: user, state: :pending, action: Todo::ASSIGNED, author: author1)
todo5 = create(:todo, project: project2, user: user, state: :pending, action: Todo::ASSIGNED, author: author1)
todos = resolve(described_class, obj: user, args: { project_id: [project2.id, project1.id] }, ctx: { current_user: user })
expect(todos).to contain_exactly(todo5)
end
it 'just uses the first group id' do
group1 = create(:group)
group2 = create(:group)
group1.add_developer(user)
group2.add_developer(user)
create(:todo, group: group1, user: user, state: :pending, action: Todo::ASSIGNED, author: author1)
todo5 = create(:todo, group: group2, user: user, state: :pending, action: Todo::ASSIGNED, author: author1)
todos = resolve(described_class, obj: user, args: { group_id: [group2.id, group1.id] }, ctx: { current_user: user })
expect(todos).to contain_exactly(todo5)
end
it 'just uses the first target' do
todos = resolve(described_class, obj: user, args: { type: %w[Issue MergeRequest] }, ctx: { current_user: user })
# Just todo3 because todo2 is in state "done"
expect(todos).to contain_exactly(todo3)
end
end
context 'when no user is provided' do
it 'returns no todos' do
todos = resolve(described_class, obj: nil, args: {}, ctx: { current_user: current_user })
expect(todos).to be_empty
end
end
context 'when provided user is not current user' do
it 'returns no todos' do
todos = resolve(described_class, obj: user, args: {}, ctx: { current_user: current_user })
expect(todos).to be_empty
end
end
end
def resolve_todos(args = {}, context = { current_user: current_user })
resolve(described_class, obj: current_user, args: args, ctx: context)
end
end
...@@ -7,7 +7,7 @@ describe GitlabSchema.types['Query'] do ...@@ -7,7 +7,7 @@ describe GitlabSchema.types['Query'] do
expect(described_class.graphql_name).to eq('Query') expect(described_class.graphql_name).to eq('Query')
end end
it { is_expected.to have_graphql_fields(:project, :namespace, :group, :echo, :metadata) } it { is_expected.to have_graphql_fields(:project, :namespace, :group, :echo, :metadata, :current_user) }
describe 'namespace field' do describe 'namespace field' do
subject { described_class.fields['namespace'] } subject { described_class.fields['namespace'] }
......
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['Todo'] do
it 'has the correct fields' do
expected_fields = [:id, :project, :group, :author, :action, :target_type, :body, :state, :created_at]
is_expected.to have_graphql_fields(*expected_fields)
end
it { expect(described_class).to require_graphql_authorizations(:read_todo) }
end
# frozen_string_literal: true
require 'spec_helper'
describe TodoPolicy do
let_it_be(:author) { create(:user) }
let_it_be(:user1) { create(:user) }
let_it_be(:user2) { create(:user) }
let_it_be(:user3) { create(:user) }
let_it_be(:todo1) { create(:todo, author: author, user: user1) }
let_it_be(:todo2) { create(:todo, author: author, user: user2) }
let_it_be(:todo3) { create(:todo, author: author, user: user2) }
let_it_be(:todo4) { create(:todo, author: author, user: user3) }
def permissions(user, todo)
described_class.new(user, todo)
end
describe 'own_todo' do
it 'allows owners to access their own todos' do
[
[user1, todo1],
[user2, todo2],
[user2, todo3],
[user3, todo4]
].each do |user, todo|
expect(permissions(user, todo)).to be_allowed(:read_todo)
end
end
it 'does not allow users to access todos of other users' do
[
[user1, todo2],
[user1, todo3],
[user2, todo1],
[user2, todo4],
[user3, todo1],
[user3, todo2],
[user3, todo3]
].each do |user, todo|
expect(permissions(user, todo)).to be_disallowed(:read_todo)
end
end
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