Skip to content
Merged
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
36 changes: 36 additions & 0 deletions app/graphql/mutations/create_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

module Mutations
class CreateComponent < BaseMutation
description 'A mutation to create a new component'
input_object_class Types::CreateComponentInputType

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

def resolve(**input)
project = GlobalID.find(input[:project_id])

raise GraphQL::ExecutionError, 'Project not found' unless project

component = Component.new input
component.project = project

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

return { component: } if component.save

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

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

raise GraphQL::ExecutionError, 'You are not permitted to create a component'
end
end
end
13 changes: 13 additions & 0 deletions app/graphql/types/create_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 CreateComponentInputType < Types::BaseInputObject
description 'Represents a component during creation'

argument :content, String, required: false, description: 'The text content of the component'
argument :default, Boolean, required: true, description: 'If this is the default component on a project'
argument :extension, String, required: true, description: 'The file extension of the component, e.g. html, csv, py'
argument :name, String, required: true, description: 'The name of the file'
argument :project_id, String, required: true, description: 'The easy-to-rememeber identifier of the project'
end
end
3 changes: 3 additions & 0 deletions app/graphql/types/mutation_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

module Types
class MutationType < Types::BaseObject
# rubocop:disable GraphQL/ExtractType
field :create_component, mutation: Mutations::CreateComponent, description: 'Create a component'
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_project, mutation: Mutations::UpdateProject, description: 'Update fields on an existing project'
# rubocop:enable GraphQL/ExtractType
end
end
60 changes: 60 additions & 0 deletions db/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,56 @@ type ComponentEdge {
node: Component
}

"""
Represents a component during creation
"""
input CreateComponentInput {
"""
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 name of the file
"""
name: String!

"""
The easy-to-rememeber identifier of the project
"""
projectId: String!
}

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

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

"""
Represents a project during creation
"""
Expand Down Expand Up @@ -219,6 +269,16 @@ type ImageEdge {
}

type Mutation {
"""
Create a component
"""
createComponent(
"""
Parameters for CreateComponent
"""
input: CreateComponentInput!
): CreateComponentPayload

"""
Create a project, complete with components
"""
Expand Down
81 changes: 81 additions & 0 deletions spec/graphql/mutations/create_component_mutation_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# frozen_string_literal: true

require 'rails_helper'

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

let(:project) { create(:project) }
let(:project_id) { project.to_gid_param }

let(:mutation) { 'mutation CreateComponent($component: CreateComponentInput!) { createComponent(input: $component) { component { id } } }' }
# let(:project_id) { 'dummy-id' }
let(:variables) do
{
component: {
projectId: project_id,
name: 'test',
extension: 'py',
content: 'blah',
default: false
}
}
end

it { expect(mutation).to be_a_valid_graphql_query }

context 'when unauthenticated' do
it 'does not create a component' do
expect { result }.not_to change(Component, :count)
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 create a component' do
expect { result }.not_to change(Component, :count)
end
end

context 'when authenticated' do
let(:current_user_id) { SecureRandom.uuid }
let(:project) { create(:project, user_id: current_user_id) }

it 'returns the component ID' do
expect(result.dig('data', 'createComponent', 'component', 'id')).not_to be_nil
end

context 'when project id doesnt exist' do
let(:project_id) { 'dummy-id' }

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

context 'when project id exists but belongs to someone else' do
let(:project) { create(:project) }

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

context 'when project component fails to save' do
before do
component = Component.new
allow(component).to receive(:save).and_return(false)
allow(Component).to receive(:new).and_return(component)
end

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