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
|
@@ -15,15 +15,13 @@ module ZeroRuby
|
|
|
15
15
|
# @see https://github.com/rocicorp/mono/blob/main/packages/zero-server/src/process-mutations.ts
|
|
16
16
|
# @see https://github.com/rocicorp/mono/blob/main/packages/zero-server/src/zql-database.ts
|
|
17
17
|
class PushProcessor
|
|
18
|
-
attr_reader :schema, :lmid_store
|
|
18
|
+
attr_reader :schema, :lmid_store
|
|
19
19
|
|
|
20
20
|
# @param schema [Class] The schema class for mutation processing
|
|
21
21
|
# @param lmid_store [LmidStore] The LMID store instance
|
|
22
|
-
|
|
23
|
-
def initialize(schema:, lmid_store:, max_retries: 3)
|
|
22
|
+
def initialize(schema:, lmid_store:)
|
|
24
23
|
@schema = schema
|
|
25
24
|
@lmid_store = lmid_store
|
|
26
|
-
@max_retries = max_retries
|
|
27
25
|
end
|
|
28
26
|
|
|
29
27
|
# Process a Zero push request
|
|
@@ -36,12 +34,33 @@ module ZeroRuby
|
|
|
36
34
|
mutations = push_data["mutations"] || []
|
|
37
35
|
results = []
|
|
38
36
|
|
|
39
|
-
mutations.
|
|
37
|
+
mutations.each_with_index do |mutation_data, index|
|
|
40
38
|
result = process_mutation_with_lmid(mutation_data, client_group_id, context)
|
|
41
39
|
results << result
|
|
42
|
-
|
|
43
|
-
#
|
|
44
|
-
|
|
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
|
+
rescue OutOfOrderMutationError => e
|
|
45
|
+
# Return top-level PushFailedBody with all unprocessed mutation IDs
|
|
46
|
+
unprocessed_ids = mutations[index..].map { |m| {id: m["id"], clientID: m["clientID"]} }
|
|
47
|
+
return {
|
|
48
|
+
kind: "PushFailed",
|
|
49
|
+
origin: "server",
|
|
50
|
+
reason: "oooMutation",
|
|
51
|
+
message: e.message,
|
|
52
|
+
mutationIDs: unprocessed_ids
|
|
53
|
+
}
|
|
54
|
+
rescue DatabaseTransactionError => e
|
|
55
|
+
# Database errors trigger top-level PushFailed per Zero protocol
|
|
56
|
+
unprocessed_ids = mutations[index..].map { |m| {id: m["id"], clientID: m["clientID"]} }
|
|
57
|
+
return {
|
|
58
|
+
kind: "PushFailed",
|
|
59
|
+
origin: "server",
|
|
60
|
+
reason: "database",
|
|
61
|
+
message: e.message,
|
|
62
|
+
mutationIDs: unprocessed_ids
|
|
63
|
+
}
|
|
45
64
|
end
|
|
46
65
|
|
|
47
66
|
{mutations: results}
|
|
@@ -49,28 +68,117 @@ module ZeroRuby
|
|
|
49
68
|
|
|
50
69
|
private
|
|
51
70
|
|
|
52
|
-
# Process a single mutation with LMID validation and
|
|
53
|
-
#
|
|
71
|
+
# Process a single mutation with LMID validation, transaction support, and phase tracking.
|
|
72
|
+
#
|
|
73
|
+
# Supports two execution modes:
|
|
74
|
+
#
|
|
75
|
+
# **Default (auto-transaction)**
|
|
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:
|
|
88
|
+
# - Pre-transaction error: LMID advanced in separate transaction
|
|
89
|
+
# - Transaction error: LMID advanced in separate transaction (original tx rolled back)
|
|
90
|
+
# - Post-commit error: LMID already committed with transaction
|
|
54
91
|
def process_mutation_with_lmid(mutation_data, client_group_id, context)
|
|
55
92
|
mutation_id = mutation_data["id"]
|
|
56
93
|
client_id = mutation_data["clientID"]
|
|
94
|
+
mutation_id_obj = {id: mutation_id, clientID: client_id}
|
|
95
|
+
mutation_name = mutation_data["name"]
|
|
57
96
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
clientID: client_id
|
|
61
|
-
}
|
|
97
|
+
handler_class = schema.handler_for(mutation_name)
|
|
98
|
+
raise MutationNotFoundError.new(mutation_name) unless handler_class
|
|
62
99
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
66
110
|
last_mutation_id = lmid_store.fetch_and_increment(client_group_id, client_id)
|
|
67
111
|
check_lmid!(client_id, mutation_id, last_mutation_id)
|
|
68
|
-
|
|
69
|
-
result = execute_with_retry(mutation_data, context)
|
|
70
|
-
{id: mutation_id_obj, result: result}
|
|
112
|
+
schema.execute_mutation(mutation_data, context)
|
|
71
113
|
end
|
|
72
|
-
|
|
114
|
+
{id: mutation_id_obj, result: result}
|
|
115
|
+
rescue MutationAlreadyProcessedError => e
|
|
73
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
|
+
phase = :pre_transaction
|
|
136
|
+
transact_called = false
|
|
137
|
+
|
|
138
|
+
transact_proc = proc { |&user_block|
|
|
139
|
+
transact_called = true
|
|
140
|
+
phase = :transaction
|
|
141
|
+
result = lmid_store.transaction do
|
|
142
|
+
last_mutation_id = lmid_store.fetch_and_increment(client_group_id, client_id)
|
|
143
|
+
check_lmid!(client_id, mutation_id, last_mutation_id)
|
|
144
|
+
user_block.call
|
|
145
|
+
end
|
|
146
|
+
phase = :post_commit
|
|
147
|
+
result
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
result = schema.execute_mutation(mutation_data, context, &transact_proc)
|
|
151
|
+
raise TransactNotCalledError.new unless transact_called
|
|
152
|
+
{id: mutation_id_obj, result: result}
|
|
153
|
+
rescue MutationAlreadyProcessedError => e
|
|
154
|
+
{id: mutation_id_obj, result: format_error_response(e)}
|
|
155
|
+
rescue OutOfOrderMutationError, DatabaseTransactionError
|
|
156
|
+
raise
|
|
157
|
+
rescue ActiveRecord::StatementInvalid => e
|
|
158
|
+
raise DatabaseTransactionError.new("Transaction failed: #{e.message}")
|
|
159
|
+
rescue ZeroRuby::Error, FloatDomainError => e
|
|
160
|
+
# LMID semantics by phase (no retry - matches TS):
|
|
161
|
+
# - Pre-transaction: LMID advanced separately
|
|
162
|
+
# - Transaction (rolled back): LMID advanced separately
|
|
163
|
+
# - Post-commit: LMID already committed with transaction
|
|
164
|
+
if phase != :post_commit
|
|
165
|
+
persist_lmid_on_application_error(client_group_id, client_id)
|
|
166
|
+
end
|
|
167
|
+
error = e.is_a?(FloatDomainError) ?
|
|
168
|
+
ValidationError.new(["Invalid numeric value: #{e.message}"]) : e
|
|
169
|
+
{id: mutation_id_obj, result: format_error_response(error)}
|
|
170
|
+
rescue => e
|
|
171
|
+
raise DatabaseTransactionError.new("Transaction failed: #{e.message}")
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Persist LMID advancement after an application error.
|
|
175
|
+
# Called for pre-transaction and transaction errors to prevent replay attacks.
|
|
176
|
+
def persist_lmid_on_application_error(client_group_id, client_id)
|
|
177
|
+
lmid_store.transaction do
|
|
178
|
+
lmid_store.fetch_and_increment(client_group_id, client_id)
|
|
179
|
+
end
|
|
180
|
+
rescue => e
|
|
181
|
+
warn "Failed to persist LMID after application error: #{e.message}"
|
|
74
182
|
end
|
|
75
183
|
|
|
76
184
|
# Validate LMID against the post-increment value.
|
|
@@ -94,29 +202,18 @@ module ZeroRuby
|
|
|
94
202
|
end
|
|
95
203
|
end
|
|
96
204
|
|
|
97
|
-
# Execute mutation with retry logic for app errors
|
|
98
|
-
def execute_with_retry(mutation_data, context)
|
|
99
|
-
attempts = 0
|
|
100
|
-
|
|
101
|
-
loop do
|
|
102
|
-
attempts += 1
|
|
103
|
-
|
|
104
|
-
begin
|
|
105
|
-
return schema.execute_mutation(mutation_data, context)
|
|
106
|
-
rescue ZeroRuby::Error => e
|
|
107
|
-
raise e unless attempts < max_retries
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
|
|
112
205
|
# Format an error into Zero protocol response
|
|
113
206
|
def format_error_response(error)
|
|
114
|
-
result = {error: error.error_type
|
|
207
|
+
result = {error: error.error_type}
|
|
115
208
|
|
|
116
209
|
case error
|
|
117
210
|
when ValidationError
|
|
211
|
+
result[:message] = error.message
|
|
118
212
|
result[:details] = {messages: error.errors}
|
|
213
|
+
when MutationAlreadyProcessedError
|
|
214
|
+
result[:details] = error.message
|
|
119
215
|
else
|
|
216
|
+
result[:message] = error.message
|
|
120
217
|
result[:details] = error.details if error.details
|
|
121
218
|
end
|
|
122
219
|
|
data/lib/zero_ruby/schema.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "errors"
|
|
4
|
+
require_relative "error_formatter"
|
|
4
5
|
|
|
5
6
|
module ZeroRuby
|
|
6
7
|
# Schema class for registering and processing Zero mutations.
|
|
@@ -28,6 +29,13 @@ module ZeroRuby
|
|
|
28
29
|
end
|
|
29
30
|
end
|
|
30
31
|
|
|
32
|
+
# Get handler class for a mutation name
|
|
33
|
+
# @param name [String] The mutation name
|
|
34
|
+
# @return [Class, nil] The handler class or nil if not found
|
|
35
|
+
def handler_for(name)
|
|
36
|
+
mutations[normalize_mutation_name(name)]
|
|
37
|
+
end
|
|
38
|
+
|
|
31
39
|
# Generate TypeScript type definitions from registered mutations
|
|
32
40
|
# @return [String] TypeScript type definitions
|
|
33
41
|
def to_typescript
|
|
@@ -46,50 +54,86 @@ module ZeroRuby
|
|
|
46
54
|
# result = ZeroSchema.execute(body, context: {current_user: user})
|
|
47
55
|
# render json: result
|
|
48
56
|
def execute(push_data, context:, lmid_store: nil)
|
|
57
|
+
validate_push_structure!(push_data)
|
|
58
|
+
|
|
49
59
|
push_version = push_data["pushVersion"]
|
|
50
60
|
supported_version = ZeroRuby.configuration.supported_push_version
|
|
51
61
|
|
|
52
62
|
unless push_version == supported_version
|
|
63
|
+
mutations = push_data["mutations"] || []
|
|
64
|
+
mutation_ids = mutations.map { |m| {id: m["id"], clientID: m["clientID"]} }
|
|
65
|
+
|
|
53
66
|
return {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
67
|
+
kind: "PushFailed",
|
|
68
|
+
origin: "server",
|
|
69
|
+
reason: "unsupportedPushVersion",
|
|
70
|
+
message: "Unsupported push version: #{push_version}. Expected: #{supported_version}",
|
|
71
|
+
mutationIDs: mutation_ids
|
|
59
72
|
}
|
|
60
73
|
end
|
|
61
74
|
|
|
62
75
|
store = lmid_store || ZeroRuby.configuration.lmid_store_instance
|
|
63
76
|
processor = PushProcessor.new(
|
|
64
77
|
schema: self,
|
|
65
|
-
lmid_store: store
|
|
66
|
-
max_retries: ZeroRuby.configuration.max_retry_attempts
|
|
78
|
+
lmid_store: store
|
|
67
79
|
)
|
|
68
80
|
processor.process(push_data, context)
|
|
81
|
+
rescue ParseError => e
|
|
82
|
+
{
|
|
83
|
+
kind: "PushFailed",
|
|
84
|
+
origin: "server",
|
|
85
|
+
reason: "parse",
|
|
86
|
+
message: e.message,
|
|
87
|
+
mutationIDs: []
|
|
88
|
+
}
|
|
69
89
|
end
|
|
70
90
|
|
|
71
91
|
# Execute a single mutation.
|
|
72
92
|
# Used by PushProcessor for LMID-tracked mutations.
|
|
73
93
|
# @param mutation_data [Hash] The mutation data from Zero
|
|
74
94
|
# @param context [Hash] Context hash to pass to mutations
|
|
95
|
+
# @param transact [Proc] Block that wraps transactional work
|
|
75
96
|
# @return [Hash] Empty hash on success
|
|
76
97
|
# @raise [MutationNotFoundError] If the mutation is not registered
|
|
77
98
|
# @raise [ZeroRuby::Error] If the mutation fails
|
|
78
|
-
def execute_mutation(mutation_data, context)
|
|
99
|
+
def execute_mutation(mutation_data, context, &transact)
|
|
79
100
|
name = normalize_mutation_name(mutation_data["name"])
|
|
80
|
-
raw_args = extract_args(mutation_data)
|
|
81
|
-
params = transform_keys(raw_args)
|
|
82
|
-
|
|
83
|
-
ctx = context.freeze
|
|
84
101
|
handler = mutations[name]
|
|
85
|
-
|
|
86
102
|
raise MutationNotFoundError.new(name) unless handler
|
|
87
103
|
|
|
88
|
-
|
|
104
|
+
raw_args = extract_args(mutation_data)
|
|
105
|
+
params = transform_keys(raw_args)
|
|
106
|
+
|
|
107
|
+
handler.new(params, context).call(&transact)
|
|
108
|
+
rescue Dry::Struct::Error => e
|
|
109
|
+
raise ValidationError.new(ErrorFormatter.format_struct_error(e))
|
|
110
|
+
rescue Dry::Types::CoercionError => e
|
|
111
|
+
raise ValidationError.new([ErrorFormatter.format_coercion_error(e)])
|
|
112
|
+
rescue Dry::Types::ConstraintError => e
|
|
113
|
+
raise ValidationError.new([ErrorFormatter.format_constraint_error(e)])
|
|
89
114
|
end
|
|
90
115
|
|
|
91
116
|
private
|
|
92
117
|
|
|
118
|
+
# Validate push data structure per Zero protocol
|
|
119
|
+
# Required fields: clientGroupID, mutations, pushVersion, timestamp, requestID
|
|
120
|
+
# @raise [ParseError] If push data is malformed
|
|
121
|
+
def validate_push_structure!(push_data)
|
|
122
|
+
unless push_data.is_a?(Hash)
|
|
123
|
+
raise ParseError.new("Push data must be a hash")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
%w[clientGroupID mutations pushVersion timestamp requestID].each do |field|
|
|
127
|
+
unless push_data.key?(field)
|
|
128
|
+
raise ParseError.new("Missing required field: #{field}")
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
unless push_data["mutations"].is_a?(Array)
|
|
133
|
+
raise ParseError.new("Field 'mutations' must be an array")
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
93
137
|
# Normalize mutation name (convert | to . for Zero's format)
|
|
94
138
|
def normalize_mutation_name(name)
|
|
95
139
|
return "" if name.nil?
|
|
@@ -105,12 +149,16 @@ module ZeroRuby
|
|
|
105
149
|
args.is_a?(Array) ? (args.first || {}) : args
|
|
106
150
|
end
|
|
107
151
|
|
|
108
|
-
# Transform camelCase string keys to snake_case
|
|
152
|
+
# Transform camelCase string keys to snake_case strings (deep).
|
|
153
|
+
# Keys are kept as strings to prevent symbol table DoS attacks.
|
|
154
|
+
# Symbolization happens later in coerce_and_validate! using schema-defined keys.
|
|
155
|
+
# @param object [Object] The object to transform
|
|
156
|
+
# @return [Object] Transformed object with string keys
|
|
109
157
|
def transform_keys(object)
|
|
110
158
|
case object
|
|
111
159
|
when Hash
|
|
112
160
|
object.each_with_object({}) do |(key, value), result|
|
|
113
|
-
new_key = key.to_s.gsub(/([A-Z])/, '_\1').downcase.delete_prefix("_")
|
|
161
|
+
new_key = key.to_s.gsub(/([A-Z])/, '_\1').downcase.delete_prefix("_")
|
|
114
162
|
result[new_key] = transform_keys(value)
|
|
115
163
|
end
|
|
116
164
|
when Array
|
data/lib/zero_ruby/type_names.rb
CHANGED
|
@@ -1,24 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "types"
|
|
4
|
+
|
|
3
5
|
module ZeroRuby
|
|
4
6
|
# Provides shorthand constants for ZeroRuby types.
|
|
5
7
|
# Include this module to use ID, Boolean, etc. without the ZeroRuby::Types:: prefix.
|
|
6
8
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
+
# @example Including in your own classes
|
|
9
10
|
# class PostCreate < ZeroRuby::Mutation
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# argument :
|
|
11
|
+
# include ZeroRuby::TypeNames
|
|
12
|
+
#
|
|
13
|
+
# argument :id, ID
|
|
14
|
+
# argument :title, Types::String
|
|
15
|
+
# argument :active, Boolean
|
|
13
16
|
# end
|
|
14
17
|
#
|
|
15
|
-
# Note: String, Integer, and Float
|
|
16
|
-
#
|
|
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.
|
|
17
20
|
module TypeNames
|
|
18
21
|
ID = ZeroRuby::Types::ID
|
|
19
22
|
Boolean = ZeroRuby::Types::Boolean
|
|
20
|
-
BigInt = ZeroRuby::Types::BigInt
|
|
21
23
|
ISO8601Date = ZeroRuby::Types::ISO8601Date
|
|
22
24
|
ISO8601DateTime = ZeroRuby::Types::ISO8601DateTime
|
|
25
|
+
|
|
26
|
+
# Provide Types module access without prefix
|
|
27
|
+
Types = ZeroRuby::Types
|
|
23
28
|
end
|
|
24
29
|
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-types"
|
|
4
|
+
|
|
5
|
+
module ZeroRuby
|
|
6
|
+
# Type definitions using dry-types.
|
|
7
|
+
#
|
|
8
|
+
# When inheriting from ZeroRuby::Mutation or ZeroRuby::InputObject, types are
|
|
9
|
+
# available via ZeroRuby::TypeNames which is automatically included:
|
|
10
|
+
# - Direct: ID, Boolean, ISO8601Date, ISO8601DateTime
|
|
11
|
+
# - Via Types: Types::String, Types::Integer, Types::Float
|
|
12
|
+
#
|
|
13
|
+
# @example Basic usage
|
|
14
|
+
# class MyMutation < ZeroRuby::Mutation
|
|
15
|
+
# argument :id, ID
|
|
16
|
+
# argument :name, Types::String
|
|
17
|
+
# argument :count, Types::Integer.optional
|
|
18
|
+
# argument :active, Boolean.default(false)
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# @example With constraints
|
|
22
|
+
# class MyMutation < ZeroRuby::Mutation
|
|
23
|
+
# argument :title, Types::String.constrained(min_size: 1, max_size: 200)
|
|
24
|
+
# argument :count, Types::Integer.constrained(gt: 0)
|
|
25
|
+
# argument :status, Types::String.constrained(included_in: %w[draft published])
|
|
26
|
+
# end
|
|
27
|
+
module Types
|
|
28
|
+
include Dry.Types()
|
|
29
|
+
|
|
30
|
+
# Params types for JSON input
|
|
31
|
+
# These handle string coercions common in form/JSON data
|
|
32
|
+
|
|
33
|
+
# String type (passes through strings, coerces nil)
|
|
34
|
+
String = Params::String
|
|
35
|
+
|
|
36
|
+
# Coerces string numbers to integers (e.g., "42" -> 42)
|
|
37
|
+
Integer = Params::Integer
|
|
38
|
+
|
|
39
|
+
# Coerces string numbers to floats (e.g., "3.14" -> 3.14)
|
|
40
|
+
Float = Params::Float
|
|
41
|
+
|
|
42
|
+
# Coerces string booleans (e.g., "true" -> true, "false" -> false)
|
|
43
|
+
Boolean = Params::Bool
|
|
44
|
+
|
|
45
|
+
# Non-empty string ID type
|
|
46
|
+
ID = Params::String.constrained(filled: true)
|
|
47
|
+
|
|
48
|
+
# ISO8601 date string -> Date object
|
|
49
|
+
ISO8601Date = Params::Date
|
|
50
|
+
|
|
51
|
+
# ISO8601 datetime string -> DateTime object
|
|
52
|
+
ISO8601DateTime = Params::DateTime
|
|
53
|
+
end
|
|
54
|
+
end
|