rubyn-code 0.1.0

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.
Files changed (235) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +620 -0
  4. data/db/migrations/000_create_schema_migrations.sql +4 -0
  5. data/db/migrations/001_create_sessions.sql +16 -0
  6. data/db/migrations/002_create_messages.sql +16 -0
  7. data/db/migrations/003_create_tasks.sql +17 -0
  8. data/db/migrations/004_create_task_dependencies.sql +8 -0
  9. data/db/migrations/005_create_memories.sql +44 -0
  10. data/db/migrations/006_create_cost_records.sql +16 -0
  11. data/db/migrations/007_create_hooks.sql +12 -0
  12. data/db/migrations/008_create_skills_cache.sql +8 -0
  13. data/db/migrations/009_create_teams.sql +27 -0
  14. data/db/migrations/010_create_instincts.sql +15 -0
  15. data/exe/rubyn-code +6 -0
  16. data/lib/rubyn_code/agent/conversation.rb +193 -0
  17. data/lib/rubyn_code/agent/loop.rb +517 -0
  18. data/lib/rubyn_code/agent/loop_detector.rb +78 -0
  19. data/lib/rubyn_code/auth/oauth.rb +174 -0
  20. data/lib/rubyn_code/auth/server.rb +126 -0
  21. data/lib/rubyn_code/auth/token_store.rb +153 -0
  22. data/lib/rubyn_code/autonomous/daemon.rb +233 -0
  23. data/lib/rubyn_code/autonomous/idle_poller.rb +111 -0
  24. data/lib/rubyn_code/autonomous/task_claimer.rb +100 -0
  25. data/lib/rubyn_code/background/job.rb +19 -0
  26. data/lib/rubyn_code/background/notifier.rb +44 -0
  27. data/lib/rubyn_code/background/worker.rb +146 -0
  28. data/lib/rubyn_code/cli/app.rb +118 -0
  29. data/lib/rubyn_code/cli/input_handler.rb +79 -0
  30. data/lib/rubyn_code/cli/renderer.rb +205 -0
  31. data/lib/rubyn_code/cli/repl.rb +519 -0
  32. data/lib/rubyn_code/cli/spinner.rb +100 -0
  33. data/lib/rubyn_code/cli/stream_formatter.rb +149 -0
  34. data/lib/rubyn_code/config/defaults.rb +43 -0
  35. data/lib/rubyn_code/config/project_config.rb +120 -0
  36. data/lib/rubyn_code/config/settings.rb +127 -0
  37. data/lib/rubyn_code/context/auto_compact.rb +81 -0
  38. data/lib/rubyn_code/context/compactor.rb +89 -0
  39. data/lib/rubyn_code/context/manager.rb +91 -0
  40. data/lib/rubyn_code/context/manual_compact.rb +87 -0
  41. data/lib/rubyn_code/context/micro_compact.rb +135 -0
  42. data/lib/rubyn_code/db/connection.rb +176 -0
  43. data/lib/rubyn_code/db/migrator.rb +146 -0
  44. data/lib/rubyn_code/db/schema.rb +106 -0
  45. data/lib/rubyn_code/hooks/built_in.rb +124 -0
  46. data/lib/rubyn_code/hooks/registry.rb +99 -0
  47. data/lib/rubyn_code/hooks/runner.rb +88 -0
  48. data/lib/rubyn_code/hooks/user_hooks.rb +90 -0
  49. data/lib/rubyn_code/learning/extractor.rb +191 -0
  50. data/lib/rubyn_code/learning/injector.rb +138 -0
  51. data/lib/rubyn_code/learning/instinct.rb +172 -0
  52. data/lib/rubyn_code/llm/client.rb +218 -0
  53. data/lib/rubyn_code/llm/message_builder.rb +116 -0
  54. data/lib/rubyn_code/llm/streaming.rb +203 -0
  55. data/lib/rubyn_code/mcp/client.rb +139 -0
  56. data/lib/rubyn_code/mcp/config.rb +83 -0
  57. data/lib/rubyn_code/mcp/sse_transport.rb +225 -0
  58. data/lib/rubyn_code/mcp/stdio_transport.rb +196 -0
  59. data/lib/rubyn_code/mcp/tool_bridge.rb +164 -0
  60. data/lib/rubyn_code/memory/models.rb +62 -0
  61. data/lib/rubyn_code/memory/search.rb +181 -0
  62. data/lib/rubyn_code/memory/session_persistence.rb +194 -0
  63. data/lib/rubyn_code/memory/store.rb +199 -0
  64. data/lib/rubyn_code/observability/budget_enforcer.rb +159 -0
  65. data/lib/rubyn_code/observability/cost_calculator.rb +61 -0
  66. data/lib/rubyn_code/observability/models.rb +29 -0
  67. data/lib/rubyn_code/observability/token_counter.rb +42 -0
  68. data/lib/rubyn_code/observability/usage_reporter.rb +140 -0
  69. data/lib/rubyn_code/output/diff_renderer.rb +212 -0
  70. data/lib/rubyn_code/output/formatter.rb +120 -0
  71. data/lib/rubyn_code/permissions/deny_list.rb +49 -0
  72. data/lib/rubyn_code/permissions/policy.rb +59 -0
  73. data/lib/rubyn_code/permissions/prompter.rb +80 -0
  74. data/lib/rubyn_code/permissions/tier.rb +22 -0
  75. data/lib/rubyn_code/protocols/interrupt_handler.rb +95 -0
  76. data/lib/rubyn_code/protocols/plan_approval.rb +67 -0
  77. data/lib/rubyn_code/protocols/shutdown_handshake.rb +109 -0
  78. data/lib/rubyn_code/skills/catalog.rb +70 -0
  79. data/lib/rubyn_code/skills/document.rb +80 -0
  80. data/lib/rubyn_code/skills/loader.rb +57 -0
  81. data/lib/rubyn_code/sub_agents/runner.rb +168 -0
  82. data/lib/rubyn_code/sub_agents/summarizer.rb +57 -0
  83. data/lib/rubyn_code/tasks/dag.rb +208 -0
  84. data/lib/rubyn_code/tasks/manager.rb +212 -0
  85. data/lib/rubyn_code/tasks/models.rb +31 -0
  86. data/lib/rubyn_code/teams/mailbox.rb +128 -0
  87. data/lib/rubyn_code/teams/manager.rb +175 -0
  88. data/lib/rubyn_code/teams/teammate.rb +38 -0
  89. data/lib/rubyn_code/tools/background_run.rb +41 -0
  90. data/lib/rubyn_code/tools/base.rb +84 -0
  91. data/lib/rubyn_code/tools/bash.rb +81 -0
  92. data/lib/rubyn_code/tools/bundle_add.rb +53 -0
  93. data/lib/rubyn_code/tools/bundle_install.rb +41 -0
  94. data/lib/rubyn_code/tools/compact.rb +57 -0
  95. data/lib/rubyn_code/tools/db_migrate.rb +52 -0
  96. data/lib/rubyn_code/tools/edit_file.rb +49 -0
  97. data/lib/rubyn_code/tools/executor.rb +62 -0
  98. data/lib/rubyn_code/tools/git_commit.rb +97 -0
  99. data/lib/rubyn_code/tools/git_diff.rb +61 -0
  100. data/lib/rubyn_code/tools/git_log.rb +59 -0
  101. data/lib/rubyn_code/tools/git_status.rb +59 -0
  102. data/lib/rubyn_code/tools/glob.rb +44 -0
  103. data/lib/rubyn_code/tools/grep.rb +81 -0
  104. data/lib/rubyn_code/tools/load_skill.rb +41 -0
  105. data/lib/rubyn_code/tools/memory_search.rb +77 -0
  106. data/lib/rubyn_code/tools/memory_write.rb +52 -0
  107. data/lib/rubyn_code/tools/rails_generate.rb +54 -0
  108. data/lib/rubyn_code/tools/read_file.rb +38 -0
  109. data/lib/rubyn_code/tools/read_inbox.rb +64 -0
  110. data/lib/rubyn_code/tools/registry.rb +48 -0
  111. data/lib/rubyn_code/tools/review_pr.rb +145 -0
  112. data/lib/rubyn_code/tools/run_specs.rb +75 -0
  113. data/lib/rubyn_code/tools/schema.rb +59 -0
  114. data/lib/rubyn_code/tools/send_message.rb +53 -0
  115. data/lib/rubyn_code/tools/spawn_agent.rb +154 -0
  116. data/lib/rubyn_code/tools/spawn_teammate.rb +168 -0
  117. data/lib/rubyn_code/tools/task.rb +148 -0
  118. data/lib/rubyn_code/tools/web_fetch.rb +108 -0
  119. data/lib/rubyn_code/tools/web_search.rb +196 -0
  120. data/lib/rubyn_code/tools/write_file.rb +30 -0
  121. data/lib/rubyn_code/version.rb +5 -0
  122. data/lib/rubyn_code.rb +203 -0
  123. data/skills/code_quality/fits_in_your_head.md +189 -0
  124. data/skills/code_quality/naming_conventions.md +213 -0
  125. data/skills/code_quality/null_object.md +205 -0
  126. data/skills/code_quality/technical_debt.md +135 -0
  127. data/skills/code_quality/value_objects.md +216 -0
  128. data/skills/code_quality/yagni.md +176 -0
  129. data/skills/design_patterns/adapter.md +191 -0
  130. data/skills/design_patterns/bridge_memento_visitor.md +254 -0
  131. data/skills/design_patterns/builder.md +158 -0
  132. data/skills/design_patterns/command.md +126 -0
  133. data/skills/design_patterns/composite.md +147 -0
  134. data/skills/design_patterns/decorator.md +204 -0
  135. data/skills/design_patterns/facade.md +133 -0
  136. data/skills/design_patterns/factory_method.md +169 -0
  137. data/skills/design_patterns/iterator.md +116 -0
  138. data/skills/design_patterns/mediator.md +133 -0
  139. data/skills/design_patterns/observer.md +177 -0
  140. data/skills/design_patterns/proxy.md +140 -0
  141. data/skills/design_patterns/singleton.md +124 -0
  142. data/skills/design_patterns/state.md +207 -0
  143. data/skills/design_patterns/strategy.md +127 -0
  144. data/skills/design_patterns/template_method.md +173 -0
  145. data/skills/gems/devise.md +365 -0
  146. data/skills/gems/dry_rb.md +186 -0
  147. data/skills/gems/factory_bot.md +268 -0
  148. data/skills/gems/faraday.md +263 -0
  149. data/skills/gems/graphql_ruby.md +514 -0
  150. data/skills/gems/pundit.md +446 -0
  151. data/skills/gems/redis.md +219 -0
  152. data/skills/gems/rubocop.md +257 -0
  153. data/skills/gems/sidekiq.md +360 -0
  154. data/skills/gems/stripe.md +224 -0
  155. data/skills/minitest/assertions.md +185 -0
  156. data/skills/minitest/fixtures.md +238 -0
  157. data/skills/minitest/integration_tests.md +210 -0
  158. data/skills/minitest/mailers_and_jobs.md +218 -0
  159. data/skills/minitest/mocking_stubbing.md +202 -0
  160. data/skills/minitest/service_tests_and_performance.md +246 -0
  161. data/skills/minitest/structure_and_conventions.md +169 -0
  162. data/skills/minitest/system_tests.md +237 -0
  163. data/skills/rails/action_cable.md +160 -0
  164. data/skills/rails/active_record_basics.md +174 -0
  165. data/skills/rails/active_storage.md +242 -0
  166. data/skills/rails/api_design.md +212 -0
  167. data/skills/rails/associations.md +182 -0
  168. data/skills/rails/background_jobs.md +212 -0
  169. data/skills/rails/caching.md +158 -0
  170. data/skills/rails/callbacks.md +135 -0
  171. data/skills/rails/concerns_controllers.md +218 -0
  172. data/skills/rails/concerns_models.md +280 -0
  173. data/skills/rails/controllers.md +190 -0
  174. data/skills/rails/engines.md +201 -0
  175. data/skills/rails/form_objects.md +168 -0
  176. data/skills/rails/hotwire.md +229 -0
  177. data/skills/rails/internationalization.md +192 -0
  178. data/skills/rails/logging.md +198 -0
  179. data/skills/rails/mailers.md +180 -0
  180. data/skills/rails/migrations.md +200 -0
  181. data/skills/rails/multitenancy.md +207 -0
  182. data/skills/rails/n_plus_one.md +151 -0
  183. data/skills/rails/presenters.md +244 -0
  184. data/skills/rails/query_objects.md +177 -0
  185. data/skills/rails/routing.md +194 -0
  186. data/skills/rails/scopes.md +187 -0
  187. data/skills/rails/security.md +233 -0
  188. data/skills/rails/serializers.md +243 -0
  189. data/skills/rails/service_objects.md +184 -0
  190. data/skills/rails/testing_strategy.md +258 -0
  191. data/skills/rails/validations.md +206 -0
  192. data/skills/refactoring/code_smells.md +251 -0
  193. data/skills/refactoring/command_query_separation.md +166 -0
  194. data/skills/refactoring/encapsulate_collection.md +125 -0
  195. data/skills/refactoring/extract_class.md +138 -0
  196. data/skills/refactoring/extract_method.md +185 -0
  197. data/skills/refactoring/replace_conditional.md +211 -0
  198. data/skills/refactoring/value_objects.md +246 -0
  199. data/skills/rspec/build_stubbed.md +199 -0
  200. data/skills/rspec/factory_design.md +206 -0
  201. data/skills/rspec/let_vs_let_bang.md +161 -0
  202. data/skills/rspec/mocking_stubbing.md +209 -0
  203. data/skills/rspec/request_specs.md +212 -0
  204. data/skills/rspec/service_specs.md +262 -0
  205. data/skills/rspec/shared_examples.md +244 -0
  206. data/skills/rspec/system_specs.md +286 -0
  207. data/skills/rspec/test_performance.md +215 -0
  208. data/skills/ruby/blocks_procs_lambdas.md +204 -0
  209. data/skills/ruby/classes.md +155 -0
  210. data/skills/ruby/concurrency.md +194 -0
  211. data/skills/ruby/data_struct_openstruct.md +158 -0
  212. data/skills/ruby/debugging_profiling.md +204 -0
  213. data/skills/ruby/enumerable_patterns.md +168 -0
  214. data/skills/ruby/exception_handling.md +199 -0
  215. data/skills/ruby/file_io.md +217 -0
  216. data/skills/ruby/hashes.md +195 -0
  217. data/skills/ruby/metaprogramming.md +170 -0
  218. data/skills/ruby/modules.md +210 -0
  219. data/skills/ruby/pattern_matching.md +177 -0
  220. data/skills/ruby/regular_expressions.md +166 -0
  221. data/skills/ruby/result_objects.md +200 -0
  222. data/skills/ruby/strings.md +177 -0
  223. data/skills/ruby_project/bundler_dependencies.md +181 -0
  224. data/skills/ruby_project/cli_tools.md +224 -0
  225. data/skills/ruby_project/rake_tasks.md +146 -0
  226. data/skills/ruby_project/structure.md +261 -0
  227. data/skills/sinatra/application_structure.md +241 -0
  228. data/skills/sinatra/middleware_and_deployment.md +221 -0
  229. data/skills/sinatra/testing.md +233 -0
  230. data/skills/solid/dependency_inversion.md +195 -0
  231. data/skills/solid/interface_segregation.md +237 -0
  232. data/skills/solid/liskov_substitution.md +263 -0
  233. data/skills/solid/open_closed.md +212 -0
  234. data/skills/solid/single_responsibility.md +183 -0
  235. metadata +397 -0
@@ -0,0 +1,514 @@
1
+ # Gem: graphql-ruby
2
+
3
+ ## What It Is
4
+
5
+ The `graphql` gem (graphql-ruby) is the standard Ruby implementation of GraphQL. It provides a type system, schema definition, query execution, mutations, subscriptions, and tooling for building GraphQL APIs in Rails. It's powerful but has a steep learning curve and many subtle gotchas.
6
+
7
+ ## Setup Done Right
8
+
9
+ ```ruby
10
+ # Gemfile
11
+ gem 'graphql'
12
+
13
+ # Generate the schema
14
+ rails generate graphql:install
15
+
16
+ # This creates:
17
+ # app/graphql/your_app_schema.rb
18
+ # app/graphql/types/query_type.rb
19
+ # app/graphql/types/mutation_type.rb
20
+ # app/graphql/types/base_*.rb
21
+ # app/controllers/graphql_controller.rb
22
+ ```
23
+
24
+ ```ruby
25
+ # app/graphql/rubyn_schema.rb
26
+ class RubynSchema < GraphQL::Schema
27
+ query Types::QueryType
28
+ mutation Types::MutationType
29
+
30
+ # IMPORTANT: Set max complexity and depth to prevent abuse
31
+ max_complexity 300
32
+ max_depth 15
33
+ default_max_page_size 25
34
+
35
+ # Use dataloader for N+1 prevention (replaces batch-loader gems)
36
+ use GraphQL::Dataloader
37
+
38
+ # Error handling
39
+ rescue_from(ActiveRecord::RecordNotFound) do |err, obj, args, ctx, field|
40
+ raise GraphQL::ExecutionError, "#{field.type.unwrap.graphql_name} not found"
41
+ end
42
+
43
+ rescue_from(Pundit::NotAuthorizedError) do |err, obj, args, ctx, field|
44
+ raise GraphQL::ExecutionError, "Not authorized"
45
+ end
46
+ end
47
+ ```
48
+
49
+ ## Gotcha #1: N+1 Queries Are Silent and Devastating
50
+
51
+ GraphQL's nested structure naturally creates N+1 queries. A query for 25 orders with their users and line items can generate 75+ queries if you're not careful.
52
+
53
+ ```graphql
54
+ # This innocent query causes N+1 hell
55
+ {
56
+ orders(first: 25) {
57
+ nodes {
58
+ id
59
+ total
60
+ user { # N queries for users
61
+ name
62
+ email
63
+ }
64
+ lineItems { # N queries for line items
65
+ product { # N*M queries for products
66
+ name
67
+ }
68
+ }
69
+ }
70
+ }
71
+ }
72
+ ```
73
+
74
+ **WRONG: Direct association access in type resolvers**
75
+
76
+ ```ruby
77
+ # app/graphql/types/order_type.rb
78
+ class Types::OrderType < Types::BaseObject
79
+ field :user, Types::UserType, null: false
80
+ field :line_items, [Types::LineItemType], null: false
81
+
82
+ # These default resolvers call order.user and order.line_items
83
+ # Each call is a separate query — N+1!
84
+ end
85
+ ```
86
+
87
+ **RIGHT: Use GraphQL::Dataloader (built-in since graphql-ruby 2.0)**
88
+
89
+ ```ruby
90
+ # app/graphql/sources/record_source.rb
91
+ class Sources::RecordSource < GraphQL::Dataloader::Source
92
+ def initialize(model_class, column: :id)
93
+ @model_class = model_class
94
+ @column = column
95
+ end
96
+
97
+ def fetch(ids)
98
+ records = @model_class.where(@column => ids)
99
+ ids.map { |id| records.find { |r| r.public_send(@column) == id } }
100
+ end
101
+ end
102
+
103
+ # app/graphql/sources/association_source.rb
104
+ class Sources::AssociationSource < GraphQL::Dataloader::Source
105
+ def initialize(model_class, association_name)
106
+ @model_class = model_class
107
+ @association_name = association_name
108
+ end
109
+
110
+ def fetch(records)
111
+ ActiveRecord::Associations::Preloader.new(
112
+ records: records,
113
+ associations: @association_name
114
+ ).call
115
+
116
+ records.map { |record| record.public_send(@association_name) }
117
+ end
118
+ end
119
+
120
+ # app/graphql/types/order_type.rb
121
+ class Types::OrderType < Types::BaseObject
122
+ field :user, Types::UserType, null: false
123
+ field :line_items, [Types::LineItemType], null: false
124
+
125
+ def user
126
+ dataloader.with(Sources::RecordSource, User).load(object.user_id)
127
+ end
128
+
129
+ def line_items
130
+ dataloader.with(Sources::AssociationSource, Order, :line_items).load(object)
131
+ end
132
+ end
133
+ ```
134
+
135
+ **The trap:** Everything works in development with 5 records. In production with 25 records per page, the query takes 3 seconds and makes 200 database calls. Always check your query count with `bullet` or query logs.
136
+
137
+ ## Gotcha #2: Authorization — Don't Trust the Client
138
+
139
+ GraphQL clients can query any field in the schema. Authorization must happen at the field/type level, not just at the query root.
140
+
141
+ ```ruby
142
+ # WRONG: Only checking auth at the query root
143
+ class Types::QueryType < Types::BaseObject
144
+ field :orders, Types::OrderType.connection_type, null: false
145
+
146
+ def orders
147
+ # Checks that user is logged in... but returns ALL orders
148
+ raise GraphQL::ExecutionError, "Not authenticated" unless context[:current_user]
149
+ Order.all # SECURITY HOLE: User sees everyone's orders
150
+ end
151
+ end
152
+
153
+ # RIGHT: Scope queries AND authorize individual records
154
+ class Types::QueryType < Types::BaseObject
155
+ field :orders, Types::OrderType.connection_type, null: false
156
+
157
+ def orders
158
+ raise GraphQL::ExecutionError, "Not authenticated" unless context[:current_user]
159
+ OrderPolicy::Scope.new(context[:current_user], Order).resolve.order(created_at: :desc)
160
+ end
161
+ end
162
+
163
+ # RIGHT: Authorize field-level access for sensitive fields
164
+ class Types::UserType < Types::BaseObject
165
+ field :email, String, null: false
166
+ field :credit_balance, Integer, null: false
167
+
168
+ # Only show email to the user themselves or admins
169
+ def email
170
+ if context[:current_user] == object || context[:current_user]&.admin?
171
+ object.email
172
+ else
173
+ raise GraphQL::ExecutionError, "Not authorized to view email"
174
+ end
175
+ end
176
+
177
+ # Only show credit balance to the user themselves
178
+ def credit_balance
179
+ raise GraphQL::ExecutionError, "Not authorized" unless context[:current_user] == object
180
+ object.credit_balance
181
+ end
182
+ end
183
+ ```
184
+
185
+ **The trap:** A user queries `{ users { email creditBalance } }` and sees everyone's email and credit balance. Field-level auth is essential because clients control which fields they request.
186
+
187
+ ## Gotcha #3: Context Setup in the Controller
188
+
189
+ The `context` hash is your bridge between Rails and GraphQL. Set it up once, use it everywhere.
190
+
191
+ ```ruby
192
+ # app/controllers/graphql_controller.rb
193
+ class GraphqlController < ApplicationController
194
+ skip_before_action :verify_authenticity_token # API endpoint, not a form
195
+
196
+ def execute
197
+ result = RubynSchema.execute(
198
+ params[:query],
199
+ variables: prepare_variables(params[:variables]),
200
+ context: {
201
+ current_user: current_user, # From Devise or your auth
202
+ request: request, # For IP, user agent
203
+ pundit_user: current_user # If using Pundit integration
204
+ },
205
+ operation_name: params[:operationName]
206
+ )
207
+ render json: result
208
+ rescue StandardError => e
209
+ handle_error(e)
210
+ end
211
+
212
+ private
213
+
214
+ def prepare_variables(variables_param)
215
+ case variables_param
216
+ when String then variables_param.present? ? JSON.parse(variables_param) : {}
217
+ when Hash then variables_param
218
+ when ActionController::Parameters then variables_param.to_unsafe_hash
219
+ when nil then {}
220
+ else raise ArgumentError, "Unexpected variables: #{variables_param}"
221
+ end
222
+ end
223
+
224
+ def handle_error(e)
225
+ Rails.logger.error("GraphQL Error: #{e.message}\n#{e.backtrace.first(5).join("\n")}")
226
+ render json: { errors: [{ message: "Internal server error" }] }, status: :internal_server_error
227
+ end
228
+ end
229
+ ```
230
+
231
+ **The trap:** `params[:variables]` can be a String (from a POST body), a Hash (from a JSON body), or ActionController::Parameters (from Rails). If you don't handle all three, queries with variables break intermittently depending on the client library.
232
+
233
+ ## Gotcha #4: Mutations — Input Types and Error Handling
234
+
235
+ ```ruby
236
+ # WRONG: Returning generic error strings
237
+ class Mutations::CreateOrder < Mutations::BaseMutation
238
+ argument :shipping_address, String, required: true
239
+
240
+ field :order, Types::OrderType, null: true
241
+
242
+ def resolve(shipping_address:)
243
+ order = context[:current_user].orders.create!(shipping_address: shipping_address)
244
+ { order: order }
245
+ rescue ActiveRecord::RecordInvalid => e
246
+ raise GraphQL::ExecutionError, e.message # Lumps all errors into one string
247
+ end
248
+ end
249
+ ```
250
+
251
+ ```ruby
252
+ # RIGHT: Structured error responses with field-level errors
253
+ class Mutations::CreateOrder < Mutations::BaseMutation
254
+ argument :shipping_address, String, required: true
255
+ argument :note, String, required: false
256
+
257
+ field :order, Types::OrderType, null: true
258
+ field :errors, [Types::UserErrorType], null: false
259
+
260
+ def resolve(shipping_address:, note: nil)
261
+ authorize_action!
262
+
263
+ order = context[:current_user].orders.build(
264
+ shipping_address: shipping_address,
265
+ note: note
266
+ )
267
+
268
+ if order.save
269
+ { order: order, errors: [] }
270
+ else
271
+ {
272
+ order: nil,
273
+ errors: order.errors.map { |e|
274
+ { field: e.attribute.to_s.camelize(:lower), message: e.full_message }
275
+ }
276
+ }
277
+ end
278
+ end
279
+
280
+ private
281
+
282
+ def authorize_action!
283
+ raise GraphQL::ExecutionError, "Not authenticated" unless context[:current_user]
284
+ raise GraphQL::ExecutionError, "Insufficient credits" unless context[:current_user].credit_balance > 0
285
+ end
286
+ end
287
+
288
+ # app/graphql/types/user_error_type.rb
289
+ class Types::UserErrorType < Types::BaseObject
290
+ field :field, String, null: true, description: "Which input field the error relates to"
291
+ field :message, String, null: false, description: "Human-readable error message"
292
+ end
293
+ ```
294
+
295
+ **The trap:** `GraphQL::ExecutionError` puts errors in the top-level `errors` array, which clients typically treat as unexpected failures. Validation errors should be in the `data` response as structured fields, so the client can display them next to form fields.
296
+
297
+ ## Gotcha #5: Connection Types and Pagination
298
+
299
+ ```ruby
300
+ # WRONG: Returning a plain array — no pagination
301
+ field :orders, [Types::OrderType], null: false
302
+ def orders
303
+ Order.all # Loads every order into memory
304
+ end
305
+
306
+ # RIGHT: Use connection types for automatic cursor-based pagination
307
+ field :orders, Types::OrderType.connection_type, null: false
308
+
309
+ def orders
310
+ policy_scope(Order).order(created_at: :desc)
311
+ # GraphQL handles first/last/before/after automatically
312
+ end
313
+ ```
314
+
315
+ ```graphql
316
+ # Client gets pagination for free
317
+ {
318
+ orders(first: 10, after: "MjA=") {
319
+ edges {
320
+ node {
321
+ id
322
+ total
323
+ }
324
+ cursor
325
+ }
326
+ pageInfo {
327
+ hasNextPage
328
+ endCursor
329
+ }
330
+ }
331
+ }
332
+ ```
333
+
334
+ **The trap:** Without connection types, requesting 10,000 orders loads them all. Connection types enforce pagination and provide cursor-based navigation. Set `default_max_page_size` in your schema.
335
+
336
+ ## Gotcha #6: Enum Types Must Match Exactly
337
+
338
+ ```ruby
339
+ # WRONG: String values that don't match DB enum
340
+ class Types::OrderStatusEnum < Types::BaseEnum
341
+ value "PENDING" # GraphQL convention is SCREAMING_SNAKE
342
+ value "CONFIRMED"
343
+ value "SHIPPED"
344
+ end
345
+
346
+ # But the DB stores "pending", "confirmed", "shipped"
347
+ # This silently doesn't match — filters return no results
348
+ ```
349
+
350
+ ```ruby
351
+ # RIGHT: Map GraphQL values to DB values
352
+ class Types::OrderStatusEnum < Types::BaseEnum
353
+ value "PENDING", value: "pending"
354
+ value "CONFIRMED", value: "confirmed"
355
+ value "SHIPPED", value: "shipped"
356
+ value "DELIVERED", value: "delivered"
357
+ value "CANCELLED", value: "cancelled"
358
+ end
359
+
360
+ # Usage in a query argument
361
+ field :orders, Types::OrderType.connection_type, null: false do
362
+ argument :status, Types::OrderStatusEnum, required: false
363
+ end
364
+
365
+ def orders(status: nil)
366
+ scope = policy_scope(Order)
367
+ scope = scope.where(status: status) if status # status is now "pending", not "PENDING"
368
+ scope.order(created_at: :desc)
369
+ end
370
+ ```
371
+
372
+ ## Gotcha #7: Circular Type References
373
+
374
+ Types that reference each other (User has Orders, Order has User) can cause load-order issues.
375
+
376
+ ```ruby
377
+ # This can cause "uninitialized constant Types::OrderType" if load order is wrong
378
+ class Types::UserType < Types::BaseObject
379
+ field :orders, [Types::OrderType], null: false # OrderType may not be loaded yet
380
+ end
381
+
382
+ # FIX: Use a string or lambda for lazy resolution
383
+ class Types::UserType < Types::BaseObject
384
+ field :orders, [Types::OrderType], null: false
385
+ # graphql-ruby handles circular references automatically in modern versions
386
+ # But if you get load errors, use:
387
+ field :orders, ["Types::OrderType"], null: false # String reference, resolved lazily
388
+ end
389
+ ```
390
+
391
+ ## Gotcha #8: Testing GraphQL
392
+
393
+ ```ruby
394
+ # spec/support/graphql_helpers.rb
395
+ module GraphqlHelpers
396
+ def execute_query(query, variables: {}, user: nil)
397
+ RubynSchema.execute(
398
+ query,
399
+ variables: variables,
400
+ context: { current_user: user }
401
+ )
402
+ end
403
+
404
+ def graphql_data(result)
405
+ result["data"]
406
+ end
407
+
408
+ def graphql_errors(result)
409
+ result["errors"]
410
+ end
411
+ end
412
+
413
+ RSpec.configure do |config|
414
+ config.include GraphqlHelpers, type: :graphql
415
+ end
416
+ ```
417
+
418
+ ```ruby
419
+ # spec/graphql/queries/orders_query_spec.rb
420
+ RSpec.describe "orders query", type: :graphql do
421
+ let(:user) { create(:user) }
422
+ let!(:orders) { create_list(:order, 3, user: user) }
423
+ let!(:other_order) { create(:order) }
424
+
425
+ let(:query) do
426
+ <<~GQL
427
+ query($first: Int) {
428
+ orders(first: $first) {
429
+ nodes {
430
+ id
431
+ total
432
+ }
433
+ }
434
+ }
435
+ GQL
436
+ end
437
+
438
+ it "returns only the user's orders" do
439
+ result = execute_query(query, variables: { first: 10 }, user: user)
440
+
441
+ expect(graphql_errors(result)).to be_nil
442
+ nodes = graphql_data(result).dig("orders", "nodes")
443
+ expect(nodes.length).to eq(3)
444
+ end
445
+
446
+ it "requires authentication" do
447
+ result = execute_query(query, user: nil)
448
+ expect(graphql_errors(result).first["message"]).to include("Not authenticated")
449
+ end
450
+ end
451
+ ```
452
+
453
+ ```ruby
454
+ # spec/graphql/mutations/create_order_mutation_spec.rb
455
+ RSpec.describe "createOrder mutation", type: :graphql do
456
+ let(:user) { create(:user, credit_balance: 100) }
457
+
458
+ let(:mutation) do
459
+ <<~GQL
460
+ mutation($input: CreateOrderInput!) {
461
+ createOrder(input: $input) {
462
+ order {
463
+ id
464
+ shippingAddress
465
+ }
466
+ errors {
467
+ field
468
+ message
469
+ }
470
+ }
471
+ }
472
+ GQL
473
+ end
474
+
475
+ it "creates an order" do
476
+ result = execute_query(mutation, variables: {
477
+ input: { shippingAddress: "123 Main St" }
478
+ }, user: user)
479
+
480
+ data = graphql_data(result)["createOrder"]
481
+ expect(data["errors"]).to be_empty
482
+ expect(data["order"]["shippingAddress"]).to eq("123 Main St")
483
+ end
484
+
485
+ it "returns validation errors" do
486
+ result = execute_query(mutation, variables: {
487
+ input: { shippingAddress: "" }
488
+ }, user: user)
489
+
490
+ data = graphql_data(result)["createOrder"]
491
+ expect(data["order"]).to be_nil
492
+ expect(data["errors"].first["field"]).to eq("shippingAddress")
493
+ end
494
+ end
495
+ ```
496
+
497
+ ## Do's and Don'ts Summary
498
+
499
+ **DO:**
500
+ - Use `GraphQL::Dataloader` for every association — N+1 is the default without it
501
+ - Set `max_complexity`, `max_depth`, and `default_max_page_size` to prevent abuse
502
+ - Use connection types for all list fields
503
+ - Return structured errors from mutations (not just `GraphQL::ExecutionError`)
504
+ - Authorize at field level, not just query level
505
+ - Handle all `variables` formats in the controller (String, Hash, ActionController::Parameters)
506
+
507
+ **DON'T:**
508
+ - Don't return plain arrays — use connection types for pagination
509
+ - Don't trust that clients only request authorized fields — enforce it server-side
510
+ - Don't use `GraphQL::ExecutionError` for validation errors — use structured error fields
511
+ - Don't forget `skip_before_action :verify_authenticity_token` on the GraphQL controller
512
+ - Don't define enum values without mapping to DB values
513
+ - Don't load records without scoping to the current user first
514
+ - Don't put business logic in resolvers — delegate to service objects