Skip to content

Commit

Permalink
Merge pull request #75 from goinvo/fermion/add-assignment-mutations
Browse files Browse the repository at this point in the history
Adds an upsertAssignment GraphQL mutation
  • Loading branch information
fermion authored Feb 15, 2024
2 parents a9d9161 + 54a745f commit a788980
Show file tree
Hide file tree
Showing 9 changed files with 298 additions and 8 deletions.
54 changes: 54 additions & 0 deletions app/graphql/mutations/upsert_assignment.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
module Mutations
class UpsertAssignment < BaseMutation
description "Create or update an assignment."

# arguments passed to the `resolve` method
argument :id, ID, required: false, description: "The ID of the assignment to update."
argument :project_id, ID, required: true, description: "The ID of the project this assignment is being created for."
argument :user_id, ID, required: true, description: "The ID of the user being assigned to the project."
argument :status, String, required: true, description: "The status of the assignment."
argument :starts_on, GraphQL::Types::ISO8601Date, required: false, description: "The date this assignment starts."
argument :ends_on, GraphQL::Types::ISO8601Date, required: false, description: "The date this assignment ends."

# return type from the mutation
type Types::StaffPlan::AssignmentType

def resolve(id: nil, project_id:, user_id:, status:, starts_on: nil, ends_on: nil)
current_company = context[:current_company]

# try and find the assignment
assignment = if id.present?
current_company.assignments.find(id)
end

if assignment
assignment.assign_attributes(project_id:, user_id:, status:)
else
project = current_company.projects.find(project_id)
assignment = project.assignments.new(user_id:, status:)
end

assignment.assign_attributes(starts_on: starts_on) if starts_on
assignment.assign_attributes(ends_on: ends_on) if ends_on

if assignment.valid?
assignment.save!
else
assignment.errors.group_by_attribute.each do |attribute, errors|
errors.each do |error|
context.add_error(
GraphQL::ExecutionError.new(
error.full_message,
extensions: {
attribute: attribute.to_s,
}
)
)
end
end
end

assignment
end
end
end
35 changes: 35 additions & 0 deletions app/graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,41 @@ type Mutation {
companyId: ID!
): Company!

"""
Create or update an assignment.
"""
upsertAssignment(
"""
The date this assignment ends.
"""
endsOn: ISO8601Date

"""
The ID of the assignment to update.
"""
id: ID

"""
The ID of the project this assignment is being created for.
"""
projectId: ID!

"""
The date this assignment starts.
"""
startsOn: ISO8601Date

"""
The status of the assignment.
"""
status: String!

"""
The ID of the user being assigned to the project.
"""
userId: ID!
): Assignment!

"""
Create or update a work week record for a StaffPlan user.
"""
Expand Down
1 change: 1 addition & 0 deletions app/graphql/types/mutation_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ module Types
class MutationType < Types::BaseObject
field :set_current_company, mutation: Mutations::SetCurrentCompany
field :upsert_work_week, mutation: Mutations::UpsertWorkWeek
field :upsert_assignment, mutation: Mutations::UpsertAssignment
end
end
8 changes: 8 additions & 0 deletions app/models/assignment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,19 @@ class Assignment < ApplicationRecord
validates :project_id, presence: true, uniqueness: { scope: :user_id }
validates :status, presence: true, inclusion: { in: VALID_STATUSES }
validate :starts_and_ends_on_rules
validate :project_and_user_belong_to_same_company

scope :for_user, ->(user) { where(user: user) }

private

def project_and_user_belong_to_same_company
project_company_users = project.company.active_users
return if project_company_users.include?(user)

errors.add(:project, "and user must belong to the same company")
end

def starts_and_ends_on_rules
if starts_on.blank? && ends_on.present?
errors.add(:starts_on, "is required if an end date is set")
Expand Down
4 changes: 4 additions & 0 deletions app/models/company.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ class Company < ApplicationRecord

validates :name, presence: true, uniqueness: { case_sensitive: false }

def active_users
users.joins(:memberships).where(memberships: { status: Membership::ACTIVE })
end

def subscription
@_subscription if defined?(@_subscription)

Expand Down
177 changes: 177 additions & 0 deletions spec/graphql/mutations/upsert_assignment_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# frozen_string_literal: true

require "rails_helper"

RSpec.describe Mutations::UpsertAssignment do

context "resolve" do
it "creates a new assignment with valid params" do
query_string = <<-GRAPHQL
mutation($projectId: ID!, $userId: ID!, $status: String!) {
upsertAssignment(projectId: $projectId, userId: $userId, status: $status) {
id
project {
id
}
user {
id
}
status
startsOn
endsOn
}
}
GRAPHQL

user = create(:user)
project = create(:project, company: user.current_company)

result = StaffplanReduxSchema.execute(
query_string,
context: {
current_user: user,
current_company: user.current_company
},
variables: {
projectId: project.id,
userId: user.id,
status: Assignment::PROPOSED
}
)

post_result = result["data"]["upsertAssignment"]
expect(result["errors"]).to be_nil
expect(post_result["project"]["id"]).to eq(project.id.to_s)
expect(post_result["user"]["id"]).to eq(user.id.to_s)
expect(post_result["status"]).to eq(Assignment::PROPOSED)
expect(post_result["startsOn"]).to be_nil
expect(post_result["endsOn"]).to be_nil
end

it "updates the assignment with valid params" do
query_string = <<-GRAPHQL
mutation($id: ID, $projectId: ID!, $userId: ID!, $status: String!, $startsOn: ISO8601Date, $endsOn: ISO8601Date) {
upsertAssignment(id: $id, projectId: $projectId, userId: $userId, status: $status, startsOn: $startsOn, endsOn: $endsOn) {
id
project {
id
}
user {
id
}
status
startsOn
endsOn
}
}
GRAPHQL

user = create(:user)
assignment = assignment_for_user(user:, status: Assignment::PROPOSED)

result = StaffplanReduxSchema.execute(
query_string,
context: {
current_user: user,
current_company: user.current_company
},
variables: {
id: assignment.id,
projectId: assignment.project_id,
userId: assignment.user_id,
status: Assignment::ACTIVE,
startsOn: starts_on = 2.weeks.from_now.to_date.iso8601,
endsOn: ends_on = 10.weeks.from_now.to_date.iso8601
}
)

post_result = result["data"]["upsertAssignment"]
expect(result["errors"]).to be_nil
expect(post_result["status"]).to eq(Assignment::ACTIVE)
expect(post_result["startsOn"]).to eq(starts_on.to_s)
expect(post_result["endsOn"]).to eq(ends_on.to_s)
end

it "renders validation errors" do
query_string = <<-GRAPHQL
mutation($id: ID, $projectId: ID!, $userId: ID!, $status: String!) {
upsertAssignment(id: $id, projectId: $projectId, userId: $userId, status: $status) {
id
project {
id
}
user {
id
}
status
startsOn
endsOn
}
}
GRAPHQL

user = create(:user)
assignment = assignment_for_user(user:)

result = StaffplanReduxSchema.execute(
query_string,
context: {
current_user: user,
current_company: user.current_company
},
variables: {
id: assignment.id,
projectId: create(:project).id,
userId: assignment.user_id,
status: "invalid-status",
}
)

post_result = result["errors"]
expect(post_result.length).to eq(2)
expect(post_result.first["message"]).to eq("Status is not included in the list")
expect(post_result.first["extensions"]["attribute"]).to eq("status")
expect(post_result.second["message"]).to eq("Project and user must belong to the same company")
expect(post_result.second["extensions"]["attribute"]).to eq("project")
end

it "raises a 404 if given an assignment id that doesn't exist on the company" do
query_string = <<-GRAPHQL
mutation($id: ID, $projectId: ID!, $userId: ID!, $status: String!) {
upsertAssignment(id: $id, projectId: $projectId, userId: $userId, status: $status) {
id
project {
id
}
user {
id
}
status
startsOn
endsOn
}
}
GRAPHQL

user = create(:user)
assignment = assignment_for_user(user:)

result = StaffplanReduxSchema.execute(
query_string,
context: {
current_user: user,
current_company: user.current_company
},
variables: {
id: assignment_for_user(user: create(:user)).id,
projectId: assignment.project_id,
userId: assignment.user_id,
status: Assignment::ACTIVE,
}
)

post_result = result["errors"]
expect(post_result.first["message"]).to eq("Assignment not found")
end
end
end
8 changes: 0 additions & 8 deletions spec/graphql/mutations/upsert_work_week_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,6 @@

RSpec.describe Mutations::UpsertWorkWeek do

# when the :work_weeks factory is used the user's current_company
# is not the same as the one the project/assignment belongs to.
def assignment_for_user(user:)
client = create(:client, company: user.current_company)
project = create(:project, client:)
create(:assignment, user:, project:)
end

context "when updating a work week" do
it "updates the work week with valid params" do
query_string = <<-GRAPHQL
Expand Down
12 changes: 12 additions & 0 deletions spec/models/assignment_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,16 @@
expect(assignment.errors[:ends_on]).to include("is required if a start date is set")
end
end

describe "#project_and_user_belong_to_same_company" do
it "validates that the user and project belong to the same company" do
user = create(:user)
project = create(:project)
assignment = build(:assignment, user: user, project: project)
expect(project.company.active_users).to_not include(user)

expect(assignment).to_not be_valid
expect(assignment.errors[:project]).to include("user must belong to the same company as the project")
end
end
end
7 changes: 7 additions & 0 deletions spec/support/helper_methods.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# when the :work_weeks factory is used the user's current_company
# is not the same as the one the project/assignment belongs to.
def assignment_for_user(user:, status: Assignment::ACTIVE)
client = create(:client, company: user.current_company)
project = create(:project, client:)
create(:assignment, user:, project:, status:)
end

0 comments on commit a788980

Please sign in to comment.