zero_ruby 0.1.0.alpha1 → 0.1.0.alpha2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ba93bb14b442ea97c4ac597b0c97e7b9e0ec00a334e5f2fd6bb3ee7639d47302
4
- data.tar.gz: 741f8f22deb20a49cf7ffd75a03657faf28d561ad32a03debb74f4f5b4e41de8
3
+ metadata.gz: 9d8baa0973deb010422a2e5d6679af4580b9c37666b2649f383c0bf6525a77c9
4
+ data.tar.gz: 27e0a5593d3344c6260d96838e94ce94a8af36225b861318091ed57532a92dda
5
5
  SHA512:
6
- metadata.gz: bab7af54647503b8322eef911336dd9abfe952eaa574baa7f61da57e72e234754a03287354f24af6022b15731097c43a51b5ba67ecb441d0dcc148de834b7b2d
7
- data.tar.gz: 9a4015f062da3df3cdfbc32fc00f747046dee0a30d03228995b59049b9ce2a44c0df10836db173f906ceb8aebd76d935a116576d74848187c88470b7aeb3c4bd
6
+ metadata.gz: 83b6256cf6db475ad7f6c5de6d958f6ed72627dd2d2ae5211183d2ed2fe2c346338fbda6cb52ca79aa7aaf7c343e342e017b4015004cba3c97d351da80b679de
7
+ data.tar.gz: 694ac953e5df97e3a747054846612923928d24eb3a184d5f53c999895419f2ba3885fb128034c8bd54b934c976e832d8cf58fb5a7469c8ace7e75f91185266c4
data/README.md CHANGED
@@ -22,41 +22,8 @@ gem 'zero_ruby'
22
22
 
23
23
  ## Usage
24
24
 
25
- ### 1. Base classes (optional)
26
25
 
27
- Create base classes to share behavior across mutations and input types:
28
-
29
- ```ruby
30
- # app/zero/types/base_input_object.rb
31
- module Types
32
- class BaseInputObject < ZeroRuby::InputObject
33
- # Add shared behavior across all input objects here
34
- end
35
- end
36
-
37
- # app/zero/mutations/application_mutation.rb
38
- class ApplicationMutation < ZeroRuby::Mutation
39
- def current_user
40
- ctx[:current_user]
41
- end
42
- end
43
- ```
44
-
45
- ### 2. Define custom input types (optional)
46
-
47
- ```ruby
48
- # app/zero/types/post_input.rb
49
- module Types
50
- class PostInput < Types::BaseInputObject
51
- argument :title, String, required: true,
52
- validates: { length: { minimum: 1, maximum: 200 } }
53
- argument :body, String, required: false
54
- argument :published, Boolean, required: false, default: false
55
- end
56
- end
57
- ```
58
-
59
- ### 3. Define mutations
26
+ ### 1. Define mutations
60
27
 
61
28
  ```ruby
62
29
  # app/zero/mutations/post_update.rb
@@ -73,7 +40,7 @@ module Mutations
73
40
  end
74
41
  ```
75
42
 
76
- ### 4. Register mutations in schema
43
+ ### 2. Register mutations in schema
77
44
 
78
45
  ```ruby
79
46
  # app/zero/app_schema.rb
@@ -85,7 +52,7 @@ class ZeroSchema < ZeroRuby::Schema
85
52
  end
86
53
  ```
87
54
 
88
- ### 5. Add controller
55
+ ### 3. Add zero_controller and route
89
56
 
90
57
  ```ruby
91
58
  # app/controllers/zero_controller.rb
@@ -112,23 +79,55 @@ class ZeroController < ApplicationController
112
79
  end
113
80
  rescue JSON::ParserError => e
114
81
  render json: {
115
- error: {
116
- kind: "PushFailed",
117
- reason: "Parse",
118
- message: "Invalid JSON: #{e.message}"
119
- }
82
+ kind: "PushFailed",
83
+ origin: "server",
84
+ reason: "parse",
85
+ message: "Invalid JSON: #{e.message}",
86
+ mutationIDs: []
120
87
  }, status: :bad_request
121
88
  end
122
89
  end
123
90
  ```
124
91
 
125
- ### 6. Add route
126
-
127
92
  ```ruby
128
93
  # config/routes.rb
129
94
  match '/zero/push', to: 'zero#push', via: [:get, :post]
130
95
  ```
131
96
 
97
+ ## Base classes (optional)
98
+
99
+ Create base classes to share behavior across mutations and input types:
100
+
101
+ ```ruby
102
+ # app/zero/types/base_input_object.rb
103
+ module Types
104
+ class BaseInputObject < ZeroRuby::InputObject
105
+ # Add shared behavior across all input objects here
106
+ end
107
+ end
108
+
109
+ # app/zero/mutations/application_mutation.rb
110
+ class ApplicationMutation < ZeroRuby::Mutation
111
+ def current_user
112
+ ctx[:current_user]
113
+ end
114
+ end
115
+ ```
116
+
117
+ ## Define custom input types (optional)
118
+
119
+ ```ruby
120
+ # app/zero/types/post_input.rb
121
+ module Types
122
+ class PostInput < Types::BaseInputObject
123
+ argument :title, String, required: true,
124
+ validates: { length: { minimum: 1, maximum: 200 } }
125
+ argument :body, String, required: false
126
+ argument :published, Boolean, required: false, default: false
127
+ end
128
+ end
129
+ ```
130
+
132
131
  ## Configuration
133
132
 
134
133
  Create an initializer to customize settings (all options have sensible defaults):
@@ -179,7 +178,7 @@ export const mutators = defineMutators({
179
178
  update: defineMutator(postsUpdateArgsSchema, async ({ tx, args }) => {
180
179
  await tx.mutate.posts.update({
181
180
  id: args.id,
182
- ...(args.postInput.title !== undefined && { title: args.postInput.title }),
181
+ title: args.postInput.title,
183
182
  updatedAt: Date.now(),
184
183
  })
185
184
  }),
@@ -191,8 +190,6 @@ export type Mutators = typeof mutators
191
190
 
192
191
  ## Validation
193
192
 
194
- Built-in validators:
195
-
196
193
  ```ruby
197
194
  argument :name, String, required: true,
198
195
  validates: {
@@ -87,4 +87,19 @@ module ZeroRuby
87
87
  "ooo"
88
88
  end
89
89
  end
90
+
91
+ # Raised for database transaction errors.
92
+ # Triggers top-level PushFailed with reason: "database"
93
+ class DatabaseError < Error
94
+ end
95
+
96
+ # Raised when push data is malformed or missing required fields.
97
+ # Triggers top-level PushFailed with reason: "parse"
98
+ class ParseError < Error
99
+ end
100
+
101
+ # Raised for unexpected internal server errors.
102
+ # Wrapped as app error per Zero protocol (no "internal" error type at mutation level)
103
+ class InternalError < Error
104
+ end
90
105
  end
@@ -120,11 +120,13 @@ module ZeroRuby
120
120
  end
121
121
 
122
122
  # Execute the mutation
123
- # @return [Hash] Empty hash on success
123
+ # @return [Hash] Empty hash on success, or {data: ...} if execute returns a Hash
124
124
  # @raise [ZeroRuby::Error] On failure (formatted at boundary)
125
125
  def call
126
- execute(**@args)
127
- {}
126
+ data = execute(**@args)
127
+ result = {}
128
+ result[:data] = data if data.is_a?(Hash) && !data.empty?
129
+ result
128
130
  end
129
131
 
130
132
  private
@@ -36,12 +36,29 @@ module ZeroRuby
36
36
  mutations = push_data["mutations"] || []
37
37
  results = []
38
38
 
39
- mutations.each do |mutation_data|
39
+ mutations.each_with_index do |mutation_data, index|
40
40
  result = process_mutation_with_lmid(mutation_data, client_group_id, context)
41
41
  results << result
42
-
43
- # If we hit an out-of-order error, stop processing the batch
44
- break if result[:result][:error] == "ooo"
42
+ rescue OutOfOrderMutationError => e
43
+ # Return top-level PushFailedBody with all unprocessed mutation IDs
44
+ unprocessed_ids = mutations[index..].map { |m| {id: m["id"], clientID: m["clientID"]} }
45
+ return {
46
+ kind: "PushFailed",
47
+ origin: "server",
48
+ reason: "oooMutation",
49
+ message: e.message,
50
+ mutationIDs: unprocessed_ids
51
+ }
52
+ rescue DatabaseError => e
53
+ # Database errors trigger top-level PushFailed per Zero protocol
54
+ unprocessed_ids = mutations[index..].map { |m| {id: m["id"], clientID: m["clientID"]} }
55
+ return {
56
+ kind: "PushFailed",
57
+ origin: "server",
58
+ reason: "database",
59
+ message: e.message,
60
+ mutationIDs: unprocessed_ids
61
+ }
45
62
  end
46
63
 
47
64
  {mutations: results}
@@ -69,6 +86,8 @@ module ZeroRuby
69
86
  result = execute_with_retry(mutation_data, context)
70
87
  {id: mutation_id_obj, result: result}
71
88
  end
89
+ rescue OutOfOrderMutationError, DatabaseError
90
+ raise
72
91
  rescue ZeroRuby::Error => e
73
92
  {id: mutation_id_obj, result: format_error_response(e)}
74
93
  end
@@ -111,12 +130,16 @@ module ZeroRuby
111
130
 
112
131
  # Format an error into Zero protocol response
113
132
  def format_error_response(error)
114
- result = {error: error.error_type, message: error.message}
133
+ result = {error: error.error_type}
115
134
 
116
135
  case error
117
136
  when ValidationError
137
+ result[:message] = error.message
118
138
  result[:details] = {messages: error.errors}
139
+ when MutationAlreadyProcessedError
140
+ result[:details] = error.message
119
141
  else
142
+ result[:message] = error.message
120
143
  result[:details] = error.details if error.details
121
144
  end
122
145
 
@@ -46,16 +46,21 @@ module ZeroRuby
46
46
  # result = ZeroSchema.execute(body, context: {current_user: user})
47
47
  # render json: result
48
48
  def execute(push_data, context:, lmid_store: nil)
49
+ validate_push_structure!(push_data)
50
+
49
51
  push_version = push_data["pushVersion"]
50
52
  supported_version = ZeroRuby.configuration.supported_push_version
51
53
 
52
54
  unless push_version == supported_version
55
+ mutations = push_data["mutations"] || []
56
+ mutation_ids = mutations.map { |m| {id: m["id"], clientID: m["clientID"]} }
57
+
53
58
  return {
54
- error: {
55
- kind: "PushFailed",
56
- reason: "UnsupportedPushVersion",
57
- message: "Unsupported push version: #{push_version}. Expected: #{supported_version}"
58
- }
59
+ kind: "PushFailed",
60
+ origin: "server",
61
+ reason: "unsupportedPushVersion",
62
+ message: "Unsupported push version: #{push_version}. Expected: #{supported_version}",
63
+ mutationIDs: mutation_ids
59
64
  }
60
65
  end
61
66
 
@@ -66,6 +71,14 @@ module ZeroRuby
66
71
  max_retries: ZeroRuby.configuration.max_retry_attempts
67
72
  )
68
73
  processor.process(push_data, context)
74
+ rescue ParseError => e
75
+ {
76
+ kind: "PushFailed",
77
+ origin: "server",
78
+ reason: "parse",
79
+ message: e.message,
80
+ mutationIDs: []
81
+ }
69
82
  end
70
83
 
71
84
  # Execute a single mutation.
@@ -90,6 +103,17 @@ module ZeroRuby
90
103
 
91
104
  private
92
105
 
106
+ # Validate push data structure
107
+ # @raise [ParseError] If push data is malformed
108
+ def validate_push_structure!(push_data)
109
+ unless push_data.is_a?(Hash)
110
+ raise ParseError.new("Push data must be a hash")
111
+ end
112
+ unless push_data.key?("clientGroupID")
113
+ raise ParseError.new("Missing required field: clientGroupID")
114
+ end
115
+ end
116
+
93
117
  # Normalize mutation name (convert | to . for Zero's format)
94
118
  def normalize_mutation_name(name)
95
119
  return "" if name.nil?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ZeroRuby
4
- VERSION = "0.1.0.alpha1"
4
+ VERSION = "0.1.0.alpha2"
5
5
  end
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.alpha1
4
+ version: 0.1.0.alpha2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Serban
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-12-20 00:00:00.000000000 Z
10
+ date: 2025-12-21 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: bundler
@@ -79,7 +79,7 @@ dependencies:
79
79
  - - "~>"
80
80
  - !ruby/object:Gem::Version
81
81
  version: '1.51'
82
- description: Handle Zero mutations with runtime type safety
82
+ description: Handle Zero mutations
83
83
  executables: []
84
84
  extensions: []
85
85
  extra_rdoc_files: []
@@ -141,5 +141,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
141
141
  requirements: []
142
142
  rubygems_version: 3.6.2
143
143
  specification_version: 4
144
- summary: Ruby gem for handling Zero mutations with type safety
144
+ summary: Ruby gem for handling Zero mutations
145
145
  test_files: []