Commit 13651c40 authored by Patrick Derichs's avatar Patrick Derichs

Add possibility to query todos by graphql

Remove explicit id field and changed authorization
types

Add todo to query type

Add todos field to user and add currentuser query field

Add changelog entry

Add TodoTargetEnum, using enum types in TodoType

Fix typo in description

Add missing connection_type to also support
querying for "first: n"

Fix "Do not use '_' for arguments that are used"

Add field id again

Remove unnecessary rubocop disable comments

Add values to action enum entries

Query for state and type

Use presenter for todos

Add supported fields for API

Fix action query
parent 92c346f6
...@@ -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