GraphQL Resolvers¶
A GraphQL mutation should do three things: accept input, run business logic once, and return a payload the client understands.
Put the business logic in a CMDx task. The resolver stays a thin adapter — and the same task remains callable from jobs, rake tasks, and specs.
Setup — shared payload helper¶
Map a Result to a consistent shape: success, errors, plus any extra fields (like the created record).
class Mutations::ApplicationMutation < GraphQL::Schema::Mutation
def self.payload_for(result, **extras)
if result.success?
{ success: true, errors: [] }.merge(extras)
else
{ success: false, errors: user_errors(result) }
end
end
def self.user_errors(result)
field_errors = result.errors.to_h.flat_map do |field, messages|
messages.map { |message| { message:, path: ["input", field.to_s] } }
end
field_errors.presence || [{ message: result.reason, path: [] }]
end
end
Usage¶
Task on the inside, resolver on the outside:
class CreateInvoice < CMDx::Task
required :customer_id, coerce: :integer
required :amount_cents, coerce: :integer
def work
context.invoice = Invoice.create!(customer_id:, amount_cents:)
end
end
class Mutations::CreateInvoice < Mutations::ApplicationMutation
argument :customer_id, ID, required: true
argument :amount_cents, Integer, required: true
field :invoice, Types::InvoiceType, null: true
field :success, Boolean, null: false
field :errors, [Types::UserErrorType], null: false
def resolve(**args)
result = CreateInvoice.execute(**args, current_user: context[:current_user])
self.class.payload_for(result, invoice: result.context.invoice)
end
end
Notes¶
Trace ids in development¶
Expose result.tid and result.cid on a debug field (or error extension) outside production. One id links the resolver log line to the task log line and any nested tasks.
Don't use execute! in resolvers¶
execute! raises CMDx::Fault on failure. graphql-ruby turns unhandled exceptions into opaque internal errors and hides result.reason. Prefer execute and translate the Result yourself.