Commit 4905b977 authored by Patrick Bajao's avatar Patrick Bajao

Merge branch '2256-add-contacts-to-graphql' into 'master'

Add group contacts query to GraphQL

See merge request gitlab-org/gitlab!69510
parents daa48175 d7dd3671
# frozen_string_literal: true
module Types
module CustomerRelations
class ContactType < BaseObject
graphql_name 'CustomerRelationsContact'
authorize :read_contact
field :id,
GraphQL::Types::ID,
null: false,
description: 'Internal ID of the contact.'
field :organization, Types::CustomerRelations::OrganizationType,
null: true,
description: "Organization of the contact."
field :first_name,
GraphQL::Types::String,
null: false,
description: 'First name of the contact.'
field :last_name,
GraphQL::Types::String,
null: false,
description: 'Last name of the contact.'
field :phone,
GraphQL::Types::String,
null: true,
description: 'Phone number of the contact.'
field :email,
GraphQL::Types::String,
null: true,
description: 'Email address of the contact.'
field :description,
GraphQL::Types::String,
null: true,
description: 'Description or notes for the contact.'
field :created_at,
Types::TimeType,
null: false,
description: 'Timestamp the contact was created.'
field :updated_at,
Types::TimeType,
null: false,
description: 'Timestamp the contact was last updated.'
end
end
end
...@@ -29,12 +29,12 @@ module Types ...@@ -29,12 +29,12 @@ module Types
field :created_at, field :created_at,
Types::TimeType, Types::TimeType,
null: true, null: false,
description: 'Timestamp the organization was created.' description: 'Timestamp the organization was created.'
field :updated_at, field :updated_at,
Types::TimeType, Types::TimeType,
null: true, null: false,
description: 'Timestamp the organization was last updated.' description: 'Timestamp the organization was last updated.'
end end
end end
......
...@@ -199,6 +199,10 @@ module Types ...@@ -199,6 +199,10 @@ module Types
null: true, null: true,
description: "Find organizations of this group." description: "Find organizations of this group."
field :contacts, Types::CustomerRelations::ContactType.connection_type,
null: true,
description: "Find contacts of this group."
def avatar_url def avatar_url
object.avatar_url(only_path: false) object.avatar_url(only_path: false)
end end
......
...@@ -760,6 +760,10 @@ class Group < Namespace ...@@ -760,6 +760,10 @@ class Group < Namespace
::CustomerRelations::Organization.where(group_id: self.id) ::CustomerRelations::Organization.where(group_id: self.id)
end end
def contacts
::CustomerRelations::Contact.where(group_id: self.id)
end
private private
def max_member_access(user_ids) def max_member_access(user_ids)
......
# frozen_string_literal: true
module CustomerRelations
class ContactPolicy < BasePolicy
delegate { @subject.group }
end
end
...@@ -113,6 +113,7 @@ class GroupPolicy < BasePolicy ...@@ -113,6 +113,7 @@ class GroupPolicy < BasePolicy
enable :read_custom_emoji enable :read_custom_emoji
enable :read_counts enable :read_counts
enable :read_organization enable :read_organization
enable :read_contact
end end
rule { ~public_group & ~has_access }.prevent :read_counts rule { ~public_group & ~has_access }.prevent :read_counts
......
...@@ -5318,6 +5318,29 @@ The edge type for [`CustomEmoji`](#customemoji). ...@@ -5318,6 +5318,29 @@ The edge type for [`CustomEmoji`](#customemoji).
| <a id="customemojiedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. | | <a id="customemojiedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="customemojiedgenode"></a>`node` | [`CustomEmoji`](#customemoji) | The item at the end of the edge. | | <a id="customemojiedgenode"></a>`node` | [`CustomEmoji`](#customemoji) | The item at the end of the edge. |
#### `CustomerRelationsContactConnection`
The connection type for [`CustomerRelationsContact`](#customerrelationscontact).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="customerrelationscontactconnectionedges"></a>`edges` | [`[CustomerRelationsContactEdge]`](#customerrelationscontactedge) | A list of edges. |
| <a id="customerrelationscontactconnectionnodes"></a>`nodes` | [`[CustomerRelationsContact]`](#customerrelationscontact) | A list of nodes. |
| <a id="customerrelationscontactconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
#### `CustomerRelationsContactEdge`
The edge type for [`CustomerRelationsContact`](#customerrelationscontact).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="customerrelationscontactedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="customerrelationscontactedgenode"></a>`node` | [`CustomerRelationsContact`](#customerrelationscontact) | The item at the end of the edge. |
#### `CustomerRelationsOrganizationConnection` #### `CustomerRelationsOrganizationConnection`
The connection type for [`CustomerRelationsOrganization`](#customerrelationsorganization). The connection type for [`CustomerRelationsOrganization`](#customerrelationsorganization).
...@@ -8545,18 +8568,34 @@ A custom emoji uploaded by user. ...@@ -8545,18 +8568,34 @@ A custom emoji uploaded by user.
| <a id="customemojiname"></a>`name` | [`String!`](#string) | Name of the emoji. | | <a id="customemojiname"></a>`name` | [`String!`](#string) | Name of the emoji. |
| <a id="customemojiurl"></a>`url` | [`String!`](#string) | Link to file of the emoji. | | <a id="customemojiurl"></a>`url` | [`String!`](#string) | Link to file of the emoji. |
### `CustomerRelationsContact`
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="customerrelationscontactcreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp the contact was created. |
| <a id="customerrelationscontactdescription"></a>`description` | [`String`](#string) | Description or notes for the contact. |
| <a id="customerrelationscontactemail"></a>`email` | [`String`](#string) | Email address of the contact. |
| <a id="customerrelationscontactfirstname"></a>`firstName` | [`String!`](#string) | First name of the contact. |
| <a id="customerrelationscontactid"></a>`id` | [`ID!`](#id) | Internal ID of the contact. |
| <a id="customerrelationscontactlastname"></a>`lastName` | [`String!`](#string) | Last name of the contact. |
| <a id="customerrelationscontactorganization"></a>`organization` | [`CustomerRelationsOrganization`](#customerrelationsorganization) | Organization of the contact. |
| <a id="customerrelationscontactphone"></a>`phone` | [`String`](#string) | Phone number of the contact. |
| <a id="customerrelationscontactupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp the contact was last updated. |
### `CustomerRelationsOrganization` ### `CustomerRelationsOrganization`
#### Fields #### Fields
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="customerrelationsorganizationcreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp the organization was created. | | <a id="customerrelationsorganizationcreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp the organization was created. |
| <a id="customerrelationsorganizationdefaultrate"></a>`defaultRate` | [`Float`](#float) | Standard billing rate for the organization. | | <a id="customerrelationsorganizationdefaultrate"></a>`defaultRate` | [`Float`](#float) | Standard billing rate for the organization. |
| <a id="customerrelationsorganizationdescription"></a>`description` | [`String`](#string) | Description or notes for the organization. | | <a id="customerrelationsorganizationdescription"></a>`description` | [`String`](#string) | Description or notes for the organization. |
| <a id="customerrelationsorganizationid"></a>`id` | [`ID!`](#id) | Internal ID of the organization. | | <a id="customerrelationsorganizationid"></a>`id` | [`ID!`](#id) | Internal ID of the organization. |
| <a id="customerrelationsorganizationname"></a>`name` | [`String`](#string) | Name of the organization. | | <a id="customerrelationsorganizationname"></a>`name` | [`String`](#string) | Name of the organization. |
| <a id="customerrelationsorganizationupdatedat"></a>`updatedAt` | [`Time`](#time) | Timestamp the organization was last updated. | | <a id="customerrelationsorganizationupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp the organization was last updated. |
### `DastProfile` ### `DastProfile`
...@@ -9783,6 +9822,7 @@ four standard [pagination arguments](#connection-pagination-arguments): ...@@ -9783,6 +9822,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="groupautodevopsenabled"></a>`autoDevopsEnabled` | [`Boolean`](#boolean) | Indicates whether Auto DevOps is enabled for all projects within this group. | | <a id="groupautodevopsenabled"></a>`autoDevopsEnabled` | [`Boolean`](#boolean) | Indicates whether Auto DevOps is enabled for all projects within this group. |
| <a id="groupavatarurl"></a>`avatarUrl` | [`String`](#string) | Avatar URL of the group. | | <a id="groupavatarurl"></a>`avatarUrl` | [`String`](#string) | Avatar URL of the group. |
| <a id="groupbillablememberscount"></a>`billableMembersCount` | [`Int`](#int) | Number of billable users in the group. | | <a id="groupbillablememberscount"></a>`billableMembersCount` | [`Int`](#int) | Number of billable users in the group. |
| <a id="groupcontacts"></a>`contacts` | [`CustomerRelationsContactConnection`](#customerrelationscontactconnection) | Find contacts of this group. (see [Connections](#connections)) |
| <a id="groupcontainerrepositoriescount"></a>`containerRepositoriesCount` | [`Int!`](#int) | Number of container repositories in the group. | | <a id="groupcontainerrepositoriescount"></a>`containerRepositoriesCount` | [`Int!`](#int) | Number of container repositories in the group. |
| <a id="groupcontainslockedprojects"></a>`containsLockedProjects` | [`Boolean!`](#boolean) | Includes at least one project where the repository size exceeds the limit. | | <a id="groupcontainslockedprojects"></a>`containsLockedProjects` | [`Boolean!`](#boolean) | Includes at least one project where the repository size exceeds the limit. |
| <a id="groupcustomemoji"></a>`customEmoji` | [`CustomEmojiConnection`](#customemojiconnection) | Custom emoji within this namespace. Available only when feature flag `custom_emoji` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. (see [Connections](#connections)) | | <a id="groupcustomemoji"></a>`customEmoji` | [`CustomEmojiConnection`](#customemojiconnection) | Custom emoji within this namespace. Available only when feature flag `custom_emoji` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. (see [Connections](#connections)) |
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['CustomerRelationsContact'] do
let(:fields) { %i[id organization first_name last_name phone email description created_at updated_at] }
it { expect(described_class.graphql_name).to eq('CustomerRelationsContact') }
it { expect(described_class).to have_graphql_fields(fields) }
it { expect(described_class).to require_graphql_authorizations(:read_contact) }
end
...@@ -22,7 +22,7 @@ RSpec.describe GitlabSchema.types['Group'] do ...@@ -22,7 +22,7 @@ RSpec.describe GitlabSchema.types['Group'] do
dependency_proxy_blobs dependency_proxy_image_count dependency_proxy_blobs dependency_proxy_image_count
dependency_proxy_blob_count dependency_proxy_total_size dependency_proxy_blob_count dependency_proxy_total_size
dependency_proxy_image_prefix shared_runners_setting dependency_proxy_image_prefix shared_runners_setting
timelogs organizations timelogs organizations contacts
] ]
expect(described_class).to include_graphql_fields(*expected_fields) expect(described_class).to include_graphql_fields(*expected_fields)
......
...@@ -2644,6 +2644,16 @@ RSpec.describe Group do ...@@ -2644,6 +2644,16 @@ RSpec.describe Group do
end end
end end
describe '.contacts' do
it 'returns contacts belonging to the group' do
contact1 = create(:contact, group: group)
create(:contact)
contact3 = create(:contact, group: group)
expect(group.contacts).to contain_exactly(contact1, contact3)
end
end
describe '#to_ability_name' do describe '#to_ability_name' do
it 'returns group' do it 'returns group' do
group = build(:group) group = build(:group)
......
...@@ -12,6 +12,7 @@ RSpec.describe GroupPolicy do ...@@ -12,6 +12,7 @@ RSpec.describe GroupPolicy do
it do it do
expect_allowed(:read_group) expect_allowed(:read_group)
expect_allowed(:read_organization) expect_allowed(:read_organization)
expect_allowed(:read_contact)
expect_allowed(:read_counts) expect_allowed(:read_counts)
expect_allowed(*read_group_permissions) expect_allowed(*read_group_permissions)
expect_disallowed(:upload_file) expect_disallowed(:upload_file)
...@@ -33,6 +34,7 @@ RSpec.describe GroupPolicy do ...@@ -33,6 +34,7 @@ RSpec.describe GroupPolicy do
it { expect_disallowed(:read_group) } it { expect_disallowed(:read_group) }
it { expect_disallowed(:read_organization) } it { expect_disallowed(:read_organization) }
it { expect_disallowed(:read_contact) }
it { expect_disallowed(:read_counts) } it { expect_disallowed(:read_counts) }
it { expect_disallowed(*read_group_permissions) } it { expect_disallowed(*read_group_permissions) }
end end
...@@ -47,6 +49,7 @@ RSpec.describe GroupPolicy do ...@@ -47,6 +49,7 @@ RSpec.describe GroupPolicy do
it { expect_disallowed(:read_group) } it { expect_disallowed(:read_group) }
it { expect_disallowed(:read_organization) } it { expect_disallowed(:read_organization) }
it { expect_disallowed(:read_contact) }
it { expect_disallowed(:read_counts) } it { expect_disallowed(:read_counts) }
it { expect_disallowed(*read_group_permissions) } it { expect_disallowed(*read_group_permissions) }
end end
...@@ -903,6 +906,7 @@ RSpec.describe GroupPolicy do ...@@ -903,6 +906,7 @@ RSpec.describe GroupPolicy do
it { is_expected.to be_allowed(:read_package) } it { is_expected.to be_allowed(:read_package) }
it { is_expected.to be_allowed(:read_group) } it { is_expected.to be_allowed(:read_group) }
it { is_expected.to be_allowed(:read_organization) } it { is_expected.to be_allowed(:read_organization) }
it { is_expected.to be_allowed(:read_contact) }
it { is_expected.to be_disallowed(:create_package) } it { is_expected.to be_disallowed(:create_package) }
end end
...@@ -913,6 +917,7 @@ RSpec.describe GroupPolicy do ...@@ -913,6 +917,7 @@ RSpec.describe GroupPolicy do
it { is_expected.to be_allowed(:read_package) } it { is_expected.to be_allowed(:read_package) }
it { is_expected.to be_allowed(:read_group) } it { is_expected.to be_allowed(:read_group) }
it { is_expected.to be_allowed(:read_organization) } it { is_expected.to be_allowed(:read_organization) }
it { is_expected.to be_allowed(:read_contact) }
it { is_expected.to be_disallowed(:destroy_package) } it { is_expected.to be_disallowed(:destroy_package) }
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