zero_ruby 0.1.0.alpha1 → 0.1.0.alpha4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +141 -90
- data/lib/zero_ruby/configuration.rb +0 -5
- data/lib/zero_ruby/error_formatter.rb +171 -0
- data/lib/zero_ruby/errors.rb +24 -0
- data/lib/zero_ruby/input_object.rb +56 -93
- data/lib/zero_ruby/lmid_stores/active_record_store.rb +0 -1
- data/lib/zero_ruby/mutation.rb +206 -80
- data/lib/zero_ruby/push_processor.rb +134 -37
- data/lib/zero_ruby/schema.rb +64 -16
- data/lib/zero_ruby/type_names.rb +13 -8
- data/lib/zero_ruby/types.rb +54 -0
- data/lib/zero_ruby/typescript_generator.rb +126 -58
- data/lib/zero_ruby/version.rb +1 -1
- data/lib/zero_ruby.rb +11 -34
- metadata +48 -22
- data/lib/zero_ruby/argument.rb +0 -75
- data/lib/zero_ruby/types/base_type.rb +0 -54
- data/lib/zero_ruby/types/big_int.rb +0 -32
- data/lib/zero_ruby/types/boolean.rb +0 -30
- data/lib/zero_ruby/types/float.rb +0 -31
- data/lib/zero_ruby/types/id.rb +0 -33
- data/lib/zero_ruby/types/integer.rb +0 -31
- data/lib/zero_ruby/types/iso8601_date.rb +0 -43
- data/lib/zero_ruby/types/iso8601_date_time.rb +0 -43
- data/lib/zero_ruby/types/string.rb +0 -20
- data/lib/zero_ruby/validator.rb +0 -69
- data/lib/zero_ruby/validators/allow_blank_validator.rb +0 -31
- data/lib/zero_ruby/validators/allow_null_validator.rb +0 -26
- data/lib/zero_ruby/validators/exclusion_validator.rb +0 -29
- data/lib/zero_ruby/validators/format_validator.rb +0 -35
- data/lib/zero_ruby/validators/inclusion_validator.rb +0 -30
- data/lib/zero_ruby/validators/length_validator.rb +0 -42
- data/lib/zero_ruby/validators/numericality_validator.rb +0 -63
|
@@ -1,120 +1,83 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require_relative "
|
|
5
|
-
require_relative "
|
|
3
|
+
require "dry-struct"
|
|
4
|
+
require_relative "types"
|
|
5
|
+
require_relative "type_names"
|
|
6
6
|
|
|
7
7
|
module ZeroRuby
|
|
8
8
|
# Base class for input objects (nested argument types).
|
|
9
|
-
#
|
|
9
|
+
# Uses Dry::Struct for type coercion and validation.
|
|
10
|
+
#
|
|
11
|
+
# Includes ZeroRuby::TypeNames for convenient type access:
|
|
12
|
+
# - ID, Boolean, ISO8601Date, ISO8601DateTime (direct constants)
|
|
13
|
+
# - Types::String, Types::Integer, Types::Float (via Types module)
|
|
10
14
|
#
|
|
11
15
|
# @example
|
|
12
16
|
# class Types::PostInput < ZeroRuby::InputObject
|
|
13
|
-
# argument :id, ID
|
|
14
|
-
# argument :title, String,
|
|
15
|
-
# argument :body, String
|
|
17
|
+
# argument :id, ID
|
|
18
|
+
# argument :title, Types::String.constrained(min_size: 1, max_size: 200)
|
|
19
|
+
# argument :body, Types::String.optional
|
|
20
|
+
# argument :published, Boolean.default(false)
|
|
16
21
|
# end
|
|
17
22
|
#
|
|
18
23
|
# class PostCreate < ZeroRuby::Mutation
|
|
19
|
-
# argument :post_input, Types::PostInput
|
|
20
|
-
# argument :notify, Boolean
|
|
24
|
+
# argument :post_input, Types::PostInput
|
|
25
|
+
# argument :notify, Boolean.default(false)
|
|
21
26
|
#
|
|
22
|
-
# def execute
|
|
23
|
-
#
|
|
27
|
+
# def execute
|
|
28
|
+
# # args[:post_input].title, args[:post_input].body, etc.
|
|
29
|
+
# # Or use **args[:post_input] to splat into method calls
|
|
24
30
|
# end
|
|
25
31
|
# end
|
|
26
|
-
class InputObject
|
|
27
|
-
include TypeNames
|
|
32
|
+
class InputObject < Dry::Struct
|
|
33
|
+
include ZeroRuby::TypeNames
|
|
34
|
+
|
|
35
|
+
# Transform string keys to symbols (for JSON input)
|
|
36
|
+
transform_keys(&:to_sym)
|
|
37
|
+
|
|
38
|
+
# Use permissive schema that allows omitting optional attributes
|
|
39
|
+
# This matches the behavior where missing optional keys are omitted from result
|
|
40
|
+
schema schema.strict(false)
|
|
28
41
|
|
|
29
42
|
class << self
|
|
30
|
-
#
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
default: default,
|
|
38
|
-
description: description,
|
|
39
|
-
**options
|
|
40
|
-
)
|
|
41
|
-
end
|
|
43
|
+
# Alias attribute to argument for DSL compatibility
|
|
44
|
+
# @param name [Symbol] The argument name
|
|
45
|
+
# @param type [Dry::Types::Type] The type (from ZeroRuby::Types)
|
|
46
|
+
# @param description [String, nil] Optional description (stored for documentation, not passed to Dry::Struct)
|
|
47
|
+
def argument(name, type, description: nil, **_options)
|
|
48
|
+
# Store description for documentation/TypeScript generation
|
|
49
|
+
argument_descriptions[name.to_sym] = description if description
|
|
42
50
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
# Use attribute? for optional types (allows key to be omitted)
|
|
52
|
+
# Use attribute for required types (key must be present)
|
|
53
|
+
if optional_type?(type)
|
|
54
|
+
attribute?(name, type)
|
|
47
55
|
else
|
|
48
|
-
|
|
56
|
+
attribute(name, type)
|
|
49
57
|
end
|
|
50
58
|
end
|
|
51
59
|
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
# @raise [ZeroRuby::ValidationError] If validation fails
|
|
57
|
-
def coerce(value, ctx)
|
|
58
|
-
return nil if value.nil?
|
|
59
|
-
return nil unless value.is_a?(Hash)
|
|
60
|
-
|
|
61
|
-
validated = {}
|
|
62
|
-
errors = []
|
|
63
|
-
|
|
64
|
-
arguments.each do |name, arg|
|
|
65
|
-
key_present = value.key?(name) || value.key?(name.to_s)
|
|
66
|
-
val = if key_present
|
|
67
|
-
value[name].nil? ? value[name.to_s] : value[name]
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# Check required
|
|
71
|
-
if arg.required? && !key_present && !arg.has_default?
|
|
72
|
-
errors << "#{name} is required"
|
|
73
|
-
next
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# Apply default if key not present
|
|
77
|
-
if !key_present && arg.has_default?
|
|
78
|
-
validated[name] = arg.default
|
|
79
|
-
next
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# Skip if key not present (don't add to validated hash)
|
|
83
|
-
next unless key_present
|
|
84
|
-
|
|
85
|
-
# Handle nil values - include in hash but skip coercion
|
|
86
|
-
if val.nil?
|
|
87
|
-
validated[name] = nil
|
|
88
|
-
next
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
# Type coercion (handles nested InputObjects)
|
|
92
|
-
begin
|
|
93
|
-
coerced = arg.coerce(val, ctx)
|
|
94
|
-
rescue CoercionError => e
|
|
95
|
-
errors << "#{name}: #{e.message}"
|
|
96
|
-
next
|
|
97
|
-
rescue ValidationError => e
|
|
98
|
-
# Nested InputObject validation errors - prefix with field name
|
|
99
|
-
e.errors.each do |err|
|
|
100
|
-
errors << "#{name}.#{err}"
|
|
101
|
-
end
|
|
102
|
-
next
|
|
103
|
-
end
|
|
60
|
+
# Get stored argument descriptions
|
|
61
|
+
def argument_descriptions
|
|
62
|
+
@argument_descriptions ||= {}
|
|
63
|
+
end
|
|
104
64
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
end
|
|
111
|
-
end
|
|
65
|
+
# Check if a type is optional (can accept nil or has a default)
|
|
66
|
+
def optional_type?(type)
|
|
67
|
+
return false unless type.respond_to?(:optional?)
|
|
68
|
+
type.optional? || (type.respond_to?(:default?) && type.default?)
|
|
69
|
+
end
|
|
112
70
|
|
|
113
|
-
|
|
71
|
+
# Returns argument metadata for TypeScript generation
|
|
72
|
+
# @return [Hash<Symbol, Hash>] Map of argument name to metadata
|
|
73
|
+
def arguments_metadata
|
|
74
|
+
schema.keys.each_with_object({}) do |key, hash|
|
|
75
|
+
hash[key.name] = {
|
|
76
|
+
type: key.type,
|
|
77
|
+
required: key.required?,
|
|
78
|
+
name: key.name
|
|
79
|
+
}
|
|
114
80
|
end
|
|
115
|
-
|
|
116
|
-
raise ValidationError.new(errors) if errors.any?
|
|
117
|
-
validated
|
|
118
81
|
end
|
|
119
82
|
end
|
|
120
83
|
end
|
data/lib/zero_ruby/mutation.rb
CHANGED
|
@@ -1,38 +1,88 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "
|
|
3
|
+
require_relative "types"
|
|
4
|
+
require_relative "type_names"
|
|
4
5
|
require_relative "errors"
|
|
5
|
-
require_relative "validator"
|
|
6
6
|
|
|
7
7
|
module ZeroRuby
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
# Base class for Zero mutations.
|
|
9
|
+
# Provides argument DSL with dry-types validation.
|
|
10
|
+
#
|
|
11
|
+
# Includes ZeroRuby::TypeNames for convenient type access:
|
|
12
|
+
# - ID, Boolean, ISO8601Date, ISO8601DateTime (direct constants)
|
|
13
|
+
# - Types::String, Types::Integer, Types::Float (via Types module)
|
|
14
|
+
#
|
|
15
|
+
# By default (auto_transact: true), the entire execute method runs inside
|
|
16
|
+
# a transaction with LMID tracking. For 3-phase control, set auto_transact false.
|
|
17
|
+
#
|
|
18
|
+
# @example Simple mutation (auto_transact: true, default)
|
|
19
|
+
# class WorkCreate < ZeroRuby::Mutation
|
|
20
|
+
# argument :id, ID
|
|
21
|
+
# argument :title, Types::String.constrained(max_size: 200)
|
|
22
|
+
#
|
|
23
|
+
# def execute(id:, title:)
|
|
24
|
+
# authorize! Work, to: :create?
|
|
25
|
+
# Work.create!(id: id, title: title) # Runs inside auto-wrapped transaction
|
|
26
|
+
# end
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# @example 3-phase mutation (skip_auto_transaction)
|
|
30
|
+
# class WorkUpdate < ZeroRuby::Mutation
|
|
31
|
+
# skip_auto_transaction
|
|
32
|
+
#
|
|
33
|
+
# argument :id, ID
|
|
34
|
+
# argument :title, Types::String
|
|
35
|
+
#
|
|
36
|
+
# def execute(id:, title:)
|
|
37
|
+
# work = Work.find(id)
|
|
38
|
+
# authorize! work, to: :update? # Pre-transaction
|
|
39
|
+
#
|
|
40
|
+
# transact do
|
|
41
|
+
# work.update!(title: title) # Transaction
|
|
42
|
+
# end
|
|
43
|
+
#
|
|
44
|
+
# notify_update(work) # Post-commit
|
|
45
|
+
# end
|
|
46
|
+
# end
|
|
47
|
+
class Mutation
|
|
48
|
+
include ZeroRuby::TypeNames
|
|
49
|
+
|
|
50
|
+
# The context hash containing current_user, etc.
|
|
51
|
+
attr_reader :ctx
|
|
52
|
+
|
|
53
|
+
# The validated arguments hash
|
|
54
|
+
attr_reader :args
|
|
55
|
+
|
|
56
|
+
class << self
|
|
57
|
+
# Opt-out of auto-transaction wrapping.
|
|
58
|
+
# By default, execute is wrapped in a transaction with LMID tracking.
|
|
59
|
+
# Call this to use explicit 3-phase model where you must call transact { }.
|
|
60
|
+
#
|
|
61
|
+
# @return [void]
|
|
62
|
+
def skip_auto_transaction
|
|
63
|
+
@skip_auto_transaction = true
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Check if auto-transaction is skipped for this mutation
|
|
67
|
+
# @return [Boolean] true if skip_auto_transaction was called
|
|
68
|
+
def skip_auto_transaction?
|
|
69
|
+
@skip_auto_transaction == true
|
|
70
|
+
end
|
|
14
71
|
|
|
15
|
-
module ClassMethods
|
|
16
72
|
# Declare an argument for this mutation
|
|
17
73
|
# @param name [Symbol] The argument name
|
|
18
|
-
# @param type [
|
|
19
|
-
# @param
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
# @param description [String] Description of the argument
|
|
23
|
-
def argument(name, type, required: true, validates: nil, default: Argument::NOT_PROVIDED, description: nil, **options)
|
|
24
|
-
arguments[name.to_sym] = Argument.new(
|
|
25
|
-
name: name,
|
|
74
|
+
# @param type [Dry::Types::Type] The type (from ZeroRuby::Types or dry-types)
|
|
75
|
+
# @param description [String, nil] Optional description for documentation
|
|
76
|
+
def argument(name, type, description: nil)
|
|
77
|
+
arguments[name.to_sym] = {
|
|
26
78
|
type: type,
|
|
27
|
-
required: required,
|
|
28
|
-
validates: validates,
|
|
29
|
-
default: default,
|
|
30
79
|
description: description,
|
|
31
|
-
|
|
32
|
-
|
|
80
|
+
name: name.to_sym
|
|
81
|
+
}
|
|
33
82
|
end
|
|
34
83
|
|
|
35
84
|
# Get all declared arguments for this mutation (including inherited)
|
|
85
|
+
# @return [Hash<Symbol, Hash>] Map of argument name to config
|
|
36
86
|
def arguments
|
|
37
87
|
@arguments ||= if superclass.respond_to?(:arguments)
|
|
38
88
|
superclass.arguments.dup
|
|
@@ -41,100 +91,176 @@ module ZeroRuby
|
|
|
41
91
|
end
|
|
42
92
|
end
|
|
43
93
|
|
|
44
|
-
# Coerce and validate raw arguments
|
|
94
|
+
# Coerce and validate raw arguments.
|
|
95
|
+
# Collects "field is required" errors for all missing required fields,
|
|
96
|
+
# but lets Dry exceptions bubble up for type/constraint errors.
|
|
97
|
+
#
|
|
45
98
|
# @param raw_args [Hash] Raw input arguments
|
|
46
|
-
# @
|
|
47
|
-
# @
|
|
48
|
-
# @raise [
|
|
49
|
-
|
|
99
|
+
# @return [Hash] Validated and coerced arguments (may contain InputObject instances)
|
|
100
|
+
# @raise [ZeroRuby::ValidationError] If required fields are missing
|
|
101
|
+
# @raise [Dry::Types::CoercionError] If type coercion fails
|
|
102
|
+
# @raise [Dry::Types::ConstraintError] If constraint validation fails
|
|
103
|
+
# @raise [Dry::Struct::Error] If InputObject validation fails
|
|
104
|
+
def coerce_and_validate!(raw_args)
|
|
50
105
|
validated = {}
|
|
51
106
|
errors = []
|
|
52
107
|
|
|
53
|
-
arguments.each do |name,
|
|
54
|
-
|
|
108
|
+
arguments.each do |name, config|
|
|
109
|
+
type = config[:type]
|
|
110
|
+
str_key = name.to_s
|
|
111
|
+
key_present = raw_args.key?(str_key)
|
|
112
|
+
value = raw_args[str_key]
|
|
55
113
|
|
|
56
|
-
#
|
|
57
|
-
if
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
114
|
+
# Handle InputObject types (subclasses of InputObject)
|
|
115
|
+
if input_object_type?(type)
|
|
116
|
+
if value.nil? && key_present
|
|
117
|
+
# Explicit nil
|
|
118
|
+
validated[name] = nil
|
|
119
|
+
elsif value.nil? && !key_present
|
|
120
|
+
# Missing key - check if required
|
|
121
|
+
if required_type?(type)
|
|
122
|
+
errors << "#{name} is required"
|
|
123
|
+
end
|
|
124
|
+
# Skip if optional and not provided
|
|
125
|
+
else
|
|
126
|
+
# Let Dry::Struct errors bubble up
|
|
127
|
+
validated[name] = type.new(value)
|
|
128
|
+
end
|
|
65
129
|
next
|
|
66
130
|
end
|
|
67
131
|
|
|
68
|
-
#
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
132
|
+
# Handle missing key
|
|
133
|
+
if !key_present
|
|
134
|
+
if has_default?(type)
|
|
135
|
+
validated[name] = get_default(type)
|
|
136
|
+
elsif required_type?(type)
|
|
137
|
+
errors << "#{name} is required"
|
|
138
|
+
end
|
|
73
139
|
next
|
|
74
140
|
end
|
|
75
141
|
|
|
76
|
-
#
|
|
77
|
-
if
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
142
|
+
# Handle explicit null
|
|
143
|
+
if value.nil?
|
|
144
|
+
if required_type?(type)
|
|
145
|
+
errors << "#{name} is required"
|
|
146
|
+
else
|
|
147
|
+
validated[name] = nil
|
|
81
148
|
end
|
|
149
|
+
next
|
|
82
150
|
end
|
|
83
151
|
|
|
84
|
-
|
|
152
|
+
# Coerce and validate value - let Dry exceptions bubble up
|
|
153
|
+
validated[name] = type[value]
|
|
85
154
|
end
|
|
86
155
|
|
|
87
156
|
raise ValidationError.new(errors) if errors.any?
|
|
88
157
|
validated
|
|
89
158
|
end
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
159
|
|
|
93
|
-
|
|
94
|
-
# Provides argument DSL, validation, and error handling.
|
|
95
|
-
#
|
|
96
|
-
# @example
|
|
97
|
-
# class WorkCreate < ZeroRuby::Mutation
|
|
98
|
-
# argument :id, ID, required: true
|
|
99
|
-
# argument :title, String, required: true,
|
|
100
|
-
# validates: { length: { maximum: 200 } }
|
|
101
|
-
#
|
|
102
|
-
# def execute(id:, title:)
|
|
103
|
-
# authorize! Work, to: :create?
|
|
104
|
-
# Work.create!(id: id, title: title)
|
|
105
|
-
# end
|
|
106
|
-
# end
|
|
107
|
-
class Mutation
|
|
108
|
-
include HasArguments
|
|
109
|
-
include TypeNames
|
|
160
|
+
private
|
|
110
161
|
|
|
111
|
-
|
|
112
|
-
|
|
162
|
+
# Check if a type is an InputObject class
|
|
163
|
+
def input_object_type?(type)
|
|
164
|
+
type.is_a?(Class) && type < InputObject
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Check if a type has a default value
|
|
168
|
+
def has_default?(type)
|
|
169
|
+
type.respond_to?(:default?) && type.default?
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Get the default value for a type
|
|
173
|
+
def get_default(type)
|
|
174
|
+
return nil unless has_default?(type)
|
|
175
|
+
type[]
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Check if a type is required (not optional and not with default)
|
|
179
|
+
def required_type?(type)
|
|
180
|
+
return true unless type.respond_to?(:optional?)
|
|
181
|
+
!type.optional? && !has_default?(type)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
113
184
|
|
|
114
185
|
# Initialize a mutation with raw arguments and context
|
|
115
186
|
# @param raw_args [Hash] Raw input arguments (will be coerced and validated)
|
|
116
187
|
# @param ctx [Hash] The context hash
|
|
117
188
|
def initialize(raw_args, ctx)
|
|
118
189
|
@ctx = ctx
|
|
119
|
-
@args = self.class.coerce_and_validate!(raw_args
|
|
190
|
+
@args = self.class.coerce_and_validate!(raw_args)
|
|
120
191
|
end
|
|
121
192
|
|
|
122
193
|
# Execute the mutation
|
|
123
|
-
# @
|
|
194
|
+
# @param transact_proc [Proc] Block that wraps transactional work (internal use)
|
|
195
|
+
# @return [Hash] Empty hash on success, or {data: ...} if execute returns a Hash
|
|
124
196
|
# @raise [ZeroRuby::Error] On failure (formatted at boundary)
|
|
125
|
-
def call
|
|
126
|
-
|
|
127
|
-
|
|
197
|
+
def call(&transact_proc)
|
|
198
|
+
@transact_proc = transact_proc
|
|
199
|
+
data = execute(**@args)
|
|
200
|
+
result = {}
|
|
201
|
+
result[:data] = data if data.is_a?(Hash) && !data.empty?
|
|
202
|
+
result
|
|
128
203
|
end
|
|
129
204
|
|
|
130
205
|
private
|
|
131
206
|
|
|
207
|
+
# Wrap database operations in a transaction with LMID tracking.
|
|
208
|
+
#
|
|
209
|
+
# Behavior depends on skip_auto_transaction:
|
|
210
|
+
# - Default (no skip) - Just executes the block (already in transaction)
|
|
211
|
+
# - skip_auto_transaction - Wraps block in transaction via transact_proc
|
|
212
|
+
#
|
|
213
|
+
# For skip_auto_transaction mutations, you MUST call this method.
|
|
214
|
+
# For default mutations, calling this is optional (no-op, just runs block).
|
|
215
|
+
#
|
|
216
|
+
# @yield Block containing database operations
|
|
217
|
+
# @return [Object] Result of the block
|
|
218
|
+
def transact(&block)
|
|
219
|
+
raise "transact requires a block" unless block_given?
|
|
220
|
+
|
|
221
|
+
if @transact_proc
|
|
222
|
+
# Manual transact mode (auto_transact: false) - use the provided proc
|
|
223
|
+
@transact_proc.call(&block)
|
|
224
|
+
else
|
|
225
|
+
# Auto transact mode - already in transaction, just execute the block
|
|
226
|
+
block.call
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Convenience method to access current_user from context.
|
|
231
|
+
# Override in your ApplicationMutation if you need different behavior.
|
|
232
|
+
def current_user
|
|
233
|
+
ctx[:current_user]
|
|
234
|
+
end
|
|
235
|
+
|
|
132
236
|
# Implement this method in subclasses to define mutation logic.
|
|
133
|
-
# Arguments
|
|
134
|
-
# Access context via ctx[:key]
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
|
|
237
|
+
# Arguments are passed as keyword arguments matching your declared arguments.
|
|
238
|
+
# Access context via ctx[:key] or use the current_user helper.
|
|
239
|
+
#
|
|
240
|
+
# By default (auto_transact: true), the entire execute method runs inside
|
|
241
|
+
# a transaction with LMID tracking. Just write your database operations directly.
|
|
242
|
+
#
|
|
243
|
+
# For 3-phase control, use `skip_auto_transaction` and call transact { ... }:
|
|
244
|
+
# - Pre-transaction: code before transact (auth, validation)
|
|
245
|
+
# - Transaction: code inside transact { } (database operations)
|
|
246
|
+
# - Post-commit: code after transact returns (side effects)
|
|
247
|
+
#
|
|
248
|
+
# @example Simple mutation (default auto_transact: true)
|
|
249
|
+
# def execute(id:, title:)
|
|
250
|
+
# authorize! Post, to: :create?
|
|
251
|
+
# Post.create!(id: id, title: title)
|
|
252
|
+
# end
|
|
253
|
+
#
|
|
254
|
+
# @example 3-phase mutation (skip_auto_transaction)
|
|
255
|
+
# def execute(id:, title:)
|
|
256
|
+
# authorize! Post, to: :create? # Pre-transaction
|
|
257
|
+
# result = transact do
|
|
258
|
+
# Post.create!(id: id, title: title) # Transaction
|
|
259
|
+
# end
|
|
260
|
+
# NotificationService.notify(result.id) # Post-commit
|
|
261
|
+
# {id: result.id}
|
|
262
|
+
# end
|
|
263
|
+
def execute(**args)
|
|
138
264
|
raise NotImplementedError, "Subclasses must implement #execute"
|
|
139
265
|
end
|
|
140
266
|
end
|