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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +620 -0
- data/db/migrations/000_create_schema_migrations.sql +4 -0
- data/db/migrations/001_create_sessions.sql +16 -0
- data/db/migrations/002_create_messages.sql +16 -0
- data/db/migrations/003_create_tasks.sql +17 -0
- data/db/migrations/004_create_task_dependencies.sql +8 -0
- data/db/migrations/005_create_memories.sql +44 -0
- data/db/migrations/006_create_cost_records.sql +16 -0
- data/db/migrations/007_create_hooks.sql +12 -0
- data/db/migrations/008_create_skills_cache.sql +8 -0
- data/db/migrations/009_create_teams.sql +27 -0
- data/db/migrations/010_create_instincts.sql +15 -0
- data/exe/rubyn-code +6 -0
- data/lib/rubyn_code/agent/conversation.rb +193 -0
- data/lib/rubyn_code/agent/loop.rb +517 -0
- data/lib/rubyn_code/agent/loop_detector.rb +78 -0
- data/lib/rubyn_code/auth/oauth.rb +174 -0
- data/lib/rubyn_code/auth/server.rb +126 -0
- data/lib/rubyn_code/auth/token_store.rb +153 -0
- data/lib/rubyn_code/autonomous/daemon.rb +233 -0
- data/lib/rubyn_code/autonomous/idle_poller.rb +111 -0
- data/lib/rubyn_code/autonomous/task_claimer.rb +100 -0
- data/lib/rubyn_code/background/job.rb +19 -0
- data/lib/rubyn_code/background/notifier.rb +44 -0
- data/lib/rubyn_code/background/worker.rb +146 -0
- data/lib/rubyn_code/cli/app.rb +118 -0
- data/lib/rubyn_code/cli/input_handler.rb +79 -0
- data/lib/rubyn_code/cli/renderer.rb +205 -0
- data/lib/rubyn_code/cli/repl.rb +519 -0
- data/lib/rubyn_code/cli/spinner.rb +100 -0
- data/lib/rubyn_code/cli/stream_formatter.rb +149 -0
- data/lib/rubyn_code/config/defaults.rb +43 -0
- data/lib/rubyn_code/config/project_config.rb +120 -0
- data/lib/rubyn_code/config/settings.rb +127 -0
- data/lib/rubyn_code/context/auto_compact.rb +81 -0
- data/lib/rubyn_code/context/compactor.rb +89 -0
- data/lib/rubyn_code/context/manager.rb +91 -0
- data/lib/rubyn_code/context/manual_compact.rb +87 -0
- data/lib/rubyn_code/context/micro_compact.rb +135 -0
- data/lib/rubyn_code/db/connection.rb +176 -0
- data/lib/rubyn_code/db/migrator.rb +146 -0
- data/lib/rubyn_code/db/schema.rb +106 -0
- data/lib/rubyn_code/hooks/built_in.rb +124 -0
- data/lib/rubyn_code/hooks/registry.rb +99 -0
- data/lib/rubyn_code/hooks/runner.rb +88 -0
- data/lib/rubyn_code/hooks/user_hooks.rb +90 -0
- data/lib/rubyn_code/learning/extractor.rb +191 -0
- data/lib/rubyn_code/learning/injector.rb +138 -0
- data/lib/rubyn_code/learning/instinct.rb +172 -0
- data/lib/rubyn_code/llm/client.rb +218 -0
- data/lib/rubyn_code/llm/message_builder.rb +116 -0
- data/lib/rubyn_code/llm/streaming.rb +203 -0
- data/lib/rubyn_code/mcp/client.rb +139 -0
- data/lib/rubyn_code/mcp/config.rb +83 -0
- data/lib/rubyn_code/mcp/sse_transport.rb +225 -0
- data/lib/rubyn_code/mcp/stdio_transport.rb +196 -0
- data/lib/rubyn_code/mcp/tool_bridge.rb +164 -0
- data/lib/rubyn_code/memory/models.rb +62 -0
- data/lib/rubyn_code/memory/search.rb +181 -0
- data/lib/rubyn_code/memory/session_persistence.rb +194 -0
- data/lib/rubyn_code/memory/store.rb +199 -0
- data/lib/rubyn_code/observability/budget_enforcer.rb +159 -0
- data/lib/rubyn_code/observability/cost_calculator.rb +61 -0
- data/lib/rubyn_code/observability/models.rb +29 -0
- data/lib/rubyn_code/observability/token_counter.rb +42 -0
- data/lib/rubyn_code/observability/usage_reporter.rb +140 -0
- data/lib/rubyn_code/output/diff_renderer.rb +212 -0
- data/lib/rubyn_code/output/formatter.rb +120 -0
- data/lib/rubyn_code/permissions/deny_list.rb +49 -0
- data/lib/rubyn_code/permissions/policy.rb +59 -0
- data/lib/rubyn_code/permissions/prompter.rb +80 -0
- data/lib/rubyn_code/permissions/tier.rb +22 -0
- data/lib/rubyn_code/protocols/interrupt_handler.rb +95 -0
- data/lib/rubyn_code/protocols/plan_approval.rb +67 -0
- data/lib/rubyn_code/protocols/shutdown_handshake.rb +109 -0
- data/lib/rubyn_code/skills/catalog.rb +70 -0
- data/lib/rubyn_code/skills/document.rb +80 -0
- data/lib/rubyn_code/skills/loader.rb +57 -0
- data/lib/rubyn_code/sub_agents/runner.rb +168 -0
- data/lib/rubyn_code/sub_agents/summarizer.rb +57 -0
- data/lib/rubyn_code/tasks/dag.rb +208 -0
- data/lib/rubyn_code/tasks/manager.rb +212 -0
- data/lib/rubyn_code/tasks/models.rb +31 -0
- data/lib/rubyn_code/teams/mailbox.rb +128 -0
- data/lib/rubyn_code/teams/manager.rb +175 -0
- data/lib/rubyn_code/teams/teammate.rb +38 -0
- data/lib/rubyn_code/tools/background_run.rb +41 -0
- data/lib/rubyn_code/tools/base.rb +84 -0
- data/lib/rubyn_code/tools/bash.rb +81 -0
- data/lib/rubyn_code/tools/bundle_add.rb +53 -0
- data/lib/rubyn_code/tools/bundle_install.rb +41 -0
- data/lib/rubyn_code/tools/compact.rb +57 -0
- data/lib/rubyn_code/tools/db_migrate.rb +52 -0
- data/lib/rubyn_code/tools/edit_file.rb +49 -0
- data/lib/rubyn_code/tools/executor.rb +62 -0
- data/lib/rubyn_code/tools/git_commit.rb +97 -0
- data/lib/rubyn_code/tools/git_diff.rb +61 -0
- data/lib/rubyn_code/tools/git_log.rb +59 -0
- data/lib/rubyn_code/tools/git_status.rb +59 -0
- data/lib/rubyn_code/tools/glob.rb +44 -0
- data/lib/rubyn_code/tools/grep.rb +81 -0
- data/lib/rubyn_code/tools/load_skill.rb +41 -0
- data/lib/rubyn_code/tools/memory_search.rb +77 -0
- data/lib/rubyn_code/tools/memory_write.rb +52 -0
- data/lib/rubyn_code/tools/rails_generate.rb +54 -0
- data/lib/rubyn_code/tools/read_file.rb +38 -0
- data/lib/rubyn_code/tools/read_inbox.rb +64 -0
- data/lib/rubyn_code/tools/registry.rb +48 -0
- data/lib/rubyn_code/tools/review_pr.rb +145 -0
- data/lib/rubyn_code/tools/run_specs.rb +75 -0
- data/lib/rubyn_code/tools/schema.rb +59 -0
- data/lib/rubyn_code/tools/send_message.rb +53 -0
- data/lib/rubyn_code/tools/spawn_agent.rb +154 -0
- data/lib/rubyn_code/tools/spawn_teammate.rb +168 -0
- data/lib/rubyn_code/tools/task.rb +148 -0
- data/lib/rubyn_code/tools/web_fetch.rb +108 -0
- data/lib/rubyn_code/tools/web_search.rb +196 -0
- data/lib/rubyn_code/tools/write_file.rb +30 -0
- data/lib/rubyn_code/version.rb +5 -0
- data/lib/rubyn_code.rb +203 -0
- data/skills/code_quality/fits_in_your_head.md +189 -0
- data/skills/code_quality/naming_conventions.md +213 -0
- data/skills/code_quality/null_object.md +205 -0
- data/skills/code_quality/technical_debt.md +135 -0
- data/skills/code_quality/value_objects.md +216 -0
- data/skills/code_quality/yagni.md +176 -0
- data/skills/design_patterns/adapter.md +191 -0
- data/skills/design_patterns/bridge_memento_visitor.md +254 -0
- data/skills/design_patterns/builder.md +158 -0
- data/skills/design_patterns/command.md +126 -0
- data/skills/design_patterns/composite.md +147 -0
- data/skills/design_patterns/decorator.md +204 -0
- data/skills/design_patterns/facade.md +133 -0
- data/skills/design_patterns/factory_method.md +169 -0
- data/skills/design_patterns/iterator.md +116 -0
- data/skills/design_patterns/mediator.md +133 -0
- data/skills/design_patterns/observer.md +177 -0
- data/skills/design_patterns/proxy.md +140 -0
- data/skills/design_patterns/singleton.md +124 -0
- data/skills/design_patterns/state.md +207 -0
- data/skills/design_patterns/strategy.md +127 -0
- data/skills/design_patterns/template_method.md +173 -0
- data/skills/gems/devise.md +365 -0
- data/skills/gems/dry_rb.md +186 -0
- data/skills/gems/factory_bot.md +268 -0
- data/skills/gems/faraday.md +263 -0
- data/skills/gems/graphql_ruby.md +514 -0
- data/skills/gems/pundit.md +446 -0
- data/skills/gems/redis.md +219 -0
- data/skills/gems/rubocop.md +257 -0
- data/skills/gems/sidekiq.md +360 -0
- data/skills/gems/stripe.md +224 -0
- data/skills/minitest/assertions.md +185 -0
- data/skills/minitest/fixtures.md +238 -0
- data/skills/minitest/integration_tests.md +210 -0
- data/skills/minitest/mailers_and_jobs.md +218 -0
- data/skills/minitest/mocking_stubbing.md +202 -0
- data/skills/minitest/service_tests_and_performance.md +246 -0
- data/skills/minitest/structure_and_conventions.md +169 -0
- data/skills/minitest/system_tests.md +237 -0
- data/skills/rails/action_cable.md +160 -0
- data/skills/rails/active_record_basics.md +174 -0
- data/skills/rails/active_storage.md +242 -0
- data/skills/rails/api_design.md +212 -0
- data/skills/rails/associations.md +182 -0
- data/skills/rails/background_jobs.md +212 -0
- data/skills/rails/caching.md +158 -0
- data/skills/rails/callbacks.md +135 -0
- data/skills/rails/concerns_controllers.md +218 -0
- data/skills/rails/concerns_models.md +280 -0
- data/skills/rails/controllers.md +190 -0
- data/skills/rails/engines.md +201 -0
- data/skills/rails/form_objects.md +168 -0
- data/skills/rails/hotwire.md +229 -0
- data/skills/rails/internationalization.md +192 -0
- data/skills/rails/logging.md +198 -0
- data/skills/rails/mailers.md +180 -0
- data/skills/rails/migrations.md +200 -0
- data/skills/rails/multitenancy.md +207 -0
- data/skills/rails/n_plus_one.md +151 -0
- data/skills/rails/presenters.md +244 -0
- data/skills/rails/query_objects.md +177 -0
- data/skills/rails/routing.md +194 -0
- data/skills/rails/scopes.md +187 -0
- data/skills/rails/security.md +233 -0
- data/skills/rails/serializers.md +243 -0
- data/skills/rails/service_objects.md +184 -0
- data/skills/rails/testing_strategy.md +258 -0
- data/skills/rails/validations.md +206 -0
- data/skills/refactoring/code_smells.md +251 -0
- data/skills/refactoring/command_query_separation.md +166 -0
- data/skills/refactoring/encapsulate_collection.md +125 -0
- data/skills/refactoring/extract_class.md +138 -0
- data/skills/refactoring/extract_method.md +185 -0
- data/skills/refactoring/replace_conditional.md +211 -0
- data/skills/refactoring/value_objects.md +246 -0
- data/skills/rspec/build_stubbed.md +199 -0
- data/skills/rspec/factory_design.md +206 -0
- data/skills/rspec/let_vs_let_bang.md +161 -0
- data/skills/rspec/mocking_stubbing.md +209 -0
- data/skills/rspec/request_specs.md +212 -0
- data/skills/rspec/service_specs.md +262 -0
- data/skills/rspec/shared_examples.md +244 -0
- data/skills/rspec/system_specs.md +286 -0
- data/skills/rspec/test_performance.md +215 -0
- data/skills/ruby/blocks_procs_lambdas.md +204 -0
- data/skills/ruby/classes.md +155 -0
- data/skills/ruby/concurrency.md +194 -0
- data/skills/ruby/data_struct_openstruct.md +158 -0
- data/skills/ruby/debugging_profiling.md +204 -0
- data/skills/ruby/enumerable_patterns.md +168 -0
- data/skills/ruby/exception_handling.md +199 -0
- data/skills/ruby/file_io.md +217 -0
- data/skills/ruby/hashes.md +195 -0
- data/skills/ruby/metaprogramming.md +170 -0
- data/skills/ruby/modules.md +210 -0
- data/skills/ruby/pattern_matching.md +177 -0
- data/skills/ruby/regular_expressions.md +166 -0
- data/skills/ruby/result_objects.md +200 -0
- data/skills/ruby/strings.md +177 -0
- data/skills/ruby_project/bundler_dependencies.md +181 -0
- data/skills/ruby_project/cli_tools.md +224 -0
- data/skills/ruby_project/rake_tasks.md +146 -0
- data/skills/ruby_project/structure.md +261 -0
- data/skills/sinatra/application_structure.md +241 -0
- data/skills/sinatra/middleware_and_deployment.md +221 -0
- data/skills/sinatra/testing.md +233 -0
- data/skills/solid/dependency_inversion.md +195 -0
- data/skills/solid/interface_segregation.md +237 -0
- data/skills/solid/liskov_substitution.md +263 -0
- data/skills/solid/open_closed.md +212 -0
- data/skills/solid/single_responsibility.md +183 -0
- metadata +397 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# Rails: Query Objects
|
|
2
|
+
|
|
3
|
+
## Pattern
|
|
4
|
+
|
|
5
|
+
Extract complex database queries into query objects when a scope chain becomes long, requires conditional logic, or involves joins and subqueries that obscure intent. Query objects live in `app/queries/` and return ActiveRecord relations so they remain chainable.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# app/queries/orders/search_query.rb
|
|
9
|
+
module Orders
|
|
10
|
+
class SearchQuery
|
|
11
|
+
def self.call(params)
|
|
12
|
+
new(params).call
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(params)
|
|
16
|
+
@params = params
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def call
|
|
20
|
+
scope = Order.includes(:user, :line_items)
|
|
21
|
+
scope = filter_by_status(scope)
|
|
22
|
+
scope = filter_by_date_range(scope)
|
|
23
|
+
scope = filter_by_total(scope)
|
|
24
|
+
scope = search_by_keyword(scope)
|
|
25
|
+
scope = apply_sorting(scope)
|
|
26
|
+
scope
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def filter_by_status(scope)
|
|
32
|
+
return scope unless @params[:status].present?
|
|
33
|
+
|
|
34
|
+
scope.where(status: @params[:status])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def filter_by_date_range(scope)
|
|
38
|
+
scope = scope.where(created_at: @params[:from]..) if @params[:from].present?
|
|
39
|
+
scope = scope.where(created_at: ..@params[:to]) if @params[:to].present?
|
|
40
|
+
scope
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def filter_by_total(scope)
|
|
44
|
+
scope = scope.where("total >= ?", @params[:min_total]) if @params[:min_total].present?
|
|
45
|
+
scope = scope.where("total <= ?", @params[:max_total]) if @params[:max_total].present?
|
|
46
|
+
scope
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def search_by_keyword(scope)
|
|
50
|
+
return scope unless @params[:q].present?
|
|
51
|
+
|
|
52
|
+
scope.where("orders.reference ILIKE :q OR users.email ILIKE :q", q: "%#{@params[:q]}%")
|
|
53
|
+
.references(:user)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def apply_sorting(scope)
|
|
57
|
+
case @params[:sort]
|
|
58
|
+
when "newest" then scope.order(created_at: :desc)
|
|
59
|
+
when "oldest" then scope.order(created_at: :asc)
|
|
60
|
+
when "highest" then scope.order(total: :desc)
|
|
61
|
+
else scope.order(created_at: :desc)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The controller stays minimal:
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
class OrdersController < ApplicationController
|
|
72
|
+
def index
|
|
73
|
+
@orders = Orders::SearchQuery.call(search_params).page(params[:page])
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def search_params
|
|
79
|
+
params.permit(:status, :from, :to, :min_total, :max_total, :q, :sort)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Why This Is Good
|
|
85
|
+
|
|
86
|
+
- **Returns a relation.** The query object returns an ActiveRecord::Relation, not an array. You can chain `.page()`, `.limit()`, `.count()` on the result. It composes with the rest of Rails.
|
|
87
|
+
- **Each filter is isolated.** Adding a new filter means adding one private method. Removing a filter means removing one method. No risk of breaking other filters.
|
|
88
|
+
- **Testable without HTTP.** Pass in a params hash, assert the SQL or the returned records. Fast, focused tests.
|
|
89
|
+
- **Reusable.** The same query object works in the controller, in an API endpoint, in a CSV export job, and in an admin panel.
|
|
90
|
+
- **Readable intent.** `Orders::SearchQuery.call(params)` communicates what's happening. A 30-line scope chain in a controller does not.
|
|
91
|
+
|
|
92
|
+
## Anti-Pattern
|
|
93
|
+
|
|
94
|
+
Building complex queries inline in the controller with conditional scope chaining:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
class OrdersController < ApplicationController
|
|
98
|
+
def index
|
|
99
|
+
@orders = Order.includes(:user, :line_items)
|
|
100
|
+
|
|
101
|
+
if params[:status].present?
|
|
102
|
+
@orders = @orders.where(status: params[:status])
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
if params[:from].present?
|
|
106
|
+
@orders = @orders.where("created_at >= ?", params[:from])
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
if params[:to].present?
|
|
110
|
+
@orders = @orders.where("created_at <= ?", params[:to])
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
if params[:min_total].present?
|
|
114
|
+
@orders = @orders.where("total >= ?", params[:min_total])
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
if params[:q].present?
|
|
118
|
+
@orders = @orders.joins(:user)
|
|
119
|
+
.where("orders.reference ILIKE :q OR users.email ILIKE :q", q: "%#{params[:q]}%")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
@orders = case params[:sort]
|
|
123
|
+
when "newest" then @orders.order(created_at: :desc)
|
|
124
|
+
when "oldest" then @orders.order(created_at: :asc)
|
|
125
|
+
when "highest" then @orders.order(total: :desc)
|
|
126
|
+
else @orders.order(created_at: :desc)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
@orders = @orders.page(params[:page])
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Why This Is Bad
|
|
135
|
+
|
|
136
|
+
- **30+ lines of query logic in a controller.** The controller's job is HTTP handling, not query construction.
|
|
137
|
+
- **Impossible to reuse.** When the admin panel needs the same search, you copy-paste the entire block. When the API needs it, you copy it again. When the logic changes, you update it in three places.
|
|
138
|
+
- **Difficult to test.** Testing this requires making HTTP requests and asserting HTML or JSON output. You can't test the query logic in isolation.
|
|
139
|
+
- **Grows unbounded.** Every new filter adds another `if` block. Every new sort option adds a `when` clause. The controller action becomes the longest method in the codebase.
|
|
140
|
+
|
|
141
|
+
## When To Apply
|
|
142
|
+
|
|
143
|
+
Use a query object when ANY of these are true:
|
|
144
|
+
|
|
145
|
+
- A query has **3 or more conditional filters** (status, date range, keyword, price range)
|
|
146
|
+
- The query involves **joins, subqueries, or raw SQL fragments** that obscure what's being queried
|
|
147
|
+
- The **same query logic is needed in multiple places** (web controller, API, admin, background job, export)
|
|
148
|
+
- The query is used for **reporting or analytics** (monthly revenue, user activity, conversion funnels)
|
|
149
|
+
- A model's scope chain is getting **longer than 3 chained scopes** for a single use case
|
|
150
|
+
|
|
151
|
+
## When NOT To Apply
|
|
152
|
+
|
|
153
|
+
- **Simple, reusable filters belong as scopes on the model.** `Order.recent`, `Order.pending`, `Order.for_user(user)` are fine as scopes. They're short, reusable, and chainable.
|
|
154
|
+
- **Single-condition queries don't need a class.** `Order.where(status: :pending)` in a controller is perfectly fine. Don't extract a query object for one `where` clause.
|
|
155
|
+
- The query is **only used in one place** and is **under 5 lines.** A small inline query in a controller is more readable than navigating to a separate file.
|
|
156
|
+
|
|
157
|
+
## Edge Cases
|
|
158
|
+
|
|
159
|
+
**Some filters should always be applied (like tenant scoping):**
|
|
160
|
+
Apply those in the constructor or at the top of `call`, not as conditional filters. Tenant scoping is not optional.
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
def call
|
|
164
|
+
scope = Order.where(company: @company) # Always applied
|
|
165
|
+
scope = filter_by_status(scope) # Conditionally applied
|
|
166
|
+
scope
|
|
167
|
+
end
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**The query needs to return raw data (not ActiveRecord objects):**
|
|
171
|
+
Use `.pluck`, `.select`, or `.to_sql` at the call site, not inside the query object. The query object returns a relation; the caller decides how to materialize it.
|
|
172
|
+
|
|
173
|
+
**You need both a count and the results:**
|
|
174
|
+
Return the relation. The caller chains `.count` or `.to_a` as needed. Don't build two methods that run nearly identical queries.
|
|
175
|
+
|
|
176
|
+
**The query is extremely complex (CTEs, window functions):**
|
|
177
|
+
Consider `Arel` for type-safe query construction, or use `.from(Arel.sql(...))` for raw SQL. Wrap it in the query object so the complexity is contained in one place. Add comments explaining the SQL.
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Rails: Routing
|
|
2
|
+
|
|
3
|
+
## Pattern
|
|
4
|
+
|
|
5
|
+
Routes define your application's public API surface. Keep them RESTful, use resources for CRUD, create new controllers instead of custom actions, and use namespaces to organize related endpoints.
|
|
6
|
+
|
|
7
|
+
### RESTful Resources
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# config/routes.rb
|
|
11
|
+
Rails.application.routes.draw do
|
|
12
|
+
# GOOD: Standard RESTful resources
|
|
13
|
+
resources :orders, only: [:index, :show, :create, :update, :destroy]
|
|
14
|
+
resources :products, only: [:index, :show]
|
|
15
|
+
|
|
16
|
+
# GOOD: Nested resources for parent-child relationships
|
|
17
|
+
resources :orders do
|
|
18
|
+
resources :line_items, only: [:create, :destroy]
|
|
19
|
+
resource :shipment, only: [:show, :create] # singular — one shipment per order
|
|
20
|
+
end
|
|
21
|
+
# Generates: /orders/:order_id/line_items
|
|
22
|
+
# /orders/:order_id/shipment
|
|
23
|
+
|
|
24
|
+
# GOOD: Shallow nesting — child resources get their own top-level routes for show/edit/destroy
|
|
25
|
+
resources :projects, shallow: true do
|
|
26
|
+
resources :memberships, only: [:index, :create, :show, :destroy]
|
|
27
|
+
end
|
|
28
|
+
# Generates: /projects/:project_id/memberships (index, create)
|
|
29
|
+
# /memberships/:id (show, destroy)
|
|
30
|
+
end
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### New Controllers Over Custom Actions
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
# BAD: Custom actions on a resource controller
|
|
37
|
+
resources :orders do
|
|
38
|
+
member do
|
|
39
|
+
post :cancel # POST /orders/:id/cancel
|
|
40
|
+
post :ship # POST /orders/:id/ship
|
|
41
|
+
post :refund # POST /orders/:id/refund
|
|
42
|
+
get :invoice # GET /orders/:id/invoice
|
|
43
|
+
get :tracking # GET /orders/:id/tracking
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# GOOD: Each verb gets its own resource controller
|
|
48
|
+
resources :orders, only: [:index, :show, :create, :update] do
|
|
49
|
+
resource :cancellation, only: [:create], controller: "order_cancellations"
|
|
50
|
+
resource :shipment, only: [:show, :create], controller: "order_shipments"
|
|
51
|
+
resource :refund, only: [:create], controller: "order_refunds"
|
|
52
|
+
resource :invoice, only: [:show], controller: "order_invoices"
|
|
53
|
+
resource :tracking, only: [:show], controller: "order_trackings"
|
|
54
|
+
end
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Each new controller has a single RESTful action. `OrderCancellationsController#create` is clearer than `OrdersController#cancel`, and each controller stays skinny.
|
|
58
|
+
|
|
59
|
+
### Namespaces, Scopes, and Modules
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
Rails.application.routes.draw do
|
|
63
|
+
# namespace: adds URL prefix AND module prefix
|
|
64
|
+
namespace :admin do
|
|
65
|
+
resources :users # Admin::UsersController, /admin/users
|
|
66
|
+
resources :orders # Admin::OrdersController, /admin/orders
|
|
67
|
+
root to: "dashboard#show"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# namespace for API versioning
|
|
71
|
+
namespace :api do
|
|
72
|
+
namespace :v1 do
|
|
73
|
+
resources :orders # Api::V1::OrdersController, /api/v1/orders
|
|
74
|
+
resources :projects do
|
|
75
|
+
resources :embeddings, only: [:index, :create]
|
|
76
|
+
end
|
|
77
|
+
namespace :ai do
|
|
78
|
+
post :refactor # Api::V1::Ai::RefactorController (if using Grape, mount instead)
|
|
79
|
+
post :review
|
|
80
|
+
post :spec
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# scope: adds URL prefix but NOT module prefix
|
|
86
|
+
scope "/dashboard" do
|
|
87
|
+
resources :analytics, only: [:index] # AnalyticsController, /dashboard/analytics
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# module: adds module prefix but NOT URL prefix
|
|
91
|
+
scope module: :public do
|
|
92
|
+
resources :products, only: [:index, :show] # Public::ProductsController, /products
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Constraints and Advanced Routing
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
Rails.application.routes.draw do
|
|
101
|
+
# Subdomain constraints
|
|
102
|
+
constraints subdomain: "api" do
|
|
103
|
+
namespace :api, path: "" do # api.rubyn.ai/v1/orders instead of api.rubyn.ai/api/v1/orders
|
|
104
|
+
namespace :v1 do
|
|
105
|
+
resources :orders
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Format constraints
|
|
111
|
+
resources :reports, only: [:show], defaults: { format: :json }
|
|
112
|
+
|
|
113
|
+
# Custom constraints
|
|
114
|
+
constraints ->(req) { req.env["HTTP_AUTHORIZATION"].present? } do
|
|
115
|
+
resources :admin_tools
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Catch-all for SPA (must be LAST)
|
|
119
|
+
get "*path", to: "application#frontend", constraints: ->(req) { !req.xhr? && req.format.html? }
|
|
120
|
+
end
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Route Helpers and Path Generation
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
# Use named routes — never hardcode paths
|
|
127
|
+
redirect_to order_path(@order) # /orders/123
|
|
128
|
+
redirect_to order_line_items_path(@order) # /orders/123/line_items
|
|
129
|
+
redirect_to [:admin, @user] # /admin/users/456
|
|
130
|
+
redirect_to new_order_path # /orders/new
|
|
131
|
+
|
|
132
|
+
# Polymorphic routing
|
|
133
|
+
redirect_to [@order, @line_item] # /orders/123/line_items/789
|
|
134
|
+
|
|
135
|
+
# URL helpers in non-controller contexts
|
|
136
|
+
Rails.application.routes.url_helpers.order_url(order, host: "rubyn.ai")
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Why This Is Good
|
|
140
|
+
|
|
141
|
+
- **RESTful resources are predictable.** Any Rails developer opening your routes file knows that `resources :orders` means 7 standard actions. Custom actions require reading each one.
|
|
142
|
+
- **New controllers keep actions skinny.** `OrderCancellationsController#create` has one job. `OrdersController#cancel` is a non-RESTful action hiding in a RESTful controller.
|
|
143
|
+
- **Namespaces organize by concern.** Admin routes, API routes, and public routes are clearly separated. Different authentication, different base controllers, different middleware.
|
|
144
|
+
- **Shallow nesting avoids deep URLs.** `/projects/1/memberships/2/permissions/3` is painful. Shallow nesting gives children their own top-level routes after creation.
|
|
145
|
+
- **`only:` keeps it explicit.** `resources :products, only: [:index, :show]` tells you exactly which endpoints exist. No guessing, no unused routes.
|
|
146
|
+
|
|
147
|
+
## Anti-Pattern
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
# BAD: Everything on one controller, no nesting discipline
|
|
151
|
+
resources :orders do
|
|
152
|
+
collection do
|
|
153
|
+
get :search
|
|
154
|
+
get :export
|
|
155
|
+
get :report
|
|
156
|
+
end
|
|
157
|
+
member do
|
|
158
|
+
post :cancel
|
|
159
|
+
post :ship
|
|
160
|
+
post :approve
|
|
161
|
+
post :reject
|
|
162
|
+
post :archive
|
|
163
|
+
post :duplicate
|
|
164
|
+
get :pdf
|
|
165
|
+
get :receipt
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
# OrdersController now has 15+ actions
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## When To Apply
|
|
172
|
+
|
|
173
|
+
- **Every Rails app.** RESTful routing is the Rails way. It's not optional.
|
|
174
|
+
- **When an action doesn't map to CRUD** — it's a new controller, not a custom action. "Cancel" is creating a cancellation. "Ship" is creating a shipment.
|
|
175
|
+
- **API versioning from day one.** `/api/v1/` costs nothing now and saves everything later.
|
|
176
|
+
- **`only:` on every `resources` call.** Don't generate routes you don't use.
|
|
177
|
+
|
|
178
|
+
## When NOT To Apply
|
|
179
|
+
|
|
180
|
+
- **Sinatra apps.** Sinatra routes are explicit — no `resources` macro. Just define the routes you need.
|
|
181
|
+
- **Single-action controllers don't need resources.** A health check is `get "/health", to: "health#show"`, not `resources :health`.
|
|
182
|
+
- **Don't over-nest.** Never go deeper than 2 levels. `/orders/:id/line_items` is fine. `/companies/:id/orders/:id/line_items/:id/adjustments` is too deep — use shallow nesting or flatten.
|
|
183
|
+
|
|
184
|
+
## Edge Cases
|
|
185
|
+
|
|
186
|
+
**Mounting engines and Rack apps:**
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
mount Rubyn::Engine => "/rubyn" if Rails.env.development?
|
|
190
|
+
mount Sidekiq::Web => "/sidekiq" if Rails.env.development?
|
|
191
|
+
mount ActionCable.server => "/cable"
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Route precedence:** Routes are matched top to bottom. Put specific routes before generic ones, and catch-all routes last.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Rails: ActiveRecord Scopes
|
|
2
|
+
|
|
3
|
+
## Pattern
|
|
4
|
+
|
|
5
|
+
Scopes are named, reusable query fragments that return `ActiveRecord::Relation`. They chain, compose, and serve as the vocabulary for querying your domain. Design scopes like building blocks — small, focused, and combinable.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
class Order < ApplicationRecord
|
|
9
|
+
# Status scopes — named after the state
|
|
10
|
+
scope :pending, -> { where(status: :pending) }
|
|
11
|
+
scope :confirmed, -> { where(status: :confirmed) }
|
|
12
|
+
scope :shipped, -> { where(status: :shipped) }
|
|
13
|
+
scope :completed, -> { where(status: %i[shipped delivered]) }
|
|
14
|
+
scope :active, -> { where.not(status: :cancelled) }
|
|
15
|
+
|
|
16
|
+
# Time scopes — named after the time frame
|
|
17
|
+
scope :recent, -> { where(created_at: 30.days.ago..) }
|
|
18
|
+
scope :today, -> { where(created_at: Date.current.all_day) }
|
|
19
|
+
scope :this_month, -> { where(created_at: Date.current.all_month) }
|
|
20
|
+
scope :before, ->(date) { where(created_at: ...date) }
|
|
21
|
+
scope :after, ->(date) { where(created_at: date..) }
|
|
22
|
+
scope :between, ->(start_date, end_date) { where(created_at: start_date..end_date) }
|
|
23
|
+
|
|
24
|
+
# Relationship scopes — named after the association
|
|
25
|
+
scope :for_user, ->(user) { where(user: user) }
|
|
26
|
+
scope :for_product, ->(product) { joins(:line_items).where(line_items: { product: product }) }
|
|
27
|
+
|
|
28
|
+
# Value scopes — named after what they filter
|
|
29
|
+
scope :high_value, -> { where("total >= ?", 200_00) }
|
|
30
|
+
scope :above, ->(amount) { where("total >= ?", amount) }
|
|
31
|
+
scope :free_shipping, -> { where("total >= ?", 50_00) }
|
|
32
|
+
|
|
33
|
+
# Ordering scopes
|
|
34
|
+
scope :by_newest, -> { order(created_at: :desc) }
|
|
35
|
+
scope :by_total, -> { order(total: :desc) }
|
|
36
|
+
scope :by_status, -> { order(:status) }
|
|
37
|
+
|
|
38
|
+
# Inclusion scopes — preload associations for performance
|
|
39
|
+
scope :with_details, -> { includes(:user, :line_items, line_items: :product) }
|
|
40
|
+
scope :with_user, -> { includes(:user) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Compose scopes naturally — reads like a sentence
|
|
44
|
+
Order.for_user(current_user).pending.recent.by_newest
|
|
45
|
+
Order.confirmed.high_value.with_details.by_total
|
|
46
|
+
Order.active.this_month.for_product(widget)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Scopes with Conditional Logic
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
class Product < ApplicationRecord
|
|
53
|
+
# Parameterized scope — nil-safe
|
|
54
|
+
scope :in_category, ->(category) { where(category: category) if category.present? }
|
|
55
|
+
scope :cheaper_than, ->(price) { where("price <= ?", price) if price.present? }
|
|
56
|
+
scope :search, ->(query) {
|
|
57
|
+
where("name ILIKE :q OR sku ILIKE :q", q: "%#{sanitize_sql_like(query)}%") if query.present?
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# Scope that wraps a subquery
|
|
61
|
+
scope :with_recent_orders, -> {
|
|
62
|
+
where(id: LineItem.joins(:order).where(orders: { created_at: 30.days.ago.. }).select(:product_id))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Scope using merge to combine conditions from another model's scope
|
|
66
|
+
scope :ordered_recently, -> {
|
|
67
|
+
joins(:line_items).merge(LineItem.joins(:order).merge(Order.recent)).distinct
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Nil-safe scopes chain gracefully — nil params are ignored
|
|
72
|
+
Product.in_category(params[:category]).cheaper_than(params[:max_price]).search(params[:q])
|
|
73
|
+
# If params[:category] is nil, that scope returns `all` — the chain continues
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Scopes vs Class Methods
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
class Order < ApplicationRecord
|
|
80
|
+
# SCOPE: Always returns a relation, even when the condition is nil
|
|
81
|
+
scope :by_status, ->(status) { where(status: status) if status.present? }
|
|
82
|
+
# When status is nil: returns `all` (chainable)
|
|
83
|
+
|
|
84
|
+
# CLASS METHOD: Can return nil, breaking the chain
|
|
85
|
+
def self.by_status(status)
|
|
86
|
+
return if status.blank? # Returns nil — .by_newest chained after this explodes
|
|
87
|
+
where(status: status)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# FIX: Class method that always returns a relation
|
|
91
|
+
def self.by_status(status)
|
|
92
|
+
return all if status.blank? # Returns scope, not nil
|
|
93
|
+
where(status: status)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Rule:** Use scopes for simple query fragments. Use class methods when you need complex logic (multiple lines, early returns, error handling) — but always return a relation or `all`/`none`, never `nil`.
|
|
99
|
+
|
|
100
|
+
## Why This Is Good
|
|
101
|
+
|
|
102
|
+
- **Composable.** Each scope is a LEGO brick. Snap them together in any combination. `Order.pending.recent.high_value` generates one SQL query with three WHERE clauses.
|
|
103
|
+
- **Readable.** `Order.for_user(user).completed.this_month` reads like English. The equivalent raw SQL is harder to scan and impossible to reuse.
|
|
104
|
+
- **Chainable.** Scopes return `ActiveRecord::Relation`, so you can always chain more scopes, `.count`, `.page()`, `.pluck()`, `.exists?` after them.
|
|
105
|
+
- **Nil-safe.** A scope with `if condition.present?` returns `all` when the condition is false — the chain continues without breaking. This makes conditional filtering trivial.
|
|
106
|
+
- **Single source of truth.** "What does 'recent' mean?" is answered in one place — the scope definition. Not scattered across 8 controllers with slightly different `where` clauses.
|
|
107
|
+
- **Preloadable.** Scopes work with `includes`, `preload`, and `eager_load`. Query objects that return arrays don't.
|
|
108
|
+
|
|
109
|
+
## Anti-Pattern
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
class Order < ApplicationRecord
|
|
113
|
+
# BAD: default_scope — poisons every query
|
|
114
|
+
default_scope { where(deleted: false) }
|
|
115
|
+
# Every Order.find, Order.count, Order.joins silently adds WHERE deleted = false
|
|
116
|
+
# Forgetting to unscope causes subtle bugs
|
|
117
|
+
|
|
118
|
+
# BAD: Scope that returns an array, not a relation
|
|
119
|
+
scope :totals, -> { pluck(:total) }
|
|
120
|
+
# Can't chain: Order.totals.pending → NoMethodError
|
|
121
|
+
|
|
122
|
+
# BAD: Scope with side effects
|
|
123
|
+
scope :expire_old, -> {
|
|
124
|
+
where(created_at: ...30.days.ago).update_all(status: :expired)
|
|
125
|
+
}
|
|
126
|
+
# Scopes should query, not mutate. This belongs in a service object.
|
|
127
|
+
|
|
128
|
+
# BAD: Overly complex scope that should be a query object
|
|
129
|
+
scope :dashboard_summary, -> {
|
|
130
|
+
select("status, COUNT(*) as count, SUM(total) as revenue")
|
|
131
|
+
.where(created_at: 30.days.ago..)
|
|
132
|
+
.where.not(status: :cancelled)
|
|
133
|
+
.group(:status)
|
|
134
|
+
.having("COUNT(*) > ?", 0)
|
|
135
|
+
.order("revenue DESC")
|
|
136
|
+
}
|
|
137
|
+
end
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## When To Apply
|
|
141
|
+
|
|
142
|
+
- **Every reusable query condition** that's used in 2+ places. If two controllers filter by `pending`, that's a scope.
|
|
143
|
+
- **Parameterized filters.** `scope :for_user, ->(user)` is cleaner than `where(user: user)` repeated everywhere.
|
|
144
|
+
- **Ordering.** `scope :by_newest` is more expressive than `.order(created_at: :desc)` in every controller.
|
|
145
|
+
- **Eager loading bundles.** `scope :with_details` bundles the `includes` for a specific use case.
|
|
146
|
+
|
|
147
|
+
## When NOT To Apply
|
|
148
|
+
|
|
149
|
+
- **Complex queries with 4+ joins, subqueries, or CTEs.** These belong in a Query Object, not a scope.
|
|
150
|
+
- **Queries with side effects.** Scopes should never `update_all`, send emails, or modify state. They read data.
|
|
151
|
+
- **One-off queries.** If a query is only used in one place and is simple (one `where` clause), inline it. Don't create a scope for everything.
|
|
152
|
+
- **Never use `default_scope`.** It silently affects every query on the model. Use explicit scopes and apply them where needed.
|
|
153
|
+
|
|
154
|
+
## Edge Cases
|
|
155
|
+
|
|
156
|
+
**Merging scopes across models:**
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
# merge applies another model's scope in a join
|
|
160
|
+
Order.joins(:user).merge(User.active)
|
|
161
|
+
# WHERE users.active = true
|
|
162
|
+
|
|
163
|
+
# Useful for combining scopes from both sides of a join
|
|
164
|
+
Order.confirmed.joins(:user).merge(User.active).merge(User.pro_plan)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Scopes on associations:**
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
class User < ApplicationRecord
|
|
171
|
+
has_many :orders
|
|
172
|
+
has_many :pending_orders, -> { pending }, class_name: "Order"
|
|
173
|
+
has_many :recent_orders, -> { recent.by_newest }, class_name: "Order"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
user.pending_orders # Preloadable scoped association
|
|
177
|
+
User.includes(:pending_orders) # Works with includes
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**`none` scope for empty results:**
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
def orders_for(user)
|
|
184
|
+
return Order.none unless user&.active? # Returns empty relation, still chainable
|
|
185
|
+
user.orders.active
|
|
186
|
+
end
|
|
187
|
+
```
|