Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add UpdateComponent mutation to allow components to be modified #162

31 changes: 31 additions & 0 deletions app/graphql/mutations/update_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

module Mutations
class UpdateComponent < BaseMutation
description 'A mutation to update an existing component'

input_object_class Types::UpdateComponentInputType

field :component, Types::ComponentType, description: 'The component that has been updated'

def resolve(**input)
component = GlobalID.find(input[:id])
raise GraphQL::ExecutionError, 'Component not found' unless component

unless context[:current_ability].can?(:update, component)
raise GraphQL::ExecutionError,
'You are not permitted to update this component'
end

return { component: } if component.update(input.slice(:content, :name, :extension, :default))

raise GraphQL::ExecutionError, component.errors.full_messages.join(', ')
end

def ready?(**_args)
return true if context[:current_ability]&.can?(:update, Component, user_id: context[:current_user_id])

raise GraphQL::ExecutionError, 'You are not permitted to update a component'
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 @@ -7,6 +7,7 @@ class MutationType < Types::BaseObject
field :create_project, mutation: Mutations::CreateProject, description: 'Create a project, complete with components'
field :delete_project, mutation: Mutations::DeleteProject, description: 'Delete an existing project'
field :remix_project, mutation: Mutations::RemixProject, description: 'Remix a project'
field :update_component, mutation: Mutations::UpdateComponent, description: 'Update fields on an existing component'
field :update_project, mutation: Mutations::UpdateProject, description: 'Update fields on an existing project'
# rubocop:enable GraphQL/ExtractType
end
Expand Down
13 changes: 13 additions & 0 deletions app/graphql/types/update_component_input_type.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

module Types
class UpdateComponentInputType < Types::BaseInputObject
description 'Represents a project during an update'

argument :content, String, required: false, description: 'The text content of the component'
argument :default, Boolean, required: false, description: 'If this is the default component on a project'
argument :extension, String, required: false, description: 'The file extension of the component, e.g. html, csv, py'
argument :id, String, required: false, description: 'The ID of the component to update'
chrisruk marked this conversation as resolved.
Show resolved Hide resolved
argument :name, String, required: false, description: 'The name of the file'
end
end
60 changes: 60 additions & 0 deletions db/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,16 @@ type Mutation {
input: RemixProjectInput!
): RemixProjectPayload

"""
Update fields on an existing component
"""
updateComponent(
"""
Parameters for UpdateComponent
"""
input: UpdateComponentInput!
): UpdateComponentPayload

"""
Update fields on an existing project
"""
Expand Down Expand Up @@ -627,6 +637,56 @@ type RemixProjectPayload {
project: Project
}

"""
Represents a project during an update
"""
input UpdateComponentInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String

"""
The text content of the component
"""
content: String

"""
If this is the default component on a project
"""
default: Boolean

"""
The file extension of the component, e.g. html, csv, py
"""
extension: String

"""
The ID of the component to update
"""
id: String

"""
The name of the file
"""
name: String
}

"""
Autogenerated return type of UpdateComponent.
"""
type UpdateComponentPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String

"""
The component that has been updated
"""
component: Component
}

"""
Represents a project during an update
"""
Expand Down
100 changes: 100 additions & 0 deletions spec/graphql/mutations/update_component_mutation_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe 'mutation UpdateComponent() { ... }' do
subject(:result) { execute_query(query: mutation, variables:) }

let(:mutation) { 'mutation UpdateComponent($component: UpdateComponentInput!) { updateComponent(input: $component) { component { id } } }' }
let(:component_id) { 'dummy-id' }
let(:variables) do
{
component: {
id: component_id,
name: 'main2',
extension: 'py',
content: '',
default: false
}
}
end

it { expect(mutation).to be_a_valid_graphql_query }

context 'with an existing component' do
let(:component) { create(:component, name: 'bob', extension: 'html', content: 'new', default: true) }
let(:component_id) { component.to_gid_param }

before do
# Instantiate component
component
end

context 'when unauthenticated' do
it 'does not update a component' do
expect { result }.not_to change { component.reload.name }
end

it 'returns an error' do
expect(result.dig('errors', 0, 'message')).not_to be_blank
end
end

context 'when the graphql context is unset' do
let(:graphql_context) { nil }

it 'does not update a component' do
expect { result }.not_to change { component.reload.name }
end
end

context 'when authenticated' do
let(:current_user_id) { component.project.user_id }

it 'updates the component name' do
expect { result }.to change { component.reload.name }.from(component.name).to(variables.dig(:component, :name))
chrisruk marked this conversation as resolved.
Show resolved Hide resolved
end

it 'updates the component content' do
expect { result }.to change { component.reload.content }.from(component.content).to(variables.dig(:component, :content))
end

it 'updates the component extension' do
expect { result }.to change { component.reload.extension }.from(component.extension).to(variables.dig(:component, :extension))
end

it 'updates the component default' do
expect { result }.to change { component.reload.default }.from(component.default).to(variables.dig(:component, :default))
end

context 'when the component cannot be found' do
let(:component_id) { 'dummy' }

it 'returns an error' do
expect(result.dig('errors', 0, 'message')).to match(/not found/)
end
end

context 'with another users component' do
let(:current_user_id) { SecureRandom.uuid }

it 'returns an error' do
expect(result.dig('errors', 0, 'message')).to match(/not permitted/)
end
end

context 'when component update fails' do
before do
errors = instance_double(ActiveModel::Errors, full_messages: ['An error message'])
allow(component).to receive(:save).and_return(false)
allow(component).to receive(:errors).and_return(errors)
allow(GlobalID).to receive(:find).and_return(component)
end

it 'returns an error' do
expect(result.dig('errors', 0, 'message')).to match(/An error message/)
end
end
end
end
end