Commit 4eedc07f authored by Max Woolf's avatar Max Woolf

Adds groupMembership and projectMembership to GraphQL API

This commit adds the ability to traverse
the graph from a User to its Projects or
Groups via a groupMembership or projectMember
object.
parent f030db7c
# frozen_string_literal: true
module Types
class AccessLevelEnum < BaseEnum
graphql_name 'AccessLevelEnum'
description 'Access level to a resource'
value 'NO_ACCESS', value: Gitlab::Access::NO_ACCESS
value 'GUEST', value: Gitlab::Access::GUEST
value 'REPORTER', value: Gitlab::Access::REPORTER
value 'DEVELOPER', value: Gitlab::Access::DEVELOPER
value 'MAINTAINER', value: Gitlab::Access::MAINTAINER
value 'OWNER', value: Gitlab::Access::OWNER
end
end
# frozen_string_literal: true
# rubocop:disable Graphql/AuthorizeTypes
module Types
class AccessLevelType < Types::BaseObject
graphql_name 'AccessLevel'
description 'Represents the access level of a relationship between a User and object that it is related to'
field :integer_value, GraphQL::INT_TYPE, null: true,
description: 'Integer representation of access level',
method: :to_i
field :string_value, Types::AccessLevelEnum, null: true,
description: 'String representation of access level',
method: :to_i
end
end
# frozen_string_literal: true
module Types
class GroupMemberType < BaseObject
expose_permissions Types::PermissionTypes::Group
authorize :read_group
implements MemberInterface
graphql_name 'GroupMember'
description 'Represents a Group Member'
field :group, Types::GroupType, null: true,
description: 'Group that a User is a member of',
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, obj.source_id).find }
end
end
# frozen_string_literal: true
module Types
module MemberInterface
include BaseInterface
field :access_level, Types::AccessLevelType, null: true,
description: 'GitLab::Access level'
field :created_by, Types::UserType, null: true,
description: 'User that authorized membership'
field :created_at, Types::TimeType, null: true,
description: 'Date and time the membership was created'
field :updated_at, Types::TimeType, null: true,
description: 'Date and time the membership was last updated'
field :expires_at, Types::TimeType, null: true,
description: 'Date and time the membership expires'
end
end
......@@ -3,18 +3,23 @@
module Types
class ProjectMemberType < BaseObject
graphql_name 'ProjectMember'
description 'Member of a project'
description 'Represents a Project Member'
expose_permissions Types::PermissionTypes::Project
implements MemberInterface
authorize :read_project
field :id, GraphQL::ID_TYPE, null: false,
description: 'ID of the member'
field :access_level, GraphQL::INT_TYPE, null: false,
description: 'Access level of the member'
field :user, Types::UserType, null: false,
description: 'User that is associated with the member object',
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, obj.user_id).find }
field :project, Types::ProjectType, null: true,
description: 'Project that User is a member of',
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, obj.source_id).find }
end
end
......@@ -25,6 +25,12 @@ module Types
field :todos, Types::TodoType.connection_type, null: false,
resolver: Resolvers::TodoResolver,
description: 'Todos of the user'
field :group_memberships, Types::GroupMemberType.connection_type, null: true,
description: 'Group memberships of the user',
method: :group_members
field :project_memberships, Types::ProjectMemberType.connection_type, null: true,
description: 'Project memberships of the user',
method: :project_members
# Merge request field: MRs can be either authored or assigned:
field :authored_merge_requests, Types::MergeRequestType.connection_type, null: true,
......
---
title: Adds groupMembership and projectMembership to GraphQL API
merge_request: 33049
author:
type: added
"""
Represents the access level of a relationship between a User and object that it is related to
"""
type AccessLevel {
"""
Integer representation of access level
"""
integerValue: Int
"""
String representation of access level
"""
stringValue: AccessLevelEnum
}
"""
Access level to a resource
"""
enum AccessLevelEnum {
DEVELOPER
GUEST
MAINTAINER
NO_ACCESS
OWNER
REPORTER
}
"""
Autogenerated input type of AddAwardEmoji
"""
......@@ -4975,6 +5002,81 @@ type Group {
webUrl: String!
}
"""
Represents a Group Member
"""
type GroupMember implements MemberInterface {
"""
GitLab::Access level
"""
accessLevel: AccessLevel
"""
Date and time the membership was created
"""
createdAt: Time
"""
User that authorized membership
"""
createdBy: User
"""
Date and time the membership expires
"""
expiresAt: Time
"""
Group that a User is a member of
"""
group: Group
"""
Date and time the membership was last updated
"""
updatedAt: Time
"""
Permissions for the current user on the resource
"""
userPermissions: GroupPermissions!
}
"""
The connection type for GroupMember.
"""
type GroupMemberConnection {
"""
A list of edges.
"""
edges: [GroupMemberEdge]
"""
A list of nodes.
"""
nodes: [GroupMember]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type GroupMemberEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: GroupMember
}
type GroupPermissions {
"""
Indicates the user can perform `read_group` on this resource
......@@ -6101,6 +6203,33 @@ type MarkAsSpamSnippetPayload {
snippet: Snippet
}
interface MemberInterface {
"""
GitLab::Access level
"""
accessLevel: AccessLevel
"""
Date and time the membership was created
"""
createdAt: Time
"""
User that authorized membership
"""
createdBy: User
"""
Date and time the membership expires
"""
expiresAt: Time
"""
Date and time the membership was last updated
"""
updatedAt: Time
}
type MergeRequest implements Noteable {
"""
Indicates if members of the target project can push to the fork
......@@ -9049,23 +9178,53 @@ type ProjectEdge {
}
"""
Member of a project
Represents a Project Member
"""
type ProjectMember {
type ProjectMember implements MemberInterface {
"""
GitLab::Access level
"""
accessLevel: AccessLevel
"""
Date and time the membership was created
"""
createdAt: Time
"""
User that authorized membership
"""
createdBy: User
"""
Access level of the member
Date and time the membership expires
"""
accessLevel: Int!
expiresAt: Time
"""
ID of the member
"""
id: ID!
"""
Project that User is a member of
"""
project: Project
"""
Date and time the membership was last updated
"""
updatedAt: Time
"""
User that is associated with the member object
"""
user: User!
"""
Permissions for the current user on the resource
"""
userPermissions: ProjectPermissions!
}
"""
......@@ -12429,6 +12588,31 @@ type User {
"""
avatarUrl: String
"""
Group memberships of the user
"""
groupMemberships(
"""
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
): GroupMemberConnection
"""
ID of the user
"""
......@@ -12439,6 +12623,31 @@ type User {
"""
name: String!
"""
Project memberships of the user
"""
projectMemberships(
"""
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
): ProjectMemberConnection
"""
Snippets authored by the user
"""
......
......@@ -16,6 +16,15 @@ fields and methods on a model are available via GraphQL.
CAUTION: **Caution:**
Fields that are deprecated are marked with **{warning-solid}**.
## AccessLevel
Represents the access level of a relationship between a User and object that it is related to
| Name | Type | Description |
| --- | ---- | ---------- |
| `integerValue` | Int | Integer representation of access level |
| `stringValue` | AccessLevelEnum | String representation of access level |
## AddAwardEmojiPayload
Autogenerated return type of AddAwardEmoji
......@@ -723,6 +732,20 @@ Autogenerated return type of EpicTreeReorder
| `visibility` | String | Visibility of the namespace |
| `webUrl` | String! | Web URL of the group |
## GroupMember
Represents a Group Member
| Name | Type | Description |
| --- | ---- | ---------- |
| `accessLevel` | AccessLevel | GitLab::Access level |
| `createdAt` | Time | Date and time the membership was created |
| `createdBy` | User | User that authorized membership |
| `expiresAt` | Time | Date and time the membership expires |
| `group` | Group | Group that a User is a member of |
| `updatedAt` | Time | Date and time the membership was last updated |
| `userPermissions` | GroupPermissions! | Permissions for the current user on the resource |
## GroupPermissions
| Name | Type | Description |
......@@ -1260,13 +1283,19 @@ Information about pagination in a connection.
## ProjectMember
Member of a project
Represents a Project Member
| Name | Type | Description |
| --- | ---- | ---------- |
| `accessLevel` | Int! | Access level of the member |
| `accessLevel` | AccessLevel | GitLab::Access level |
| `createdAt` | Time | Date and time the membership was created |
| `createdBy` | User | User that authorized membership |
| `expiresAt` | Time | Date and time the membership expires |
| `id` | ID! | ID of the member |
| `project` | Project | Project that User is a member of |
| `updatedAt` | Time | Date and time the membership was last updated |
| `user` | User! | User that is associated with the member object |
| `userPermissions` | ProjectPermissions! | Permissions for the current user on the resource |
## ProjectPermissions
......
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['AccessLevelEnum'] do
specify { expect(described_class.graphql_name).to eq('AccessLevelEnum') }
it 'exposes all the existing access levels' do
expect(described_class.values.keys).to match_array(%w[NO_ACCESS GUEST REPORTER DEVELOPER MAINTAINER OWNER])
end
end
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['AccessLevel'] do
specify { expect(described_class.graphql_name).to eq('AccessLevel') }
specify { expect(described_class).to require_graphql_authorizations(nil) }
it 'has expected fields' do
expected_fields = [:integer_value, :string_value]
expect(described_class).to have_graphql_fields(*expected_fields)
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Types::GroupMemberType do
specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Group) }
specify { expect(described_class.graphql_name).to eq('GroupMember') }
specify { expect(described_class).to require_graphql_authorizations(:read_group) }
it 'has the expected fields' do
expected_fields = %w[
access_level created_by created_at updated_at expires_at group
]
expect(described_class).to include_graphql_fields(*expected_fields)
end
end
......@@ -2,14 +2,18 @@
require 'spec_helper'
describe GitlabSchema.types['ProjectMember'] do
describe Types::ProjectMemberType do
specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Project) }
specify { expect(described_class.graphql_name).to eq('ProjectMember') }
specify { expect(described_class).to require_graphql_authorizations(:read_project) }
it 'has the expected fields' do
expected_fields = %w[id accessLevel user]
expected_fields = %w[
access_level created_by created_at updated_at expires_at project user
]
expect(described_class).to have_graphql_fields(*expected_fields)
expect(described_class).to include_graphql_fields(*expected_fields)
end
specify { expect(described_class).to require_graphql_authorizations(:read_project) }
end
......@@ -9,8 +9,19 @@ describe GitlabSchema.types['User'] do
it 'has the expected fields' do
expected_fields = %w[
id user_permissions snippets name username avatarUrl webUrl todos state
authoredMergeRequests assignedMergeRequests
id
user_permissions
snippets
name
username
avatarUrl
webUrl
todos
state
authoredMergeRequests
assignedMergeRequests
groupMemberships
projectMemberships
]
expect(described_class).to have_graphql_fields(*expected_fields)
......
# frozen_string_literal: true
require 'spec_helper'
describe 'GroupMember' do
include GraphqlHelpers
let_it_be(:member) { create(:group_member, :developer) }
let_it_be(:fields) do
<<~HEREDOC
nodes {
accessLevel {
integerValue
stringValue
}
group {
id
}
}
HEREDOC
end
let_it_be(:query) do
graphql_query_for('user', { id: member.user.to_global_id.to_s }, query_graphql_field("groupMemberships", {}, fields))
end
before do
post_graphql(query, current_user: member.user)
end
it_behaves_like 'a working graphql query'
it_behaves_like 'a working membership object query'
end
# frozen_string_literal: true
require 'spec_helper'
describe 'ProjectMember' do
include GraphqlHelpers
let_it_be(:member) { create(:project_member, :developer) }
let_it_be(:fields) do
<<~HEREDOC
nodes {
accessLevel {
integerValue
stringValue
}
project {
id
}
}
HEREDOC
end
let_it_be(:query) do
graphql_query_for('user', { id: member.user.to_global_id.to_s }, query_graphql_field("projectMemberships", {}, fields))
end
before do
post_graphql(query, current_user: member.user)
end
it_behaves_like 'a working graphql query'
it_behaves_like 'a working membership object query'
end
# frozen_string_literal: true
RSpec.shared_examples 'a working membership object query' do |model_option|
let_it_be(:member_source) { member.source }
let_it_be(:member_source_type) { member_source.class.to_s.downcase }
it 'contains edge to expected project' do
expect(
graphql_data.dig('user', "#{member_source_type}Memberships", 'nodes', 0, member_source_type, 'id')
).to eq(member.send(member_source_type).to_global_id.to_s)
end
it 'contains correct access level' do
expect(
graphql_data.dig('user', "#{member_source_type}Memberships", 'nodes', 0, 'accessLevel', 'integerValue')
).to eq(30)
expect(
graphql_data.dig('user', "#{member_source_type}Memberships", 'nodes', 0, 'accessLevel', 'stringValue')
).to eq('DEVELOPER')
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