From 586a4fe23f15b92b65173c77ab6f9060a707d2a6 Mon Sep 17 00:00:00 2001 From: chrisruk Date: Mon, 6 Mar 2023 12:30:45 +0000 Subject: [PATCH 1/9] Create draft PR for #149 From 62b398c5c8e8a76b17617d1a2f22478c4c43f1b9 Mon Sep 17 00:00:00 2001 From: chrisruk Date: Thu, 9 Mar 2023 10:23:50 +0000 Subject: [PATCH 2/9] Create component mutation (WIP) --- app/graphql/mutations/create_component.rb | 54 +++++++++++++++++++ .../types/create_component_input_type.rb | 12 +++++ app/graphql/types/mutation_type.rb | 1 + 3 files changed, 67 insertions(+) create mode 100644 app/graphql/mutations/create_component.rb create mode 100644 app/graphql/types/create_component_input_type.rb diff --git a/app/graphql/mutations/create_component.rb b/app/graphql/mutations/create_component.rb new file mode 100644 index 000000000..d03bfb35d --- /dev/null +++ b/app/graphql/mutations/create_component.rb @@ -0,0 +1,54 @@ +# 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_hash = input.merge(user_id: context[:current_user_id], + # components: input[:components]&.map(&:to_h)) + + #print("here", input) + #project = GlobalID.find(input[:identifier]) + #response = Component.new(project_id: 'atom-spout-enter', name: 'test1', extension: 'py') + #raise GraphQL::ExecutionError, response[:error] unless response.success? + + print("--------") + print(input) + + print("------------") + + project = GlobalID.find("Z2lkOi8vYXBwL1Byb2plY3QvMmNmOTllOWItNGQzZS00MDQ2LWFjMWEtY2Q3MzMwNTFiNjA2") + + + raise GraphQL::ExecutionError, 'Project not found' unless project + + unless context[:current_ability].can?(:update, project) + raise GraphQL::ExecutionError, + 'You are not permitted to update this project' + end + + #return { project: } if project.update(input[:components]) + t = {components: project.components.append([Component.new(name: 'main2', extension: 'py')])} + return { project: } if project.update(t) + + raise GraphQL::ExecutionError, project.errors.full_messages.join(', ') + + + + + + end + + def ready?(**_args) + print("here2") + print("uid",context[:current_user_id]) + return true if context[:current_ability]&.can?(:create, Component, user_id: context[:current_user_id]) + + raise GraphQL::ExecutionError, 'You are not permitted to create a component' + end + end +end diff --git a/app/graphql/types/create_component_input_type.rb b/app/graphql/types/create_component_input_type.rb new file mode 100644 index 000000000..bddb127e0 --- /dev/null +++ b/app/graphql/types/create_component_input_type.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Types + class CreateComponentInputType < Types::BaseInputObject + description 'Represents a component during creation' + + argument :identifier, String, required: true, description: 'The easy-to-rememeber identifier of the project' + + argument :components, [Types::ProjectComponentInputType], required: false, description: 'Any project components' + + end +end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 547693db6..6786f4e94 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -5,5 +5,6 @@ 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 :update_project, mutation: Mutations::UpdateProject, description: 'Update fields on an existing project' + field :create_component, mutation: Mutations::CreateComponent, description: 'Create a component' end end From 931015f8b5ac34fe0251bf8fd71d58d3ed402fcf Mon Sep 17 00:00:00 2001 From: chrisruk Date: Thu, 9 Mar 2023 11:29:04 +0000 Subject: [PATCH 3/9] Simplify --- app/graphql/mutations/create_component.rb | 28 +++++------------------ 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/app/graphql/mutations/create_component.rb b/app/graphql/mutations/create_component.rb index d03bfb35d..8b990aa7d 100644 --- a/app/graphql/mutations/create_component.rb +++ b/app/graphql/mutations/create_component.rb @@ -8,21 +8,7 @@ class CreateComponent < BaseMutation field :component, Types::ComponentType, description: 'The component that has been created' def resolve(**input) - #project_hash = input.merge(user_id: context[:current_user_id], - # components: input[:components]&.map(&:to_h)) - - #print("here", input) - #project = GlobalID.find(input[:identifier]) - #response = Component.new(project_id: 'atom-spout-enter', name: 'test1', extension: 'py') - #raise GraphQL::ExecutionError, response[:error] unless response.success? - - print("--------") - print(input) - - print("------------") - project = GlobalID.find("Z2lkOi8vYXBwL1Byb2plY3QvMmNmOTllOWItNGQzZS00MDQ2LWFjMWEtY2Q3MzMwNTFiNjA2") - raise GraphQL::ExecutionError, 'Project not found' unless project @@ -31,16 +17,14 @@ def resolve(**input) 'You are not permitted to update this project' end - #return { project: } if project.update(input[:components]) - t = {components: project.components.append([Component.new(name: 'main2', extension: 'py')])} - return { project: } if project.update(t) - - raise GraphQL::ExecutionError, project.errors.full_messages.join(', ') - - - + newc = [] + input[:components].each {|component| + newc.append(Component.new component.to_h) + } + return { project: } if project.update({components: project.components.append(newc)}) + raise GraphQL::ExecutionError, project.errors.full_messages.join(', ') end def ready?(**_args) From 609a91c299b92f78af186562ee4b77c8d577db25 Mon Sep 17 00:00:00 2001 From: chrisruk Date: Thu, 9 Mar 2023 14:48:06 +0000 Subject: [PATCH 4/9] Specify id of project via graphql --- app/graphql/mutations/create_component.rb | 2 +- app/graphql/types/create_component_input_type.rb | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/graphql/mutations/create_component.rb b/app/graphql/mutations/create_component.rb index 8b990aa7d..2ec25e1ab 100644 --- a/app/graphql/mutations/create_component.rb +++ b/app/graphql/mutations/create_component.rb @@ -8,7 +8,7 @@ class CreateComponent < BaseMutation field :component, Types::ComponentType, description: 'The component that has been created' def resolve(**input) - project = GlobalID.find("Z2lkOi8vYXBwL1Byb2plY3QvMmNmOTllOWItNGQzZS00MDQ2LWFjMWEtY2Q3MzMwNTFiNjA2") + project = GlobalID.find(input[:id]) raise GraphQL::ExecutionError, 'Project not found' unless project diff --git a/app/graphql/types/create_component_input_type.rb b/app/graphql/types/create_component_input_type.rb index bddb127e0..6bad20ed2 100644 --- a/app/graphql/types/create_component_input_type.rb +++ b/app/graphql/types/create_component_input_type.rb @@ -4,9 +4,7 @@ module Types class CreateComponentInputType < Types::BaseInputObject description 'Represents a component during creation' - argument :identifier, String, required: true, description: 'The easy-to-rememeber identifier of the project' - + argument :id, String, required: true, description: 'The easy-to-rememeber identifier of the project' argument :components, [Types::ProjectComponentInputType], required: false, description: 'Any project components' - end end From 9ccc3a2fae635db25a15a020f4519d4c99ad9406 Mon Sep 17 00:00:00 2001 From: chrisruk Date: Fri, 10 Mar 2023 11:38:07 +0000 Subject: [PATCH 5/9] New component mutation changes --- app/graphql/mutations/create_component.rb | 21 ++++++++----------- .../types/create_component_input_type.rb | 7 +++++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/graphql/mutations/create_component.rb b/app/graphql/mutations/create_component.rb index 2ec25e1ab..d5ac9dc3b 100644 --- a/app/graphql/mutations/create_component.rb +++ b/app/graphql/mutations/create_component.rb @@ -8,29 +8,26 @@ class CreateComponent < BaseMutation field :component, Types::ComponentType, description: 'The component that has been created' def resolve(**input) - project = GlobalID.find(input[:id]) + project = GlobalID.find(input[:project_id]) raise GraphQL::ExecutionError, 'Project not found' unless project - unless context[:current_ability].can?(:update, 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 project' + 'You are not permitted to update this component' end - newc = [] - input[:components].each {|component| - newc.append(Component.new component.to_h) - } - return { project: } if project.update({components: project.components.append(newc)}) + return { component: } if component.save() - raise GraphQL::ExecutionError, project.errors.full_messages.join(', ') + raise GraphQL::ExecutionError, component.errors.full_messages.join(', ') end def ready?(**_args) - print("here2") - print("uid",context[:current_user_id]) - return true if context[:current_ability]&.can?(:create, Component, user_id: context[:current_user_id]) + return true if context[:current_ability]&.can?(:create, Component, Project.new(user_id: context[:current_user_id])) raise GraphQL::ExecutionError, 'You are not permitted to create a component' end diff --git a/app/graphql/types/create_component_input_type.rb b/app/graphql/types/create_component_input_type.rb index 6bad20ed2..ded6f31e9 100644 --- a/app/graphql/types/create_component_input_type.rb +++ b/app/graphql/types/create_component_input_type.rb @@ -4,7 +4,10 @@ module Types class CreateComponentInputType < Types::BaseInputObject description 'Represents a component during creation' - argument :id, String, required: true, description: 'The easy-to-rememeber identifier of the project' - argument :components, [Types::ProjectComponentInputType], required: false, description: 'Any project components' + 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 From 176fc8682817053892a9ce81629d4daeb1b3cc01 Mon Sep 17 00:00:00 2001 From: chrisruk Date: Fri, 10 Mar 2023 12:35:34 +0000 Subject: [PATCH 6/9] Initial create component spec --- .../create_component_mutation_spec.rb | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 spec/graphql/mutations/create_component_mutation_spec.rb diff --git a/spec/graphql/mutations/create_component_mutation_spec.rb b/spec/graphql/mutations/create_component_mutation_spec.rb new file mode 100644 index 000000000..d37b0e92f --- /dev/null +++ b/spec/graphql/mutations/create_component_mutation_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'mutation CreateComponent() { ... }' do + subject(:result) { execute_query(query: mutation, variables:) } + + 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 + userid = SecureRandom.uuid + let(:current_user_id) { userid } + let!(:project) { create(:project, user_id: userid) } + let(:project_id) { project.to_gid_param } + + before { mock_phrase_generation } + + it 'returns the component ID' do + expect(result.dig('data', 'createComponent', 'component', 'id')).not_to be_nil + end + + end +end From eb214279012af363fce3554f58e4743d4cbd42a8 Mon Sep 17 00:00:00 2001 From: chrisruk Date: Fri, 10 Mar 2023 15:38:58 +0000 Subject: [PATCH 7/9] Create component spec --- .../create_component_mutation_spec.rb | 39 +++++++++++++++---- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/spec/graphql/mutations/create_component_mutation_spec.rb b/spec/graphql/mutations/create_component_mutation_spec.rb index d37b0e92f..ecd3a7172 100644 --- a/spec/graphql/mutations/create_component_mutation_spec.rb +++ b/spec/graphql/mutations/create_component_mutation_spec.rb @@ -5,8 +5,11 @@ 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(:project_id) { 'dummy-id' } let(:variables) do { component: { @@ -40,16 +43,38 @@ end context 'when authenticated' do - userid = SecureRandom.uuid - let(:current_user_id) { userid } - let!(:project) { create(:project, user_id: userid) } - let(:project_id) { project.to_gid_param } - - before { mock_phrase_generation } + 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 From 37b7c3c091efe739c1d7617d23c09886913e6166 Mon Sep 17 00:00:00 2001 From: chrisruk Date: Mon, 13 Mar 2023 10:23:04 +0000 Subject: [PATCH 8/9] Update schema --- app/graphql/mutations/create_component.rb | 7 ++- app/graphql/types/mutation_type.rb | 2 +- db/schema.graphql | 60 +++++++++++++++++++ .../create_component_mutation_spec.rb | 17 +++--- 4 files changed, 74 insertions(+), 12 deletions(-) diff --git a/app/graphql/mutations/create_component.rb b/app/graphql/mutations/create_component.rb index d5ac9dc3b..421733d30 100644 --- a/app/graphql/mutations/create_component.rb +++ b/app/graphql/mutations/create_component.rb @@ -20,14 +20,15 @@ def resolve(**input) 'You are not permitted to update this component' end - - return { component: } if component.save() + return { component: } if component.save raise GraphQL::ExecutionError, component.errors.full_messages.join(', ') end def ready?(**_args) - return true if context[:current_ability]&.can?(:create, Component, Project.new(user_id: context[:current_user_id])) + 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 diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 6786f4e94..62d976e3d 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -2,9 +2,9 @@ module Types class MutationType < Types::BaseObject + 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 :update_project, mutation: Mutations::UpdateProject, description: 'Update fields on an existing project' - field :create_component, mutation: Mutations::CreateComponent, description: 'Create a component' end end diff --git a/db/schema.graphql b/db/schema.graphql index 169f6039a..f1ff495eb 100644 --- a/db/schema.graphql +++ b/db/schema.graphql @@ -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 """ @@ -219,6 +269,16 @@ type ImageEdge { } type Mutation { + """ + Create a component + """ + createComponent( + """ + Parameters for CreateComponent + """ + input: CreateComponentInput! + ): CreateComponentPayload + """ Create a project, complete with components """ diff --git a/spec/graphql/mutations/create_component_mutation_spec.rb b/spec/graphql/mutations/create_component_mutation_spec.rb index ecd3a7172..65e39ee18 100644 --- a/spec/graphql/mutations/create_component_mutation_spec.rb +++ b/spec/graphql/mutations/create_component_mutation_spec.rb @@ -9,15 +9,15 @@ let(:project_id) { project.to_gid_param } let(:mutation) { 'mutation CreateComponent($component: CreateComponentInput!) { createComponent(input: $component) { component { id } } }' } - #let(:project_id) { 'dummy-id' } + # let(:project_id) { 'dummy-id' } let(:variables) do { component: { projectId: project_id, - name:"test", - extension:"py", - content:"blah", - default:false + name: 'test', + extension: 'py', + content: 'blah', + default: false } } end @@ -47,11 +47,12 @@ 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 + 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 @@ -59,6 +60,7 @@ 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 @@ -70,11 +72,10 @@ 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 From dbe73d52e9814015c09825fbe81b351b109e843e Mon Sep 17 00:00:00 2001 From: chrisruk Date: Mon, 13 Mar 2023 10:25:50 +0000 Subject: [PATCH 9/9] Fix rubocop issue --- app/graphql/types/mutation_type.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 62d976e3d..14310bd2f 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -2,9 +2,11 @@ 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 :update_project, mutation: Mutations::UpdateProject, description: 'Update fields on an existing project' + # rubocop:enable GraphQL/ExtractType end end