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

GraphQL: Clone Samples and Transfer Samples mutations #814

Merged
merged 18 commits into from
Nov 1, 2024
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
109 changes: 109 additions & 0 deletions app/graphql/mutations/clone_samples.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# frozen_string_literal: true

module Mutations
# Base Mutation
class CloneSamples < BaseMutation
null true
description 'Copy samples to another project.'
argument :new_project_id, ID,
required: false,
description: 'The Node ID of the project to copy to. For example, `gid://irida/Project/a84cd757-dedb-4c64-8b01-097020163077`.' # rubocop:disable Layout/LineLength
argument :new_project_puid, ID,
required: false,
description: 'Persistent Unique Identifier of the project to copy to. For example, `INXT_PRJ_AAAAAAAAAA`.'
argument :project_id, ID, # rubocop:disable GraphQL/ExtractInputType
required: false,
description: 'The Node ID of the project to copy to. For example, `gid://irida/Project/a84cd757-dedb-4c64-8b01-097020163077`.' # rubocop:disable Layout/LineLength
argument :project_puid, ID, # rubocop:disable GraphQL/ExtractInputType
required: false,
description: 'Persistent Unique Identifier of the project to copy to. For example, `INXT_PRJ_AAAAAAAAAA`.'

argument :sample_ids, [ID], required: true, description: 'List of samples to copy.' # rubocop:disable GraphQL/ExtractInputType
validates required: { one_of: %i[new_project_id new_project_puid] }
validates required: { one_of: %i[project_id project_puid] }

field :errors, [Types::UserErrorType], null: false, description: 'A list of errors that prevented the mutation.'
field :samples, GraphQL::Types::JSON, description: 'List of original and copied sample ids.'

def resolve(args) # rubocop:disable Metrics/MethodLength
project = get_project_from_id_or_puid_args(args)

if project.nil? || !project.persisted?
user_errors = [{
path: ['project'],
message: 'Project not found by provided ID or PUID'
}]
return {
samples: nil,
errors: user_errors
}
end

new_project_args = { project_id: args[:new_project_id], project_puid: args[:new_project_puid] }
new_project = get_project_from_id_or_puid_args(new_project_args)

if new_project.nil? || !new_project.persisted?
user_errors = [{
path: ['new_project'],
message: 'Project not found by provided ID or PUID'
}]
return {
samples: nil,
errors: user_errors
}
end

clone_samples(project, new_project.id, args[:sample_ids])
end

def ready?(**_args)
authorize!(to: :mutate?, with: GraphqlPolicy, context: { user: context[:current_user], token: context[:token] })
end

private

def clone_samples(project, new_project_id, sample_gids) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
user_errors = []
# remove prefix from sample_ids
sample_ids = sample_gids.map do |sample_gid|
IridaSchema.parse_gid(sample_gid, { expected_type: Sample }).model_id
rescue GraphQL::CoercionError => e
user_errors.append(
{
path: ['copySamples'],
message: e.message
}
)
next
end

samples = Samples::CloneService.new(
project, current_user
).execute(new_project_id, sample_ids.compact)

prepended_samples = []
unless samples.empty?
# add the prefix to sample_ids
prepended_samples = samples.map do |key, value|
{ original: URI::GID.build(app: GlobalID.app, model_name: Sample.name, model_id: key).to_s,
copy: URI::GID.build(app: GlobalID.app, model_name: Sample.name, model_id: value).to_s }
end
end

project_user_errors = []
if project.errors.count.positive?
project_user_errors = project.errors.map do |error|
{
path: ['samples', error.attribute.to_s.camelize(:lower)],
message: error.message
}
end
end

{
samples: prepended_samples,
errors: user_errors.concat(project_user_errors)
}
end
end
end
109 changes: 109 additions & 0 deletions app/graphql/mutations/transfer_samples.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# frozen_string_literal: true

module Mutations
# Base Mutation
class TransferSamples < BaseMutation
null true
description 'Transfer a list of sample to another project.'
argument :new_project_id, ID,
required: false,
description: 'The Node ID of the project to transfer to. For example, `gid://irida/Project/a84cd757-dedb-4c64-8b01-097020163077`.' # rubocop:disable Layout/LineLength
argument :new_project_puid, ID,
required: false,
description: 'Persistent Unique Identifier of the project to transfer to. For example, `INXT_PRJ_AAAAAAAAAA`.' # rubocop:disable Layout/LineLength
argument :project_id, ID, # rubocop:disable GraphQL/ExtractInputType
required: false,
description: 'The Node ID of the project to transfer to. For example, `gid://irida/Project/a84cd757-dedb-4c64-8b01-097020163077`.' # rubocop:disable Layout/LineLength
argument :project_puid, ID, # rubocop:disable GraphQL/ExtractInputType
required: false,
description: 'Persistent Unique Identifier of the project to transfer to. For example, `INXT_PRJ_AAAAAAAAAA`.' # rubocop:disable Layout/LineLength

argument :sample_ids, [ID], required: true, description: 'List of samples to transfer.' # rubocop:disable GraphQL/ExtractInputType
validates required: { one_of: %i[new_project_id new_project_puid] }
validates required: { one_of: %i[project_id project_puid] }

field :errors, [Types::UserErrorType], null: false, description: 'A list of errors that prevented the mutation.'
field :samples, [ID], description: 'List of transfered sample ids.'

def resolve(args) # rubocop:disable Metrics/MethodLength
project = get_project_from_id_or_puid_args(args)

if project.nil? || !project.persisted?
user_errors = [{
path: ['project'],
message: 'Project not found by provided ID or PUID'
}]
return {
samples: nil,
errors: user_errors
}
end

new_project_args = { project_id: args[:new_project_id], project_puid: args[:new_project_puid] }
new_project = get_project_from_id_or_puid_args(new_project_args)

if new_project.nil? || !new_project.persisted?
user_errors = [{
path: ['new_project'],
message: 'Project not found by provided ID or PUID'
}]
return {
samples: nil,
errors: user_errors
}
end

transfer_samples(project, new_project.id, args[:sample_ids])
end

def ready?(**_args)
authorize!(to: :mutate?, with: GraphqlPolicy, context: { user: context[:current_user], token: context[:token] })
end

private

def transfer_samples(project, new_project_id, sample_gids) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
user_errors = []
# remove prefix from sample_ids
sample_ids = sample_gids.map do |sample_gid|
IridaSchema.parse_gid(sample_gid, { expected_type: Sample }).model_id
rescue GraphQL::CoercionError => e
user_errors.append(
{
path: ['transferSamples'],
message: e.message
}
)
next
end

samples = Samples::TransferService.new(
project, current_user
).execute(new_project_id, sample_ids.compact)

if samples.empty? # rubocop:disable Style/ConditionalAssignment
samples = nil
else
# add the prefix to sample_ids
samples = samples.map do |sample_id|
URI::GID.build(app: GlobalID.app, model_name: Sample.name, model_id: sample_id).to_s
end
end

project_user_errors = []
if project.errors.count.positive?
project_user_errors = project.errors.map do |error|
{
path: ['samples', error.attribute.to_s.camelize(:lower)],
message: error.message
}
end
end

{
samples:,
errors: user_errors.concat(project_user_errors)
}
end
end
end
130 changes: 130 additions & 0 deletions app/graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,61 @@ enum AttachmentOrderField {
updated_at
}

"""
Autogenerated input type of CloneSamples
"""
input CloneSamplesInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String

"""
The Node ID of the project to copy to. For example, `gid://irida/Project/a84cd757-dedb-4c64-8b01-097020163077`.
"""
newProjectId: ID

"""
Persistent Unique Identifier of the project to copy to. For example, `INXT_PRJ_AAAAAAAAAA`.
"""
newProjectPuid: ID

"""
The Node ID of the project to copy to. For example, `gid://irida/Project/a84cd757-dedb-4c64-8b01-097020163077`.
"""
projectId: ID

"""
Persistent Unique Identifier of the project to copy to. For example, `INXT_PRJ_AAAAAAAAAA`.
"""
projectPuid: ID

"""
List of samples to copy.
"""
sampleIds: [ID!]!
}

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

"""
A list of errors that prevented the mutation.
"""
errors: [UserError!]!

"""
List of original and copied sample ids.
"""
samples: JSON
}

"""
Autogenerated input type of CreateDirectUpload
"""
Expand Down Expand Up @@ -1311,6 +1366,16 @@ type Mutation {
input: AttachFilesToSampleInput!
): AttachFilesToSamplePayload

"""
Copy samples to another project.
"""
copySamples(
"""
Parameters for CloneSamples
"""
input: CloneSamplesInput!
): CloneSamplesPayload

"""
Create blob to upload data to.
"""
Expand Down Expand Up @@ -1351,6 +1416,16 @@ type Mutation {
input: CreateSampleInput!
): CreateSamplePayload

"""
Transfer a list of sample to another project.
"""
transferSamples(
"""
Parameters for TransferSamples
"""
input: TransferSamplesInput!
): TransferSamplesPayload

"""
Update metadata for a sample.
"""
Expand Down Expand Up @@ -2630,6 +2705,61 @@ enum SampleOrderField {
updated_at
}

"""
Autogenerated input type of TransferSamples
"""
input TransferSamplesInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String

"""
The Node ID of the project to transfer to. For example, `gid://irida/Project/a84cd757-dedb-4c64-8b01-097020163077`.
"""
newProjectId: ID

"""
Persistent Unique Identifier of the project to transfer to. For example, `INXT_PRJ_AAAAAAAAAA`.
"""
newProjectPuid: ID

"""
The Node ID of the project to transfer to. For example, `gid://irida/Project/a84cd757-dedb-4c64-8b01-097020163077`.
"""
projectId: ID

"""
Persistent Unique Identifier of the project to transfer to. For example, `INXT_PRJ_AAAAAAAAAA`.
"""
projectPuid: ID

"""
List of samples to transfer.
"""
sampleIds: [ID!]!
}

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

"""
A list of errors that prevented the mutation.
"""
errors: [UserError!]!

"""
List of transfered sample ids.
"""
samples: [ID!]
}

"""
Autogenerated input type of UpdateSampleMetadata
"""
Expand Down
2 changes: 2 additions & 0 deletions app/graphql/types/mutation_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ class MutationType < Types::BaseObject
field :attach_files_to_group, mutation: Mutations::AttachFilesToGroup # rubocop:disable GraphQL/FieldDescription
field :attach_files_to_project, mutation: Mutations::AttachFilesToProject # rubocop:disable GraphQL/FieldDescription
field :attach_files_to_sample, mutation: Mutations::AttachFilesToSample # rubocop:disable GraphQL/FieldDescription, GraphQL/ExtractType
field :copy_samples, mutation: Mutations::CloneSamples # rubocop:disable GraphQL/FieldDescription
field :create_direct_upload, mutation: Mutations::CreateDirectUpload # rubocop:disable GraphQL/FieldDescription
field :create_group, mutation: Mutations::CreateGroup # rubocop:disable GraphQL/FieldDescription
field :create_project, mutation: Mutations::CreateProject # rubocop:disable GraphQL/FieldDescription
field :create_sample, mutation: Mutations::CreateSample # rubocop:disable GraphQL/FieldDescription,GraphQL/ExtractType
field :transfer_samples, mutation: Mutations::TransferSamples # rubocop:disable GraphQL/FieldDescription
field :update_sample_metadata, mutation: Mutations::UpdateSampleMetadata # rubocop:disable GraphQL/FieldDescription
end
end
Loading
Loading