zero_ruby 0.1.0.alpha4 → 0.1.0.alpha5
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 +10 -15
- data/lib/zero_ruby/error_formatter.rb +4 -2
- data/lib/zero_ruby/errors.rb +1 -1
- data/lib/zero_ruby/input_object.rb +5 -6
- data/lib/zero_ruby/mutation.rb +74 -43
- data/lib/zero_ruby/push_processor.rb +15 -70
- data/lib/zero_ruby/type_names.rb +5 -18
- data/lib/zero_ruby/types.rb +3 -5
- data/lib/zero_ruby/typescript_generator.rb +44 -2
- data/lib/zero_ruby/version.rb +1 -1
- data/lib/zero_ruby.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 68451ec0c319aab33578ee4ac8aa28f61426ade64f4185047528387c5c95f3e5
|
|
4
|
+
data.tar.gz: fc5b90718c5c0aa5ee11020b02e01d422cee0108bb1095c9aac8eb184dd6c050
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b60c2a285735db948bf33ebd52f78fc09329ed3c0a012cc000dab2eda668bf37920058dd4444317ff8bde80fcf70713014ed24186037fdf6b9acf35f32f3dc6b
|
|
7
|
+
data.tar.gz: bb7c53048a48d3c65c0303be1d67bf59368e3a2eec7ae8ce482c50e94304a8295be05ffb062e4591162a5d2cfb53f66d322b807e9c9c26c0039ec5ac3967e5d9
|
data/README.md
CHANGED
|
@@ -27,7 +27,7 @@ By default, the entire `execute` method runs inside a transaction with LMID (Las
|
|
|
27
27
|
# app/zero/mutations/post_update.rb
|
|
28
28
|
module Mutations
|
|
29
29
|
class PostUpdate < ApplicationMutation
|
|
30
|
-
argument :id, ID
|
|
30
|
+
argument :id, Types::ID
|
|
31
31
|
argument :post_input, Types::PostInput
|
|
32
32
|
|
|
33
33
|
def execute(id:, post_input:)
|
|
@@ -101,14 +101,14 @@ module Types
|
|
|
101
101
|
class PostInput < Types::BaseInputObject
|
|
102
102
|
argument :title, Types::String.constrained(min_size: 1, max_size: 200)
|
|
103
103
|
argument :body, Types::String.optional
|
|
104
|
-
argument :published, Boolean.default(false)
|
|
104
|
+
argument :published, Types::Boolean.default(false)
|
|
105
105
|
end
|
|
106
106
|
end
|
|
107
107
|
```
|
|
108
108
|
|
|
109
109
|
## Configuration
|
|
110
110
|
|
|
111
|
-
Create an initializer to customize settings
|
|
111
|
+
Create an initializer to customize settings:
|
|
112
112
|
|
|
113
113
|
```ruby
|
|
114
114
|
# config/initializers/zero_ruby.rb
|
|
@@ -168,29 +168,24 @@ export type Mutators = typeof mutators
|
|
|
168
168
|
|
|
169
169
|
## Types
|
|
170
170
|
|
|
171
|
-
ZeroRuby provides types built on [dry-types](https://dry-rb.org/gems/dry-types/). When you inherit from `ZeroRuby::Mutation` or `ZeroRuby::InputObject`,
|
|
172
|
-
|
|
173
|
-
- **Direct constants**: `ID`, `Boolean`, `ISO8601Date`, `ISO8601DateTime`
|
|
174
|
-
- **Via Types module**: `Types::String`, `Types::Integer`, `Types::Float`
|
|
175
|
-
|
|
176
|
-
(Note: `String`, `Integer`, `Float` can't be direct constants because they conflict with Ruby's built-in classes)
|
|
171
|
+
ZeroRuby provides types built on [dry-types](https://dry-rb.org/gems/dry-types/). When you inherit from `ZeroRuby::Mutation` or `ZeroRuby::InputObject`, types are available via the `Types` module:
|
|
177
172
|
|
|
178
173
|
```ruby
|
|
179
174
|
# Basic types
|
|
180
175
|
argument :name, Types::String
|
|
181
176
|
argument :count, Types::Integer
|
|
182
177
|
argument :price, Types::Float
|
|
183
|
-
argument :active, Boolean
|
|
184
|
-
argument :id, ID
|
|
185
|
-
argument :date, ISO8601Date
|
|
186
|
-
argument :timestamp, ISO8601DateTime
|
|
178
|
+
argument :active, Types::Boolean
|
|
179
|
+
argument :id, Types::ID # Non-empty string
|
|
180
|
+
argument :date, Types::ISO8601Date
|
|
181
|
+
argument :timestamp, Types::ISO8601DateTime
|
|
187
182
|
|
|
188
183
|
# Optional types (accepts nil)
|
|
189
184
|
argument :nickname, Types::String.optional
|
|
190
185
|
|
|
191
186
|
# Default values
|
|
192
187
|
argument :status, Types::String.default("draft")
|
|
193
|
-
argument :enabled, Boolean.default(false)
|
|
188
|
+
argument :enabled, Types::Boolean.default(false)
|
|
194
189
|
```
|
|
195
190
|
|
|
196
191
|
## Validation with Constraints
|
|
@@ -260,7 +255,7 @@ By default, the entire `execute` method runs inside a transaction in order to at
|
|
|
260
255
|
class PostUpdate < ApplicationMutation
|
|
261
256
|
skip_auto_transaction
|
|
262
257
|
|
|
263
|
-
argument :id, ID
|
|
258
|
+
argument :id, Types::ID
|
|
264
259
|
argument :post_input, Types::PostInput
|
|
265
260
|
|
|
266
261
|
def execute(id:, post_input:)
|
|
@@ -21,7 +21,9 @@ module ZeroRuby
|
|
|
21
21
|
field = match ? match[1] : "field"
|
|
22
22
|
["#{field} is required"]
|
|
23
23
|
elsif message.include?("has invalid type")
|
|
24
|
-
|
|
24
|
+
# Matches both ":title has invalid type" and "has invalid type for :title"
|
|
25
|
+
match = message.match(/:(\w+) has invalid type/) ||
|
|
26
|
+
message.match(/has invalid type for :(\w+)/)
|
|
25
27
|
field = match ? match[1] : "field"
|
|
26
28
|
["#{field}: invalid type"]
|
|
27
29
|
elsif message.include?("violates constraints")
|
|
@@ -81,7 +83,7 @@ module ZeroRuby
|
|
|
81
83
|
begin
|
|
82
84
|
result = MESSAGES.call(key, path: [:base], tokens: build_tokens(key, args))
|
|
83
85
|
result&.text
|
|
84
|
-
rescue
|
|
86
|
+
rescue
|
|
85
87
|
nil
|
|
86
88
|
end
|
|
87
89
|
end
|
data/lib/zero_ruby/errors.rb
CHANGED
|
@@ -91,7 +91,7 @@ module ZeroRuby
|
|
|
91
91
|
# Raised for database transaction errors.
|
|
92
92
|
# Triggers top-level PushFailed with reason: "database"
|
|
93
93
|
# Wraps unexpected errors during transaction execution (matching TS behavior).
|
|
94
|
-
class
|
|
94
|
+
class TransactionError < Error
|
|
95
95
|
end
|
|
96
96
|
|
|
97
97
|
# Raised when push data is malformed or missing required fields.
|
|
@@ -8,21 +8,20 @@ module ZeroRuby
|
|
|
8
8
|
# Base class for input objects (nested argument types).
|
|
9
9
|
# Uses Dry::Struct for type coercion and validation.
|
|
10
10
|
#
|
|
11
|
-
# Includes ZeroRuby::TypeNames for convenient type access
|
|
12
|
-
#
|
|
13
|
-
# - Types::String, Types::Integer, Types::Float (via Types module)
|
|
11
|
+
# Includes ZeroRuby::TypeNames for convenient type access via the Types module
|
|
12
|
+
# (e.g., Types::String, Types::ID, Types::Boolean).
|
|
14
13
|
#
|
|
15
14
|
# @example
|
|
16
15
|
# class Types::PostInput < ZeroRuby::InputObject
|
|
17
|
-
# argument :id, ID
|
|
16
|
+
# argument :id, Types::ID
|
|
18
17
|
# argument :title, Types::String.constrained(min_size: 1, max_size: 200)
|
|
19
18
|
# argument :body, Types::String.optional
|
|
20
|
-
# argument :published, Boolean.default(false)
|
|
19
|
+
# argument :published, Types::Boolean.default(false)
|
|
21
20
|
# end
|
|
22
21
|
#
|
|
23
22
|
# class PostCreate < ZeroRuby::Mutation
|
|
24
23
|
# argument :post_input, Types::PostInput
|
|
25
|
-
# argument :notify, Boolean.default(false)
|
|
24
|
+
# argument :notify, Types::Boolean.default(false)
|
|
26
25
|
#
|
|
27
26
|
# def execute
|
|
28
27
|
# # args[:post_input].title, args[:post_input].body, etc.
|
data/lib/zero_ruby/mutation.rb
CHANGED
|
@@ -3,21 +3,21 @@
|
|
|
3
3
|
require_relative "types"
|
|
4
4
|
require_relative "type_names"
|
|
5
5
|
require_relative "errors"
|
|
6
|
+
require_relative "error_formatter"
|
|
6
7
|
|
|
7
8
|
module ZeroRuby
|
|
8
9
|
# Base class for Zero mutations.
|
|
9
10
|
# Provides argument DSL with dry-types validation.
|
|
10
11
|
#
|
|
11
|
-
# Includes ZeroRuby::TypeNames for convenient type access
|
|
12
|
-
#
|
|
13
|
-
# - Types::String, Types::Integer, Types::Float (via Types module)
|
|
12
|
+
# Includes ZeroRuby::TypeNames for convenient type access via the Types module
|
|
13
|
+
# (e.g., Types::String, Types::ID, Types::Boolean).
|
|
14
14
|
#
|
|
15
15
|
# By default (auto_transact: true), the entire execute method runs inside
|
|
16
16
|
# a transaction with LMID tracking. For 3-phase control, set auto_transact false.
|
|
17
17
|
#
|
|
18
18
|
# @example Simple mutation (auto_transact: true, default)
|
|
19
19
|
# class WorkCreate < ZeroRuby::Mutation
|
|
20
|
-
# argument :id, ID
|
|
20
|
+
# argument :id, Types::ID
|
|
21
21
|
# argument :title, Types::String.constrained(max_size: 200)
|
|
22
22
|
#
|
|
23
23
|
# def execute(id:, title:)
|
|
@@ -30,7 +30,7 @@ module ZeroRuby
|
|
|
30
30
|
# class WorkUpdate < ZeroRuby::Mutation
|
|
31
31
|
# skip_auto_transaction
|
|
32
32
|
#
|
|
33
|
-
# argument :id, ID
|
|
33
|
+
# argument :id, Types::ID
|
|
34
34
|
# argument :title, Types::String
|
|
35
35
|
#
|
|
36
36
|
# def execute(id:, title:)
|
|
@@ -92,16 +92,21 @@ module ZeroRuby
|
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
# Coerce and validate raw arguments.
|
|
95
|
-
# Collects
|
|
96
|
-
#
|
|
95
|
+
# Collects ALL validation errors (missing fields, type coercion, constraints)
|
|
96
|
+
# and raises a single ValidationError with all issues.
|
|
97
97
|
#
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
# @
|
|
103
|
-
# @
|
|
98
|
+
# Uses type.try(value) which returns a Result instead of raising, allowing
|
|
99
|
+
# us to collect all errors in one pass rather than failing on the first one.
|
|
100
|
+
# Works for both Dry::Types (scalars) and Dry::Struct (InputObjects).
|
|
101
|
+
#
|
|
102
|
+
# @param raw_args [Hash] Raw input arguments (string keys from JSON)
|
|
103
|
+
# @return [Hash] Validated and coerced arguments (symbol keys, may contain InputObject instances)
|
|
104
|
+
# @raise [ZeroRuby::ValidationError] If any validation fails
|
|
104
105
|
def coerce_and_validate!(raw_args)
|
|
106
|
+
# Result hash: symbol keys → coerced values (strings, integers, InputObject instances, etc.)
|
|
107
|
+
# eg:
|
|
108
|
+
# raw_args: {"name" => "test", "count" => "5"} # string keys, raw values
|
|
109
|
+
# validated: {name: "test", count: 5} # symbol keys, coerced values
|
|
105
110
|
validated = {}
|
|
106
111
|
errors = []
|
|
107
112
|
|
|
@@ -110,47 +115,38 @@ module ZeroRuby
|
|
|
110
115
|
str_key = name.to_s
|
|
111
116
|
key_present = raw_args.key?(str_key)
|
|
112
117
|
value = raw_args[str_key]
|
|
118
|
+
is_input_object = input_object_type?(type)
|
|
113
119
|
|
|
114
|
-
#
|
|
115
|
-
|
|
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
|
|
129
|
-
next
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
# Handle missing key
|
|
133
|
-
if !key_present
|
|
120
|
+
# Missing key: use default if available, otherwise error if required
|
|
121
|
+
unless key_present
|
|
134
122
|
if has_default?(type)
|
|
135
123
|
validated[name] = get_default(type)
|
|
136
124
|
elsif required_type?(type)
|
|
137
125
|
errors << "#{name} is required"
|
|
138
126
|
end
|
|
127
|
+
# Optional fields without defaults are simply omitted from result
|
|
139
128
|
next
|
|
140
129
|
end
|
|
141
130
|
|
|
142
|
-
#
|
|
131
|
+
# Explicit null: InputObjects always allow nil (they handle optionality internally),
|
|
132
|
+
# scalars only allow nil if the type is optional
|
|
143
133
|
if value.nil?
|
|
144
|
-
if required_type?(type)
|
|
145
|
-
errors << "#{name} is required"
|
|
146
|
-
else
|
|
134
|
+
if is_input_object || !required_type?(type)
|
|
147
135
|
validated[name] = nil
|
|
136
|
+
else
|
|
137
|
+
errors << "#{name} is required"
|
|
148
138
|
end
|
|
149
139
|
next
|
|
150
140
|
end
|
|
151
141
|
|
|
152
|
-
# Coerce
|
|
153
|
-
|
|
142
|
+
# Coerce value: type.try returns Result instead of raising, so we can
|
|
143
|
+
# collect all errors. Works for both Dry::Types and Dry::Struct.
|
|
144
|
+
result = type.try(value)
|
|
145
|
+
if result.failure?
|
|
146
|
+
errors << format_type_error(name, result.error, is_input_object)
|
|
147
|
+
else
|
|
148
|
+
validated[name] = result.input
|
|
149
|
+
end
|
|
154
150
|
end
|
|
155
151
|
|
|
156
152
|
raise ValidationError.new(errors) if errors.any?
|
|
@@ -180,6 +176,28 @@ module ZeroRuby
|
|
|
180
176
|
return true unless type.respond_to?(:optional?)
|
|
181
177
|
!type.optional? && !has_default?(type)
|
|
182
178
|
end
|
|
179
|
+
|
|
180
|
+
# Format a type error (coercion or constraint failure)
|
|
181
|
+
# @param name [Symbol] The field name
|
|
182
|
+
# @param error [Exception] The error from try.failure
|
|
183
|
+
# @param is_input_object [Boolean] Whether this is an InputObject type
|
|
184
|
+
# @return [String] Formatted error message
|
|
185
|
+
def format_type_error(name, error, is_input_object = false)
|
|
186
|
+
if is_input_object && error.is_a?(Dry::Struct::Error)
|
|
187
|
+
# InputObject errors get prefixed with field name
|
|
188
|
+
ErrorFormatter.format_struct_error(error).map { |m| "#{name}.#{m}" }.first
|
|
189
|
+
else
|
|
190
|
+
message = case error
|
|
191
|
+
when Dry::Types::CoercionError
|
|
192
|
+
ErrorFormatter.format_coercion_error(error)
|
|
193
|
+
when Dry::Types::ConstraintError
|
|
194
|
+
ErrorFormatter.format_constraint_error(error)
|
|
195
|
+
else
|
|
196
|
+
error.message
|
|
197
|
+
end
|
|
198
|
+
"#{name}: #{message}"
|
|
199
|
+
end
|
|
200
|
+
end
|
|
183
201
|
end
|
|
184
202
|
|
|
185
203
|
# Initialize a mutation with raw arguments and context
|
|
@@ -194,9 +212,20 @@ module ZeroRuby
|
|
|
194
212
|
# @param transact_proc [Proc] Block that wraps transactional work (internal use)
|
|
195
213
|
# @return [Hash] Empty hash on success, or {data: ...} if execute returns a Hash
|
|
196
214
|
# @raise [ZeroRuby::Error] On failure (formatted at boundary)
|
|
215
|
+
# @raise [ZeroRuby::TransactNotCalledError] If skip_auto_transaction and transact not called
|
|
197
216
|
def call(&transact_proc)
|
|
198
217
|
@transact_proc = transact_proc
|
|
199
|
-
|
|
218
|
+
@transact_called = false
|
|
219
|
+
|
|
220
|
+
if self.class.skip_auto_transaction?
|
|
221
|
+
# Manual mode: Use defined mutation calls transact {}
|
|
222
|
+
data = execute(**@args)
|
|
223
|
+
raise TransactNotCalledError.new unless @transact_called
|
|
224
|
+
else
|
|
225
|
+
# Auto mode: wrap entire execute in transaction
|
|
226
|
+
data = transact_proc.call { execute(**@args) }
|
|
227
|
+
end
|
|
228
|
+
|
|
200
229
|
result = {}
|
|
201
230
|
result[:data] = data if data.is_a?(Hash) && !data.empty?
|
|
202
231
|
result
|
|
@@ -205,6 +234,7 @@ module ZeroRuby
|
|
|
205
234
|
private
|
|
206
235
|
|
|
207
236
|
# Wrap database operations in a transaction with LMID tracking.
|
|
237
|
+
# Used by user defined mutation.
|
|
208
238
|
#
|
|
209
239
|
# Behavior depends on skip_auto_transaction:
|
|
210
240
|
# - Default (no skip) - Just executes the block (already in transaction)
|
|
@@ -217,12 +247,13 @@ module ZeroRuby
|
|
|
217
247
|
# @return [Object] Result of the block
|
|
218
248
|
def transact(&block)
|
|
219
249
|
raise "transact requires a block" unless block_given?
|
|
250
|
+
@transact_called = true
|
|
220
251
|
|
|
221
|
-
if
|
|
222
|
-
# Manual
|
|
252
|
+
if self.class.skip_auto_transaction?
|
|
253
|
+
# Manual mode - actually call the transact_proc to start transaction
|
|
223
254
|
@transact_proc.call(&block)
|
|
224
255
|
else
|
|
225
|
-
# Auto
|
|
256
|
+
# Auto mode - already in transaction, just execute the block
|
|
226
257
|
block.call
|
|
227
258
|
end
|
|
228
259
|
end
|
|
@@ -37,10 +37,6 @@ module ZeroRuby
|
|
|
37
37
|
mutations.each_with_index do |mutation_data, index|
|
|
38
38
|
result = process_mutation_with_lmid(mutation_data, client_group_id, context)
|
|
39
39
|
results << result
|
|
40
|
-
rescue MutationNotFoundError => e
|
|
41
|
-
# Unknown mutation - return error response and continue batch
|
|
42
|
-
mutation_id_obj = {id: mutation_data["id"], clientID: mutation_data["clientID"]}
|
|
43
|
-
results << {id: mutation_id_obj, result: format_error_response(e)}
|
|
44
40
|
rescue OutOfOrderMutationError => e
|
|
45
41
|
# Return top-level PushFailedBody with all unprocessed mutation IDs
|
|
46
42
|
unprocessed_ids = mutations[index..].map { |m| {id: m["id"], clientID: m["clientID"]} }
|
|
@@ -51,7 +47,7 @@ module ZeroRuby
|
|
|
51
47
|
message: e.message,
|
|
52
48
|
mutationIDs: unprocessed_ids
|
|
53
49
|
}
|
|
54
|
-
rescue
|
|
50
|
+
rescue TransactionError => e
|
|
55
51
|
# Database errors trigger top-level PushFailed per Zero protocol
|
|
56
52
|
unprocessed_ids = mutations[index..].map { |m| {id: m["id"], clientID: m["clientID"]} }
|
|
57
53
|
return {
|
|
@@ -70,21 +66,10 @@ module ZeroRuby
|
|
|
70
66
|
|
|
71
67
|
# Process a single mutation with LMID validation, transaction support, and phase tracking.
|
|
72
68
|
#
|
|
73
|
-
#
|
|
69
|
+
# The Mutation#call method decides whether to auto-wrap execute in a transaction
|
|
70
|
+
# (default behavior) or pass control to user code (skip_auto_transaction mode).
|
|
74
71
|
#
|
|
75
|
-
#
|
|
76
|
-
# - Entire execute method is wrapped in a transaction with LMID tracking
|
|
77
|
-
# - User does NOT need to call transact {} - it's automatic
|
|
78
|
-
# - Simple mutations benefit from less boilerplate
|
|
79
|
-
#
|
|
80
|
-
# **skip_auto_transaction**
|
|
81
|
-
# - Uses three-phase model matching Zero's TypeScript implementation:
|
|
82
|
-
# 1. Pre-transaction: User code runs before calling transact (auth, validation)
|
|
83
|
-
# 2. Transaction: User code inside transact { } block (database operations, LMID tracking)
|
|
84
|
-
# 3. Post-commit: User code after transact returns (side effects)
|
|
85
|
-
# - User MUST call transact {} or TransactNotCalledError is raised
|
|
86
|
-
#
|
|
87
|
-
# LMID semantics by phase:
|
|
72
|
+
# Phase tracking enables correct LMID semantics:
|
|
88
73
|
# - Pre-transaction error: LMID advanced in separate transaction
|
|
89
74
|
# - Transaction error: LMID advanced in separate transaction (original tx rolled back)
|
|
90
75
|
# - Post-commit error: LMID already committed with transaction
|
|
@@ -97,46 +82,9 @@ module ZeroRuby
|
|
|
97
82
|
handler_class = schema.handler_for(mutation_name)
|
|
98
83
|
raise MutationNotFoundError.new(mutation_name) unless handler_class
|
|
99
84
|
|
|
100
|
-
if handler_class.skip_auto_transaction?
|
|
101
|
-
process_manual_transact(mutation_data, client_group_id, client_id, mutation_id, mutation_id_obj, context)
|
|
102
|
-
else
|
|
103
|
-
process_auto_transact(mutation_data, client_group_id, client_id, mutation_id, mutation_id_obj, context)
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
# Process mutation with auto-transact (entire execute wrapped in transaction)
|
|
108
|
-
def process_auto_transact(mutation_data, client_group_id, client_id, mutation_id, mutation_id_obj, context)
|
|
109
|
-
result = lmid_store.transaction do
|
|
110
|
-
last_mutation_id = lmid_store.fetch_and_increment(client_group_id, client_id)
|
|
111
|
-
check_lmid!(client_id, mutation_id, last_mutation_id)
|
|
112
|
-
schema.execute_mutation(mutation_data, context)
|
|
113
|
-
end
|
|
114
|
-
{id: mutation_id_obj, result: result}
|
|
115
|
-
rescue MutationAlreadyProcessedError => e
|
|
116
|
-
{id: mutation_id_obj, result: format_error_response(e)}
|
|
117
|
-
rescue OutOfOrderMutationError, DatabaseTransactionError
|
|
118
|
-
raise
|
|
119
|
-
rescue ActiveRecord::StatementInvalid => e
|
|
120
|
-
raise DatabaseTransactionError.new("Transaction failed: #{e.message}")
|
|
121
|
-
rescue ZeroRuby::Error, FloatDomainError => e
|
|
122
|
-
# No retry - matches TypeScript behavior (TS does NOT retry user code)
|
|
123
|
-
# Transaction rolled back - advance LMID separately to prevent replay
|
|
124
|
-
persist_lmid_on_application_error(client_group_id, client_id)
|
|
125
|
-
error = e.is_a?(FloatDomainError) ?
|
|
126
|
-
ValidationError.new(["Invalid numeric value: #{e.message}"]) : e
|
|
127
|
-
{id: mutation_id_obj, result: format_error_response(error)}
|
|
128
|
-
rescue => e
|
|
129
|
-
raise DatabaseTransactionError.new("Transaction failed: #{e.message}")
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
# Process mutation with manual transact (3-phase model)
|
|
133
|
-
# No retry - matches TypeScript behavior (TS does NOT retry user code)
|
|
134
|
-
def process_manual_transact(mutation_data, client_group_id, client_id, mutation_id, mutation_id_obj, context)
|
|
135
85
|
phase = :pre_transaction
|
|
136
|
-
transact_called = false
|
|
137
86
|
|
|
138
87
|
transact_proc = proc { |&user_block|
|
|
139
|
-
transact_called = true
|
|
140
88
|
phase = :transaction
|
|
141
89
|
result = lmid_store.transaction do
|
|
142
90
|
last_mutation_id = lmid_store.fetch_and_increment(client_group_id, client_id)
|
|
@@ -148,27 +96,24 @@ module ZeroRuby
|
|
|
148
96
|
}
|
|
149
97
|
|
|
150
98
|
result = schema.execute_mutation(mutation_data, context, &transact_proc)
|
|
151
|
-
raise TransactNotCalledError.new unless transact_called
|
|
152
99
|
{id: mutation_id_obj, result: result}
|
|
153
|
-
rescue MutationAlreadyProcessedError => e
|
|
100
|
+
rescue MutationNotFoundError, MutationAlreadyProcessedError => e
|
|
101
|
+
# Known skip conditions - return error response, batch continues
|
|
154
102
|
{id: mutation_id_obj, result: format_error_response(e)}
|
|
155
|
-
rescue OutOfOrderMutationError,
|
|
103
|
+
rescue OutOfOrderMutationError, TransactionError
|
|
104
|
+
# Batch-terminating errors - bubble up to process() for PushFailed response
|
|
156
105
|
raise
|
|
157
|
-
rescue
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
# LMID
|
|
161
|
-
# - Pre-transaction: LMID advanced separately
|
|
162
|
-
# - Transaction (rolled back): LMID advanced separately
|
|
163
|
-
# - Post-commit: LMID already committed with transaction
|
|
106
|
+
rescue ZeroRuby::Error => e
|
|
107
|
+
# Application errors - advance LMID based on phase, return error response
|
|
108
|
+
# Pre-transaction/transaction: LMID advanced separately
|
|
109
|
+
# Post-commit: LMID already committed with transaction
|
|
164
110
|
if phase != :post_commit
|
|
165
111
|
persist_lmid_on_application_error(client_group_id, client_id)
|
|
166
112
|
end
|
|
167
|
-
|
|
168
|
-
ValidationError.new(["Invalid numeric value: #{e.message}"]) : e
|
|
169
|
-
{id: mutation_id_obj, result: format_error_response(error)}
|
|
113
|
+
{id: mutation_id_obj, result: format_error_response(e)}
|
|
170
114
|
rescue => e
|
|
171
|
-
|
|
115
|
+
# Unexpected errors - wrap and bubble up as batch-terminating
|
|
116
|
+
raise TransactionError.new("Transaction failed: #{e.message}")
|
|
172
117
|
end
|
|
173
118
|
|
|
174
119
|
# Persist LMID advancement after an application error.
|
data/lib/zero_ruby/type_names.rb
CHANGED
|
@@ -1,29 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "types"
|
|
4
|
-
|
|
5
3
|
module ZeroRuby
|
|
6
|
-
# Provides
|
|
7
|
-
#
|
|
4
|
+
# Provides access to ZeroRuby::Types via the Types constant.
|
|
5
|
+
# Automatically included in Mutation and InputObject classes.
|
|
8
6
|
#
|
|
9
|
-
# @example
|
|
7
|
+
# @example Usage in mutations
|
|
10
8
|
# class PostCreate < ZeroRuby::Mutation
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# argument :id, ID
|
|
9
|
+
# argument :id, Types::ID
|
|
14
10
|
# argument :title, Types::String
|
|
15
|
-
# argument :active, Boolean
|
|
11
|
+
# argument :active, Types::Boolean
|
|
16
12
|
# end
|
|
17
|
-
#
|
|
18
|
-
# Note: Ruby's built-in String, Integer, and Float cannot be aliased since
|
|
19
|
-
# they are actual classes. Use ZeroRuby::Types::String, etc. instead.
|
|
20
13
|
module TypeNames
|
|
21
|
-
ID = ZeroRuby::Types::ID
|
|
22
|
-
Boolean = ZeroRuby::Types::Boolean
|
|
23
|
-
ISO8601Date = ZeroRuby::Types::ISO8601Date
|
|
24
|
-
ISO8601DateTime = ZeroRuby::Types::ISO8601DateTime
|
|
25
|
-
|
|
26
|
-
# Provide Types module access without prefix
|
|
27
14
|
Types = ZeroRuby::Types
|
|
28
15
|
end
|
|
29
16
|
end
|
data/lib/zero_ruby/types.rb
CHANGED
|
@@ -6,16 +6,14 @@ module ZeroRuby
|
|
|
6
6
|
# Type definitions using dry-types.
|
|
7
7
|
#
|
|
8
8
|
# When inheriting from ZeroRuby::Mutation or ZeroRuby::InputObject, types are
|
|
9
|
-
# available via
|
|
10
|
-
# - Direct: ID, Boolean, ISO8601Date, ISO8601DateTime
|
|
11
|
-
# - Via Types: Types::String, Types::Integer, Types::Float
|
|
9
|
+
# available via the Types module (e.g., Types::String, Types::ID).
|
|
12
10
|
#
|
|
13
11
|
# @example Basic usage
|
|
14
12
|
# class MyMutation < ZeroRuby::Mutation
|
|
15
|
-
# argument :id, ID
|
|
13
|
+
# argument :id, Types::ID
|
|
16
14
|
# argument :name, Types::String
|
|
17
15
|
# argument :count, Types::Integer.optional
|
|
18
|
-
# argument :active, Boolean.default(false)
|
|
16
|
+
# argument :active, Types::Boolean.default(false)
|
|
19
17
|
# end
|
|
20
18
|
#
|
|
21
19
|
# @example With constraints
|
|
@@ -58,6 +58,18 @@ module ZeroRuby
|
|
|
58
58
|
def collect_input_objects_from_arguments(arguments)
|
|
59
59
|
arguments.each do |_name, config|
|
|
60
60
|
type = config[:type]
|
|
61
|
+
|
|
62
|
+
# Unwrap optional types first (Sum type: NilClass | actual_type)
|
|
63
|
+
if sum_type?(type)
|
|
64
|
+
inner = extract_non_nil_type(type)
|
|
65
|
+
type = inner if inner
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Then unwrap array types to check element type for InputObjects
|
|
69
|
+
if array_type?(type)
|
|
70
|
+
type = type.member
|
|
71
|
+
end
|
|
72
|
+
|
|
61
73
|
if input_object_type?(type)
|
|
62
74
|
type_name = extract_type_name(type)
|
|
63
75
|
unless @input_objects.key?(type_name)
|
|
@@ -65,7 +77,7 @@ module ZeroRuby
|
|
|
65
77
|
# Recursively collect nested InputObjects from Dry::Struct schema
|
|
66
78
|
if type.respond_to?(:schema)
|
|
67
79
|
nested_args = type.schema.keys.each_with_object({}) do |key, hash|
|
|
68
|
-
hash[key.name] = {
|
|
80
|
+
hash[key.name] = {type: key.type, name: key.name}
|
|
69
81
|
end
|
|
70
82
|
collect_input_objects_from_arguments(nested_args)
|
|
71
83
|
end
|
|
@@ -169,6 +181,19 @@ module ZeroRuby
|
|
|
169
181
|
return extract_type_name(type)
|
|
170
182
|
end
|
|
171
183
|
|
|
184
|
+
# Handle Array types
|
|
185
|
+
if array_type?(type)
|
|
186
|
+
element_type = type.member
|
|
187
|
+
return "#{resolve_type(element_type)}[]"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Handle optional types (Sum of NilClass and another type)
|
|
191
|
+
# This must come before unwrap_primitive to handle optional arrays
|
|
192
|
+
if sum_type?(type)
|
|
193
|
+
inner = extract_non_nil_type(type)
|
|
194
|
+
return resolve_type(inner) if inner
|
|
195
|
+
end
|
|
196
|
+
|
|
172
197
|
# Unwrap the dry-type to get the primitive
|
|
173
198
|
primitive = unwrap_primitive(type)
|
|
174
199
|
|
|
@@ -181,7 +206,7 @@ module ZeroRuby
|
|
|
181
206
|
PRIMITIVE_MAP[primitive] || raise(
|
|
182
207
|
ArgumentError,
|
|
183
208
|
"Cannot map type #{type.inspect} (primitive: #{primitive.inspect}) to TypeScript. " \
|
|
184
|
-
"Supported primitives: #{PRIMITIVE_MAP.keys.map(&:name).join(
|
|
209
|
+
"Supported primitives: #{PRIMITIVE_MAP.keys.map(&:name).join(", ")}"
|
|
185
210
|
)
|
|
186
211
|
end
|
|
187
212
|
|
|
@@ -231,6 +256,23 @@ module ZeroRuby
|
|
|
231
256
|
type.is_a?(Class) && type < ZeroRuby::InputObject
|
|
232
257
|
end
|
|
233
258
|
|
|
259
|
+
def array_type?(type)
|
|
260
|
+
type.respond_to?(:primitive) && type.primitive == Array
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def sum_type?(type)
|
|
264
|
+
type.respond_to?(:left) && type.respond_to?(:right)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def extract_non_nil_type(type)
|
|
268
|
+
return nil unless sum_type?(type)
|
|
269
|
+
if type.left.respond_to?(:primitive) && type.left.primitive == NilClass
|
|
270
|
+
type.right
|
|
271
|
+
elsif type.right.respond_to?(:primitive) && type.right.primitive == NilClass
|
|
272
|
+
type.left
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
234
276
|
def extract_type_name(type)
|
|
235
277
|
# Get just the class name without module prefixes
|
|
236
278
|
type.name.split("::").last
|
data/lib/zero_ruby/version.rb
CHANGED
data/lib/zero_ruby.rb
CHANGED
|
@@ -7,7 +7,7 @@ require_relative "zero_ruby/errors"
|
|
|
7
7
|
# Types module (dry-types based)
|
|
8
8
|
require_relative "zero_ruby/types"
|
|
9
9
|
|
|
10
|
-
#
|
|
10
|
+
# Provides Types constant for accessing ZeroRuby::Types
|
|
11
11
|
require_relative "zero_ruby/type_names"
|
|
12
12
|
|
|
13
13
|
# Core classes
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: zero_ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.0.
|
|
4
|
+
version: 0.1.0.alpha5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex Serban
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 2026-01-01 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: dry-struct
|