Skip to content

Commit 61e9146

Browse files
Add UpdateComponent mutation to allow components to be modified (#162)
Example: ``` mutation { updateComponent(input: { id: "Z2lkOi8vYXBwL0NvbXBvbmVudC81MTcyZjk2NC1mNjVlLTRkZDgtOGY1ZC1lODdjZTZlMzU2MzA" name: "mainB", extension: "py", content: "", default: false, }) { component { id } } } ``` closes #150 --------- Co-authored-by: chrisruk <[email protected]> Co-authored-by: chrisruk <[email protected]>
1 parent 4dc6d78 commit 61e9146

File tree

5 files changed

+205
-0
lines changed

5 files changed

+205
-0
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# frozen_string_literal: true
2+
3+
module Mutations
4+
class UpdateComponent < BaseMutation
5+
description 'A mutation to update an existing component'
6+
7+
input_object_class Types::UpdateComponentInputType
8+
9+
field :component, Types::ComponentType, description: 'The component that has been updated'
10+
11+
def resolve(**input)
12+
component = GlobalID.find(input[:id])
13+
raise GraphQL::ExecutionError, 'Component not found' unless component
14+
15+
unless context[:current_ability].can?(:update, component)
16+
raise GraphQL::ExecutionError,
17+
'You are not permitted to update this component'
18+
end
19+
20+
return { component: } if component.update(input.slice(:content, :name, :extension, :default))
21+
22+
raise GraphQL::ExecutionError, component.errors.full_messages.join(', ')
23+
end
24+
25+
def ready?(**_args)
26+
return true if context[:current_ability]&.can?(:update, Component, user_id: context[:current_user_id])
27+
28+
raise GraphQL::ExecutionError, 'You are not permitted to update a component'
29+
end
30+
end
31+
end

app/graphql/types/mutation_type.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ class MutationType < Types::BaseObject
77
field :create_project, mutation: Mutations::CreateProject, description: 'Create a project, complete with components'
88
field :delete_project, mutation: Mutations::DeleteProject, description: 'Delete an existing project'
99
field :remix_project, mutation: Mutations::RemixProject, description: 'Remix a project'
10+
field :update_component, mutation: Mutations::UpdateComponent, description: 'Update fields on an existing component'
1011
field :update_project, mutation: Mutations::UpdateProject, description: 'Update fields on an existing project'
1112
# rubocop:enable GraphQL/ExtractType
1213
end
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# frozen_string_literal: true
2+
3+
module Types
4+
class UpdateComponentInputType < Types::BaseInputObject
5+
description 'Represents a project during an update'
6+
7+
argument :content, String, required: false, description: 'The text content of the component'
8+
argument :default, Boolean, required: false, description: 'If this is the default component on a project'
9+
argument :extension, String, required: false, description: 'The file extension of the component, e.g. html, csv, py'
10+
argument :id, String, required: true, description: 'The ID of the component to update'
11+
argument :name, String, required: false, description: 'The name of the file'
12+
end
13+
end

db/schema.graphql

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,16 @@ type Mutation {
309309
input: RemixProjectInput!
310310
): RemixProjectPayload
311311

312+
"""
313+
Update fields on an existing component
314+
"""
315+
updateComponent(
316+
"""
317+
Parameters for UpdateComponent
318+
"""
319+
input: UpdateComponentInput!
320+
): UpdateComponentPayload
321+
312322
"""
313323
Update fields on an existing project
314324
"""
@@ -627,6 +637,56 @@ type RemixProjectPayload {
627637
project: Project
628638
}
629639

640+
"""
641+
Represents a project during an update
642+
"""
643+
input UpdateComponentInput {
644+
"""
645+
A unique identifier for the client performing the mutation.
646+
"""
647+
clientMutationId: String
648+
649+
"""
650+
The text content of the component
651+
"""
652+
content: String
653+
654+
"""
655+
If this is the default component on a project
656+
"""
657+
default: Boolean
658+
659+
"""
660+
The file extension of the component, e.g. html, csv, py
661+
"""
662+
extension: String
663+
664+
"""
665+
The ID of the component to update
666+
"""
667+
id: String!
668+
669+
"""
670+
The name of the file
671+
"""
672+
name: String
673+
}
674+
675+
"""
676+
Autogenerated return type of UpdateComponent.
677+
"""
678+
type UpdateComponentPayload {
679+
"""
680+
A unique identifier for the client performing the mutation.
681+
"""
682+
clientMutationId: String
683+
684+
"""
685+
The component that has been updated
686+
"""
687+
component: Component
688+
}
689+
630690
"""
631691
Represents a project during an update
632692
"""
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe 'mutation UpdateComponent() { ... }' do
6+
subject(:result) { execute_query(query: mutation, variables:) }
7+
8+
let(:mutation) { 'mutation UpdateComponent($component: UpdateComponentInput!) { updateComponent(input: $component) { component { id } } }' }
9+
let(:component_id) { 'dummy-id' }
10+
let(:variables) do
11+
{
12+
component: {
13+
id: component_id,
14+
name: 'main2',
15+
extension: 'py',
16+
content: '',
17+
default: false
18+
}
19+
}
20+
end
21+
22+
it { expect(mutation).to be_a_valid_graphql_query }
23+
24+
context 'with an existing component' do
25+
let(:component) { create(:component, name: 'bob', extension: 'html', content: 'new', default: true) }
26+
let(:component_id) { component.to_gid_param }
27+
28+
before do
29+
# Instantiate component
30+
component
31+
end
32+
33+
context 'when unauthenticated' do
34+
it 'does not update a component' do
35+
expect { result }.not_to change { component.reload.name }
36+
end
37+
38+
it 'returns an error' do
39+
expect(result.dig('errors', 0, 'message')).not_to be_blank
40+
end
41+
end
42+
43+
context 'when the graphql context is unset' do
44+
let(:graphql_context) { nil }
45+
46+
it 'does not update a component' do
47+
expect { result }.not_to change { component.reload.name }
48+
end
49+
end
50+
51+
context 'when authenticated' do
52+
let(:current_user_id) { component.project.user_id }
53+
54+
it 'updates the component name' do
55+
expect { result }.to change { component.reload.name }.from(component.name).to(variables.dig(:component, :name))
56+
end
57+
58+
it 'updates the component content' do
59+
expect { result }.to change { component.reload.content }.from(component.content).to(variables.dig(:component, :content))
60+
end
61+
62+
it 'updates the component extension' do
63+
expect { result }.to change { component.reload.extension }.from(component.extension).to(variables.dig(:component, :extension))
64+
end
65+
66+
it 'updates the component default' do
67+
expect { result }.to change { component.reload.default }.from(component.default).to(variables.dig(:component, :default))
68+
end
69+
70+
context 'when the component cannot be found' do
71+
let(:component_id) { 'dummy' }
72+
73+
it 'returns an error' do
74+
expect(result.dig('errors', 0, 'message')).to match(/not found/)
75+
end
76+
end
77+
78+
context 'with another users component' do
79+
let(:current_user_id) { SecureRandom.uuid }
80+
81+
it 'returns an error' do
82+
expect(result.dig('errors', 0, 'message')).to match(/not permitted/)
83+
end
84+
end
85+
86+
context 'when component update fails' do
87+
before do
88+
errors = instance_double(ActiveModel::Errors, full_messages: ['An error message'])
89+
allow(component).to receive(:save).and_return(false)
90+
allow(component).to receive(:errors).and_return(errors)
91+
allow(GlobalID).to receive(:find).and_return(component)
92+
end
93+
94+
it 'returns an error' do
95+
expect(result.dig('errors', 0, 'message')).to match(/An error message/)
96+
end
97+
end
98+
end
99+
end
100+
end

0 commit comments

Comments
 (0)