Class: ZeroRuby::Mutation

Inherits:
Object
  • Object
show all
Includes:
TypeNames
Defined in:
lib/zero_ruby/mutation.rb

Overview

Base class for Zero mutations. Provides argument DSL with dry-types validation.

Includes ZeroRuby::TypeNames for convenient type access via the Types module (e.g., Types::String, Types::ID, Types::Boolean).

By default (auto_transact: true), the entire execute method runs inside a transaction with LMID tracking. For 3-phase control, set auto_transact false.

Examples:

Simple mutation (auto_transact: true, default)

class WorkCreate < ZeroRuby::Mutation
  argument :id, Types::ID
  argument :title, Types::String.constrained(max_size: 200)

  def execute(id:, title:)
    authorize! Work, to: :create?
    Work.create!(id: id, title: title)  # Runs inside auto-wrapped transaction
  end
end

3-phase mutation (skip_auto_transaction)

class WorkUpdate < ZeroRuby::Mutation
  skip_auto_transaction

  argument :id, Types::ID
  argument :title, Types::String

  def execute(id:, title:)
    work = Work.find(id)
    authorize! work, to: :update?  # Pre-transaction

    transact do
      work.update!(title: title)   # Transaction
    end

    notify_update(work)            # Post-commit
  end
end

Constant Summary

Constants included from TypeNames

TypeNames::Types

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(raw_args, ctx) ⇒ Mutation

Initialize a mutation with raw arguments and context

Parameters:

  • raw_args (Hash)

    Raw input arguments (will be coerced and validated)

  • ctx (Hash)

    The context hash



206
207
208
209
# File 'lib/zero_ruby/mutation.rb', line 206

def initialize(raw_args, ctx)
  @ctx = ctx
  @args = self.class.coerce_and_validate!(raw_args)
end

Instance Attribute Details

#argsObject (readonly)

The validated arguments hash



54
55
56
# File 'lib/zero_ruby/mutation.rb', line 54

def args
  @args
end

#ctxObject (readonly)

The context hash containing current_user, etc.



51
52
53
# File 'lib/zero_ruby/mutation.rb', line 51

def ctx
  @ctx
end

Class Method Details

.argument(name, type, description: nil) ⇒ Object

Declare an argument for this mutation

Parameters:

  • name (Symbol)

    The argument name

  • type (Dry::Types::Type)

    The type (from ZeroRuby::Types or dry-types)

  • description (String, nil) (defaults to: nil)

    Optional description for documentation



76
77
78
79
80
81
82
# File 'lib/zero_ruby/mutation.rb', line 76

def argument(name, type, description: nil)
  arguments[name.to_sym] = {
    type: type,
    description: description,
    name: name.to_sym
  }
end

.argumentsHash<Symbol, Hash>

Get all declared arguments for this mutation (including inherited)

Returns:

  • (Hash<Symbol, Hash>)

    Map of argument name to config



86
87
88
89
90
91
92
# File 'lib/zero_ruby/mutation.rb', line 86

def arguments
  @arguments ||= if superclass.respond_to?(:arguments)
    superclass.arguments.dup
  else
    {}
  end
end

.coerce_and_validate!(raw_args) ⇒ Hash

Coerce and validate raw arguments. Collects ALL validation errors (missing fields, type coercion, constraints) and raises a single ValidationError with all issues.

Uses type.try(value) which returns a Result instead of raising, allowing us to collect all errors in one pass rather than failing on the first one. Works for both Dry::Types (scalars) and Dry::Struct (InputObjects).

Parameters:

  • raw_args (Hash)

    Raw input arguments (string keys from JSON)

Returns:

  • (Hash)

    Validated and coerced arguments (symbol keys, may contain InputObject instances)

Raises:



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/zero_ruby/mutation.rb', line 105

def coerce_and_validate!(raw_args)
  # Result hash: symbol keys → coerced values (strings, integers, InputObject instances, etc.)
  # eg:
  # raw_args:  {"name" => "test", "count" => "5"}  # string keys, raw values
  # validated: {name: "test", count: 5}            # symbol keys, coerced values
  validated = {}
  errors = []

  arguments.each do |name, config|
    type = config[:type]
    str_key = name.to_s
    key_present = raw_args.key?(str_key)
    value = raw_args[str_key]
    is_input_object = input_object_type?(type)

    # Missing key: use default if available, otherwise error if required
    unless key_present
      if has_default?(type)
        validated[name] = get_default(type)
      elsif required_type?(type)
        errors << "#{name} is required"
      end
      # Optional fields without defaults are simply omitted from result
      next
    end

    # Explicit null: InputObjects always allow nil (they handle optionality internally),
    # scalars only allow nil if the type is optional
    if value.nil?
      if is_input_object || !required_type?(type)
        validated[name] = nil
      else
        errors << "#{name} is required"
      end
      next
    end

    # Coerce value: type.try returns Result instead of raising, so we can
    # collect all errors. Works for both Dry::Types and Dry::Struct.
    result = type.try(value)
    if result.failure?
      errors << format_type_error(name, result.error, is_input_object)
    else
      validated[name] = result.input
    end
  end

  raise ValidationError.new(errors) if errors.any?
  validated
end

.skip_auto_transactionvoid

This method returns an undefined value.

Opt-out of auto-transaction wrapping. By default, execute is wrapped in a transaction with LMID tracking. Call this to use explicit 3-phase model where you must call transact { }.



62
63
64
# File 'lib/zero_ruby/mutation.rb', line 62

def skip_auto_transaction
  @skip_auto_transaction = true
end

.skip_auto_transaction?Boolean

Check if auto-transaction is skipped for this mutation

Returns:

  • (Boolean)

    true if skip_auto_transaction was called



68
69
70
# File 'lib/zero_ruby/mutation.rb', line 68

def skip_auto_transaction?
  @skip_auto_transaction == true
end

Instance Method Details

#call(&transact_proc) ⇒ Hash

Execute the mutation

Parameters:

  • transact_proc (Proc)

    Block that wraps transactional work (internal use)

Returns:

  • (Hash)

    Empty hash on success, or … if execute returns a Hash

Raises:



216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/zero_ruby/mutation.rb', line 216

def call(&transact_proc)
  @transact_proc = transact_proc
  @transact_called = false

  if self.class.skip_auto_transaction?
    # Manual mode: Use defined mutation calls transact {}
    data = execute(**@args)
    raise TransactNotCalledError.new unless @transact_called
  else
    # Auto mode: wrap entire execute in transaction
    data = transact_proc.call { execute(**@args) }
  end

  result = {}
  result[:data] = data if data.is_a?(Hash) && !data.empty?
  result
end