Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions app/graphql/mutations/namespaces/members/bulk_invite.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# frozen_string_literal: true

module Mutations
module Namespaces
module Members
class BulkInvite < BaseMutation
description 'Invite multiple new members to a namespace.'

field :namespace_members, [Types::NamespaceMemberType],
description: 'The newly created namespace members'

argument :namespace_id, Types::GlobalIdType[::Namespace],
description: 'The id of the namespace which these members will belong to'
argument :user_ids, [Types::GlobalIdType[::User]], description: 'The ids of the users to invite'

def resolve(namespace_id:, user_ids:)
namespace = SagittariusSchema.object_from_id(namespace_id)
users = user_ids.map { |id| SagittariusSchema.object_from_id(id) }

if namespace.nil?
return { namespace_members: nil,
errors: [create_error(:namespace_not_found, 'Invalid namespace')] }
end
if users.any?(&:nil?)
return { namespace_members: nil,
errors: [create_error(:user_not_found, 'Invalid user')] }
end

::Namespaces::Members::BulkInviteService.new(
current_authentication,
namespace,
users
).execute.to_mutation_response(success_key: :namespace_members)
end
end
end
end
end
1 change: 1 addition & 0 deletions app/graphql/types/mutation_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class MutationType < Types::BaseObject
mount_mutation Mutations::Ai::GenerateFlow
mount_mutation Mutations::Echo
mount_mutation Mutations::Namespaces::Members::AssignRoles
mount_mutation Mutations::Namespaces::Members::BulkInvite
mount_mutation Mutations::Namespaces::Members::Delete
mount_mutation Mutations::Namespaces::Members::Invite
mount_mutation Mutations::Namespaces::Projects::AssignRuntimes
Expand Down
30 changes: 30 additions & 0 deletions app/services/namespaces/members/bulk_invite_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

module Namespaces
module Members
class BulkInviteService
include Sagittarius::Database::Transactional

attr_reader :current_authentication, :namespace, :users

def initialize(current_authentication, namespace, users)
@current_authentication = current_authentication
@namespace = namespace
@users = users
end

def execute
transactional do |transaction|
namespace_members = users.map do |user|
response = InviteService.new(current_authentication, namespace, user).execute
transaction.rollback_and_return!(response) if response.error?

response.payload
end

ServiceResponse.success(message: 'Namespace members invited', payload: namespace_members)
end
end
end
end
end
21 changes: 21 additions & 0 deletions docs/graphql/mutation/namespacesmembersbulkinvite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
title: namespacesMembersBulkInvite
---

Invite multiple new members to a namespace.

## Arguments

| Name | Type | Description |
|------|------|-------------|
| `clientMutationId` | [`String`](../scalar/string.md) | A unique identifier for the client performing the mutation. |
| `namespaceId` | [`NamespaceID!`](../scalar/namespaceid.md) | The id of the namespace which these members will belong to |
| `userIds` | [`[UserID!]!`](../scalar/userid.md) | The ids of the users to invite |

## Fields

| Name | Type | Description |
|------|------|-------------|
| `clientMutationId` | [`String`](../scalar/string.md) | A unique identifier for the client performing the mutation. |
| `errors` | [`[Error!]!`](../object/error.md) | Errors encountered during execution of the mutation. |
| `namespaceMembers` | [`[NamespaceMember!]`](../object/namespacemember.md) | The newly created namespace members |
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe 'namespacesMembersBulkInvite Mutation' do
include GraphqlHelpers

subject(:mutate!) { post_graphql mutation, variables: variables, current_user: current_user }

let(:mutation) do
<<~QUERY
mutation($input: NamespacesMembersBulkInviteInput!) {
namespacesMembersBulkInvite(input: $input) {
#{error_query}
namespaceMembers {
id
user {
id
}
namespace {
id
}
}
}
}
QUERY
end

let(:namespace) { create(:namespace) }
let(:users) { create_list(:user, 2) }
let(:input) do
{
namespaceId: namespace.to_global_id.to_s,
userIds: users.map { |user| user.to_global_id.to_s },
}
end
let(:variables) { { input: input } }
let(:current_user) { create(:user) }

context 'when user has permission' do
before do
create(:namespace_member, namespace: namespace, user: current_user)
stub_allowed_ability(NamespacePolicy, :invite_member, user: current_user, subject: namespace)
end

it 'creates all namespace members and their audit events' do
mutate!

member_ids = graphql_data_at(:namespaces_members_bulk_invite, :namespace_members, :id)
namespace_members = member_ids.map { |id| SagittariusSchema.object_from_id(id) }

expect(namespace_members.map(&:user)).to match_array(users)
expect(namespace_members.map(&:namespace)).to all(eq(namespace))
expect(
AuditEvent.where(
action_type: :namespace_member_invited,
entity_type: 'NamespaceMember',
entity_id: namespace_members.map(&:id)
).count
).to eq(2)
end

it 'rolls back all invitations when one user is already a member' do
create(:namespace_member, namespace: namespace, user: users.last)

expect { mutate! }.not_to change { namespace.namespace_members.count }

expect(graphql_data_at(:namespaces_members_bulk_invite, :namespace_members)).to be_nil
expect(
graphql_data_at(:namespaces_members_bulk_invite, :errors, :details)
).to include([{ 'attribute' => 'namespace', 'type' => 'taken' }])
end
end

context 'when user does not have permission' do
it 'returns an error without creating members' do
expect { mutate! }.not_to change { NamespaceMember.count }

expect(graphql_data_at(:namespaces_members_bulk_invite, :namespace_members)).to be_nil
expect(
graphql_data_at(:namespaces_members_bulk_invite, :errors, :error_code)
).to include('MISSING_PERMISSION')
end
end
end
40 changes: 40 additions & 0 deletions spec/services/namespaces/members/bulk_invite_service_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe Namespaces::Members::BulkInviteService do
subject(:service_response) do
described_class.new(create_authentication(current_user), namespace, users).execute
end

let(:namespace) { create(:namespace) }
let(:users) { create_list(:user, 2) }
let(:current_user) { create(:user) }

context 'when user has permission' do
before do
stub_allowed_ability(NamespacePolicy, :invite_member, user: current_user, subject: namespace)
end

it 'creates an audit event for every invited member' do
expect { service_response }.to change {
AuditEvent.where(action_type: :namespace_member_invited).count
}.by(users.count)

events = AuditEvent.where(
action_type: :namespace_member_invited,
entity_type: 'NamespaceMember'
)

expect(events.pluck(:entity_id)).to match_array(service_response.payload.map(&:id))
expect(events).to all(
have_attributes(
author_id: current_user.id,
details: {},
target_id: namespace.id,
target_type: 'Namespace'
)
)
end
end
end