source_monitor 0.2.0 → 0.3.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 +4 -4
- data/.claude/agents/rails-concern.md +464 -0
- data/.claude/agents/rails-controller.md +424 -0
- data/.claude/agents/rails-hotwire.md +446 -0
- data/.claude/agents/rails-implement.md +374 -0
- data/.claude/agents/rails-job.md +334 -0
- data/.claude/agents/rails-lint.md +294 -0
- data/.claude/agents/rails-mailer.md +371 -0
- data/.claude/agents/rails-migration.md +449 -0
- data/.claude/agents/rails-model.md +420 -0
- data/.claude/agents/rails-policy.md +443 -0
- data/.claude/agents/rails-presenter.md +427 -0
- data/.claude/agents/rails-query.md +412 -0
- data/.claude/agents/rails-review.md +490 -0
- data/.claude/agents/rails-service.md +458 -0
- data/.claude/agents/rails-state-records.md +465 -0
- data/.claude/agents/rails-tdd.md +314 -0
- data/.claude/agents/rails-test.md +441 -0
- data/.claude/agents/rails-view-component.md +418 -0
- data/.claude/hooks/block-secrets.sh +52 -0
- data/.claude/settings.json +85 -0
- data/.claude/skills/action-cable-patterns/SKILL.md +296 -0
- data/.claude/skills/action-mailer-patterns/SKILL.md +295 -0
- data/.claude/skills/active-storage-setup/SKILL.md +311 -0
- data/.claude/skills/api-versioning/SKILL.md +294 -0
- data/.claude/skills/authentication-flow/SKILL.md +335 -0
- data/.claude/skills/authentication-flow/reference/current.md +248 -0
- data/.claude/skills/authentication-flow/reference/passwordless.md +253 -0
- data/.claude/skills/authentication-flow/reference/sessions.md +201 -0
- data/.claude/skills/authorization-pundit/SKILL.md +462 -0
- data/.claude/skills/caching-strategies/SKILL.md +350 -0
- data/.claude/skills/database-migrations/SKILL.md +354 -0
- data/.claude/skills/form-object-patterns/SKILL.md +399 -0
- data/.claude/skills/hotwire-patterns/SKILL.md +247 -0
- data/.claude/skills/hotwire-patterns/reference/stimulus.md +307 -0
- data/.claude/skills/hotwire-patterns/reference/tailwind-integration.md +112 -0
- data/.claude/skills/hotwire-patterns/reference/turbo-frames.md +158 -0
- data/.claude/skills/hotwire-patterns/reference/turbo-streams.md +218 -0
- data/.claude/skills/i18n-patterns/SKILL.md +320 -0
- data/.claude/skills/install/SKILL.md +367 -0
- data/.claude/skills/performance-optimization/SKILL.md +311 -0
- data/.claude/skills/rails-architecture/SKILL.md +259 -0
- data/.claude/skills/rails-architecture/reference/error-handling.md +333 -0
- data/.claude/skills/rails-architecture/reference/event-tracking.md +142 -0
- data/.claude/skills/rails-architecture/reference/layer-interactions.md +417 -0
- data/.claude/skills/rails-architecture/reference/multi-tenancy.md +152 -0
- data/.claude/skills/rails-architecture/reference/query-patterns.md +342 -0
- data/.claude/skills/rails-architecture/reference/service-patterns.md +286 -0
- data/.claude/skills/rails-architecture/reference/state-records.md +250 -0
- data/.claude/skills/rails-architecture/reference/testing-strategy.md +326 -0
- data/.claude/skills/rails-concern/SKILL.md +399 -0
- data/.claude/skills/rails-controller/SKILL.md +336 -0
- data/.claude/skills/rails-model-generator/SKILL.md +321 -0
- data/.claude/skills/rails-model-generator/reference/validations.md +298 -0
- data/.claude/skills/rails-presenter/SKILL.md +274 -0
- data/.claude/skills/rails-query-object/SKILL.md +289 -0
- data/.claude/skills/rails-service-object/SKILL.md +349 -0
- data/.claude/skills/solid-queue-setup/SKILL.md +307 -0
- data/.claude/skills/tdd-cycle/SKILL.md +359 -0
- data/.claude/skills/viewcomponent-patterns/SKILL.md +333 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +2 -0
- data/.ruby-version +1 -1
- data/.vbw-planning/.notification-log.jsonl +192 -0
- data/.vbw-planning/.session-log.jsonl +871 -0
- data/.vbw-planning/PROJECT.md +51 -0
- data/.vbw-planning/REQUIREMENTS.md +50 -0
- data/.vbw-planning/SHIPPED.md +28 -0
- data/.vbw-planning/codebase/ARCHITECTURE.md +147 -0
- data/.vbw-planning/codebase/CONCERNS.md +99 -0
- data/.vbw-planning/codebase/CONVENTIONS.md +97 -0
- data/.vbw-planning/codebase/DEPENDENCIES.md +100 -0
- data/.vbw-planning/codebase/INDEX.md +86 -0
- data/.vbw-planning/codebase/META.md +42 -0
- data/.vbw-planning/codebase/PATTERNS.md +262 -0
- data/.vbw-planning/codebase/STACK.md +101 -0
- data/.vbw-planning/codebase/STRUCTURE.md +324 -0
- data/.vbw-planning/codebase/TESTING.md +154 -0
- data/.vbw-planning/config.json +12 -0
- data/.vbw-planning/discovery.json +24 -0
- data/.vbw-planning/milestones/default/ROADMAP.md +115 -0
- data/.vbw-planning/milestones/default/STATE.md +83 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01-SUMMARY.md +56 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01.md +187 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02-SUMMARY.md +64 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02.md +137 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01-SUMMARY.md +67 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01.md +142 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02-SUMMARY.md +64 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02.md +138 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03-SUMMARY.md +85 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03.md +147 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04-SUMMARY.md +63 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04.md +129 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05-SUMMARY.md +74 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05.md +154 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION-wave1.md +303 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION.md +510 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01-SUMMARY.md +61 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01.md +161 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02-SUMMARY.md +66 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02.md +132 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03-SUMMARY.md +59 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03.md +171 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04-SUMMARY.md +56 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04.md +152 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/04-CONTEXT.md +33 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01-SUMMARY.md +42 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01.md +119 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02-SUMMARY.md +52 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02.md +195 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03-SUMMARY.md +79 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03.md +130 -0
- data/CHANGELOG.md +28 -0
- data/CLAUDE.md +179 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +114 -101
- data/Rakefile +2 -0
- data/app/assets/builds/source_monitor/application.css +2076 -0
- data/app/assets/builds/source_monitor/application.js +2758 -0
- data/app/assets/builds/source_monitor/application.js.map +7 -0
- data/app/controllers/source_monitor/application_controller.rb +2 -0
- data/app/controllers/source_monitor/health_controller.rb +2 -0
- data/app/controllers/source_monitor/import_sessions/bulk_configuration.rb +106 -0
- data/app/controllers/source_monitor/import_sessions/entry_annotation.rb +187 -0
- data/app/controllers/source_monitor/import_sessions/health_check_management.rb +112 -0
- data/app/controllers/source_monitor/import_sessions/opml_parser.rb +130 -0
- data/app/controllers/source_monitor/import_sessions_controller.rb +6 -507
- data/app/controllers/source_monitor/items_controller.rb +2 -0
- data/app/controllers/source_monitor/sources_controller.rb +0 -14
- data/app/helpers/source_monitor/application_helper.rb +4 -112
- data/app/helpers/source_monitor/health_badge_helper.rb +69 -0
- data/app/helpers/source_monitor/table_sort_helper.rb +53 -0
- data/app/jobs/source_monitor/application_job.rb +2 -0
- data/app/models/source_monitor/application_record.rb +2 -0
- data/app/models/source_monitor/log_entry.rb +0 -2
- data/config/coverage_baseline.json +217 -1862
- data/config/routes.rb +2 -0
- data/db/migrate/20251009103000_add_feed_content_readability_to_sources.rb +2 -0
- data/db/migrate/20251014171659_add_performance_indexes.rb +2 -0
- data/db/migrate/20251014172525_add_fetch_status_check_constraint.rb +2 -0
- data/db/migrate/20251108120116_refresh_fetch_status_constraint.rb +2 -0
- data/db/migrate/20260210204022_add_composite_index_to_log_entries.rb +17 -0
- data/lib/source_monitor/assets/bundler.rb +2 -0
- data/lib/source_monitor/assets.rb +2 -0
- data/lib/source_monitor/configuration/authentication_settings.rb +62 -0
- data/lib/source_monitor/configuration/events.rb +60 -0
- data/lib/source_monitor/configuration/fetching_settings.rb +27 -0
- data/lib/source_monitor/configuration/health_settings.rb +27 -0
- data/lib/source_monitor/configuration/http_settings.rb +43 -0
- data/lib/source_monitor/configuration/model_definition.rb +108 -0
- data/lib/source_monitor/configuration/models.rb +36 -0
- data/lib/source_monitor/configuration/realtime_settings.rb +95 -0
- data/lib/source_monitor/configuration/retention_settings.rb +45 -0
- data/lib/source_monitor/configuration/scraper_registry.rb +67 -0
- data/lib/source_monitor/configuration/scraping_settings.rb +39 -0
- data/lib/source_monitor/configuration/validation_definition.rb +32 -0
- data/lib/source_monitor/configuration.rb +12 -579
- data/lib/source_monitor/dashboard/queries/recent_activity_query.rb +138 -0
- data/lib/source_monitor/dashboard/queries/stats_query.rb +71 -0
- data/lib/source_monitor/dashboard/queries.rb +2 -195
- data/lib/source_monitor/engine.rb +2 -0
- data/lib/source_monitor/fetching/feed_fetcher/adaptive_interval.rb +141 -0
- data/lib/source_monitor/fetching/feed_fetcher/entry_processor.rb +89 -0
- data/lib/source_monitor/fetching/feed_fetcher/source_updater.rb +200 -0
- data/lib/source_monitor/fetching/feed_fetcher.rb +37 -379
- data/lib/source_monitor/items/item_creator/content_extractor.rb +113 -0
- data/lib/source_monitor/items/item_creator/entry_parser/media_extraction.rb +96 -0
- data/lib/source_monitor/items/item_creator/entry_parser.rb +294 -0
- data/lib/source_monitor/items/item_creator.rb +28 -455
- data/lib/source_monitor/setup/bundle_installer.rb +2 -0
- data/lib/source_monitor/setup/cli.rb +2 -0
- data/lib/source_monitor/setup/dependency_checker.rb +2 -0
- data/lib/source_monitor/setup/detectors.rb +2 -0
- data/lib/source_monitor/setup/gemfile_editor.rb +2 -0
- data/lib/source_monitor/setup/initializer_patcher.rb +2 -0
- data/lib/source_monitor/setup/install_generator.rb +2 -0
- data/lib/source_monitor/setup/migration_installer.rb +2 -0
- data/lib/source_monitor/setup/node_installer.rb +2 -0
- data/lib/source_monitor/setup/prompter.rb +2 -0
- data/lib/source_monitor/setup/requirements.rb +2 -0
- data/lib/source_monitor/setup/shell_runner.rb +2 -0
- data/lib/source_monitor/setup/verification/action_cable_verifier.rb +2 -0
- data/lib/source_monitor/setup/verification/printer.rb +2 -0
- data/lib/source_monitor/setup/verification/result.rb +2 -0
- data/lib/source_monitor/setup/verification/runner.rb +2 -0
- data/lib/source_monitor/setup/verification/solid_queue_verifier.rb +2 -0
- data/lib/source_monitor/setup/verification/telemetry_logger.rb +2 -0
- data/lib/source_monitor/setup/workflow.rb +2 -0
- data/lib/source_monitor/version.rb +3 -1
- data/lib/source_monitor.rb +140 -58
- data/lib/tasks/source_monitor_assets.rake +2 -0
- data/lib/tasks/source_monitor_setup.rake +2 -0
- data/lib/tasks/source_monitor_tasks.rake +2 -0
- data/source_monitor.gemspec +3 -1
- metadata +144 -4
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rails-query
|
|
3
|
+
description: Query objects for complex database queries beyond simple scopes
|
|
4
|
+
tools: Read, Write, Edit, Bash, Glob, Grep
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Rails Query Agent
|
|
8
|
+
|
|
9
|
+
You are an expert at building query objects that encapsulate complex database queries, keeping models clean and queries testable.
|
|
10
|
+
|
|
11
|
+
## Project Conventions
|
|
12
|
+
- **Testing:** Minitest + fixtures (NEVER RSpec or FactoryBot)
|
|
13
|
+
- **Components:** ViewComponents for reusable UI (partials OK for simple one-offs)
|
|
14
|
+
- **Authorization:** Pundit policies (deny by default)
|
|
15
|
+
- **Jobs:** Solid Queue, shallow jobs, `_later`/`_now` naming
|
|
16
|
+
- **Frontend:** Hotwire (Turbo + Stimulus) + Tailwind CSS
|
|
17
|
+
- **State:** State-as-records for business state (booleans only for technical flags)
|
|
18
|
+
- **Architecture:** Rich models first, service objects for multi-model orchestration
|
|
19
|
+
- **Routing:** Everything-is-CRUD (new resource over new action)
|
|
20
|
+
- **Quality:** RuboCop (omakase) + Brakeman
|
|
21
|
+
|
|
22
|
+
## When to Use Query Objects vs Scopes
|
|
23
|
+
|
|
24
|
+
| Use Scope | Use Query Object |
|
|
25
|
+
|-----------|-----------------|
|
|
26
|
+
| 1-2 conditions | 3+ conditions or joins |
|
|
27
|
+
| Single table | Multiple table joins |
|
|
28
|
+
| Reusable fragments | Page-specific complex query |
|
|
29
|
+
| Simple `where`/`order` | Aggregations, subqueries |
|
|
30
|
+
| Chainable building blocks | Complete query with parameters |
|
|
31
|
+
|
|
32
|
+
### Decision Guide
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
# SCOPE: Simple, reusable, chainable
|
|
36
|
+
scope :active, -> { where.missing(:closure) }
|
|
37
|
+
scope :recent, -> { order(created_at: :desc) }
|
|
38
|
+
scope :for_account, ->(account) { where(account: account) }
|
|
39
|
+
|
|
40
|
+
# QUERY OBJECT: Complex, multi-join, parameterized
|
|
41
|
+
# "Find overdue tasks with their project and assignee info,
|
|
42
|
+
# filtered by account, grouped by priority, for the dashboard"
|
|
43
|
+
Dashboard::OverdueTasksQuery.new(account: current_account).call
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Query Object Structure
|
|
47
|
+
|
|
48
|
+
### Base Query
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
# app/queries/application_query.rb
|
|
52
|
+
class ApplicationQuery
|
|
53
|
+
def self.call(...)
|
|
54
|
+
new(...).call
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def initialize(**args)
|
|
58
|
+
# Subclasses define their own initializers
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def call
|
|
62
|
+
raise NotImplementedError
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Standard Query Object
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
# app/queries/tasks/overdue_query.rb
|
|
71
|
+
module Tasks
|
|
72
|
+
class OverdueQuery < ApplicationQuery
|
|
73
|
+
def initialize(account:, assignee: nil, project: nil)
|
|
74
|
+
@account = account
|
|
75
|
+
@assignee = assignee
|
|
76
|
+
@project = project
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def call
|
|
80
|
+
scope = base_scope
|
|
81
|
+
scope = scope.where(assignee: @assignee) if @assignee
|
|
82
|
+
scope = scope.where(project: @project) if @project
|
|
83
|
+
scope
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def base_scope
|
|
89
|
+
Task
|
|
90
|
+
.joins(:project)
|
|
91
|
+
.where(projects: { account_id: @account.id })
|
|
92
|
+
.where.missing(:closure)
|
|
93
|
+
.where("tasks.due_date < ?", Date.current)
|
|
94
|
+
.includes(:assignee, :project)
|
|
95
|
+
.order(due_date: :asc)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Usage
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
# In controller
|
|
105
|
+
@overdue_tasks = Tasks::OverdueQuery.call(
|
|
106
|
+
account: current_account,
|
|
107
|
+
assignee: current_user
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Returns an ActiveRecord::Relation - can still chain
|
|
111
|
+
@overdue_tasks.limit(10)
|
|
112
|
+
@overdue_tasks.count
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Query Categories
|
|
116
|
+
|
|
117
|
+
### Filter Queries
|
|
118
|
+
|
|
119
|
+
Filter and sort records based on multiple criteria.
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
# app/queries/projects/filter_query.rb
|
|
123
|
+
module Projects
|
|
124
|
+
class FilterQuery < ApplicationQuery
|
|
125
|
+
def initialize(account:, params: {})
|
|
126
|
+
@account = account
|
|
127
|
+
@params = params
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def call
|
|
131
|
+
scope = @account.projects.includes(:creator, :closure)
|
|
132
|
+
scope = apply_status_filter(scope)
|
|
133
|
+
scope = apply_priority_filter(scope)
|
|
134
|
+
scope = apply_search(scope)
|
|
135
|
+
scope = apply_sort(scope)
|
|
136
|
+
scope
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
private
|
|
140
|
+
|
|
141
|
+
def apply_status_filter(scope)
|
|
142
|
+
case @params[:status]
|
|
143
|
+
when "open" then scope.open
|
|
144
|
+
when "closed" then scope.closed
|
|
145
|
+
else scope
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def apply_priority_filter(scope)
|
|
150
|
+
return scope if @params[:priority].blank?
|
|
151
|
+
scope.where(priority: @params[:priority])
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def apply_search(scope)
|
|
155
|
+
return scope if @params[:search].blank?
|
|
156
|
+
scope.where("projects.name LIKE ?", "%#{ActiveRecord::Base.sanitize_sql_like(@params[:search])}%")
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def apply_sort(scope)
|
|
160
|
+
case @params[:sort]
|
|
161
|
+
when "name" then scope.order(name: :asc)
|
|
162
|
+
when "newest" then scope.order(created_at: :desc)
|
|
163
|
+
when "oldest" then scope.order(created_at: :asc)
|
|
164
|
+
when "priority" then scope.order(priority: :asc)
|
|
165
|
+
else scope.order(created_at: :desc)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Aggregation Queries
|
|
173
|
+
|
|
174
|
+
Return computed results, not just filtered records.
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
# app/queries/accounts/task_stats_query.rb
|
|
178
|
+
module Accounts
|
|
179
|
+
class TaskStatsQuery < ApplicationQuery
|
|
180
|
+
def initialize(account:, date_range: nil)
|
|
181
|
+
@account = account
|
|
182
|
+
@date_range = date_range || (30.days.ago.to_date..Date.current)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def call
|
|
186
|
+
{
|
|
187
|
+
total: total_tasks,
|
|
188
|
+
open: open_tasks,
|
|
189
|
+
closed: closed_tasks,
|
|
190
|
+
overdue: overdue_tasks,
|
|
191
|
+
by_priority: tasks_by_priority,
|
|
192
|
+
by_project: tasks_by_project
|
|
193
|
+
}
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
private
|
|
197
|
+
|
|
198
|
+
def base_scope
|
|
199
|
+
@account.tasks.where(created_at: @date_range)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def total_tasks
|
|
203
|
+
base_scope.count
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def open_tasks
|
|
207
|
+
base_scope.open.count
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def closed_tasks
|
|
211
|
+
base_scope.closed.count
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def overdue_tasks
|
|
215
|
+
base_scope.open.where("due_date < ?", Date.current).count
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def tasks_by_priority
|
|
219
|
+
base_scope.group(:priority).count
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def tasks_by_project
|
|
223
|
+
base_scope
|
|
224
|
+
.joins(:project)
|
|
225
|
+
.group("projects.name")
|
|
226
|
+
.count
|
|
227
|
+
.sort_by { |_, count| -count }
|
|
228
|
+
.first(10)
|
|
229
|
+
.to_h
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Performance Patterns
|
|
236
|
+
|
|
237
|
+
### Eager Loading
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
# GOOD: Prevent N+1 queries
|
|
241
|
+
def call
|
|
242
|
+
Task
|
|
243
|
+
.includes(:project, :assignee, :closure)
|
|
244
|
+
.where(projects: { account_id: @account.id })
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# includes - Loads associations in separate queries (best for has_many)
|
|
248
|
+
# preload - Always uses separate queries
|
|
249
|
+
# eager_load - Uses LEFT JOIN (best when filtering on association)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Choosing the Right Loading Strategy
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
# Use includes for display (separate queries, no filtering)
|
|
256
|
+
Task.includes(:assignee).where(project: @project)
|
|
257
|
+
|
|
258
|
+
# Use eager_load when filtering on association (LEFT JOIN)
|
|
259
|
+
Task.eager_load(:closure).where(closures: { id: nil })
|
|
260
|
+
|
|
261
|
+
# Use preload when you know you need separate queries
|
|
262
|
+
Task.preload(:comments).where(project: @project)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Batch Processing
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
# Use find_each for large datasets to avoid loading all records into memory
|
|
269
|
+
@account.projects.find_each(batch_size: 100) do |project|
|
|
270
|
+
# Process each project
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Use in_batches for batch updates
|
|
274
|
+
@account.tasks.where(priority: nil).in_batches(of: 1000).update_all(priority: "medium")
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Select Only What You Need
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
# Instead of loading full records
|
|
281
|
+
@account.tasks.select(:id, :title, :due_date, :priority, :assignee_id)
|
|
282
|
+
|
|
283
|
+
# Use pluck for simple value extraction
|
|
284
|
+
@account.projects.pluck(:id, :name)
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## Composition Patterns
|
|
288
|
+
|
|
289
|
+
### Queries Returning Relations (Chainable)
|
|
290
|
+
|
|
291
|
+
```ruby
|
|
292
|
+
# Queries that return ActiveRecord::Relation can be chained
|
|
293
|
+
tasks = Tasks::OverdueQuery.call(account: current_account)
|
|
294
|
+
tasks.limit(10) # Still chainable
|
|
295
|
+
tasks.count # Works
|
|
296
|
+
tasks.where(priority: "high") # Further filtering
|
|
297
|
+
|
|
298
|
+
# In controller
|
|
299
|
+
@tasks = Tasks::OverdueQuery.call(account: current_account)
|
|
300
|
+
@tasks = paginate(@tasks) # Works with pagination concern
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Composing Multiple Queries
|
|
304
|
+
|
|
305
|
+
```ruby
|
|
306
|
+
# Compose by using one query's output as another's input
|
|
307
|
+
class Dashboard::MyWorkQuery < ApplicationQuery
|
|
308
|
+
def initialize(account:, user:)
|
|
309
|
+
@account = account
|
|
310
|
+
@user = user
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def call
|
|
314
|
+
{
|
|
315
|
+
overdue: Tasks::OverdueQuery.call(account: @account, assignee: @user).limit(5),
|
|
316
|
+
upcoming: Tasks::UpcomingQuery.call(account: @account, assignee: @user).limit(5),
|
|
317
|
+
recently_completed: Tasks::RecentlyCompletedQuery.call(account: @account, assignee: @user).limit(5)
|
|
318
|
+
}
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
## File Organization
|
|
324
|
+
|
|
325
|
+
```
|
|
326
|
+
app/queries/
|
|
327
|
+
application_query.rb
|
|
328
|
+
tasks/
|
|
329
|
+
overdue_query.rb
|
|
330
|
+
upcoming_query.rb
|
|
331
|
+
filter_query.rb
|
|
332
|
+
recently_completed_query.rb
|
|
333
|
+
projects/
|
|
334
|
+
filter_query.rb
|
|
335
|
+
accounts/
|
|
336
|
+
task_stats_query.rb
|
|
337
|
+
dashboard/
|
|
338
|
+
overview_query.rb
|
|
339
|
+
my_work_query.rb
|
|
340
|
+
search/
|
|
341
|
+
global_query.rb
|
|
342
|
+
reports/
|
|
343
|
+
project_progress_query.rb
|
|
344
|
+
monthly_summary_query.rb
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## Testing Query Objects with Minitest
|
|
348
|
+
|
|
349
|
+
### Testing Filter Queries
|
|
350
|
+
|
|
351
|
+
```ruby
|
|
352
|
+
# test/queries/projects/filter_query_test.rb
|
|
353
|
+
require "test_helper"
|
|
354
|
+
|
|
355
|
+
class Projects::FilterQueryTest < ActiveSupport::TestCase
|
|
356
|
+
setup do
|
|
357
|
+
@account = accounts(:acme)
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
test "returns all account projects by default" do
|
|
361
|
+
results = Projects::FilterQuery.call(account: @account)
|
|
362
|
+
assert_equal @account.projects.count, results.count
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
test "filters by open status" do
|
|
366
|
+
results = Projects::FilterQuery.call(account: @account, params: { status: "open" })
|
|
367
|
+
results.each do |project|
|
|
368
|
+
assert project.open?
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
test "filters by closed status" do
|
|
373
|
+
results = Projects::FilterQuery.call(account: @account, params: { status: "closed" })
|
|
374
|
+
results.each do |project|
|
|
375
|
+
assert project.closed?
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
test "filters by priority" do
|
|
380
|
+
results = Projects::FilterQuery.call(account: @account, params: { priority: "high" })
|
|
381
|
+
results.each do |project|
|
|
382
|
+
assert_equal "high", project.priority
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
test "searches by name" do
|
|
387
|
+
results = Projects::FilterQuery.call(account: @account, params: { search: "Redesign" })
|
|
388
|
+
assert_includes results, projects(:website_redesign)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
test "sorts by name" do
|
|
392
|
+
results = Projects::FilterQuery.call(account: @account, params: { sort: "name" })
|
|
393
|
+
names = results.map(&:name)
|
|
394
|
+
assert_equal names.sort, names
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
test "returns ActiveRecord::Relation for chaining" do
|
|
398
|
+
results = Projects::FilterQuery.call(account: @account)
|
|
399
|
+
assert_kind_of ActiveRecord::Relation, results
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## Anti-Patterns to Avoid
|
|
405
|
+
|
|
406
|
+
1. **Query objects for simple scopes** - `where(active: true)` belongs on the model.
|
|
407
|
+
2. **Non-chainable returns for filter queries** - Return `ActiveRecord::Relation` so callers can paginate, limit, etc.
|
|
408
|
+
3. **N+1 queries** - Always use `includes`/`preload`/`eager_load` for associated data.
|
|
409
|
+
4. **Database-specific SQL** - Stay agnostic. No `jsonb`, `array`, `pg_search`, `ILIKE`.
|
|
410
|
+
5. **Business logic in queries** - Queries should only read data. Mutations belong in services or models.
|
|
411
|
+
6. **Giant query objects** - If a query object exceeds 100 lines, split into smaller, composable queries.
|
|
412
|
+
7. **Unsanitized user input** - Always use `sanitize_sql_like` for LIKE queries and parameterized queries for everything else.
|