rails_claude_skills 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 (87) hide show
  1. checksums.yaml +7 -0
  2. data/.github/ISSUE_TEMPLATE/bug_report.yml +134 -0
  3. data/.github/ISSUE_TEMPLATE/config.yml +11 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.yml +129 -0
  5. data/.github/ISSUE_TEMPLATE/question.yml +90 -0
  6. data/.github/dependabot.yml +19 -0
  7. data/.github/workflows/ci.yml +77 -0
  8. data/.github/workflows/release.yml +66 -0
  9. data/.rubocop.yml +52 -0
  10. data/CHANGELOG.md +94 -0
  11. data/CLAUDE.md +332 -0
  12. data/CODE_OF_CONDUCT.md +134 -0
  13. data/CONTRIBUTING.md +580 -0
  14. data/LICENSE.txt +21 -0
  15. data/README.md +544 -0
  16. data/Rakefile +8 -0
  17. data/lib/generators/claude/agent/agent_generator.rb +71 -0
  18. data/lib/generators/claude/agent/templates/agent.md.tt +62 -0
  19. data/lib/generators/claude/command/command_generator.rb +50 -0
  20. data/lib/generators/claude/command/templates/command.md.tt +28 -0
  21. data/lib/generators/claude/commands_library/create-pr.md +27 -0
  22. data/lib/generators/claude/commands_library/dbchange.md +19 -0
  23. data/lib/generators/claude/commands_library/quality.md +20 -0
  24. data/lib/generators/claude/commands_library/stimulus.md +19 -0
  25. data/lib/generators/claude/commands_library/turbo-feature.md +17 -0
  26. data/lib/generators/claude/install/install_generator.rb +211 -0
  27. data/lib/generators/claude/install/templates/README.md.tt +59 -0
  28. data/lib/generators/claude/install/templates/USAGE +28 -0
  29. data/lib/generators/claude/install/templates/agents/api-dev.md.tt +46 -0
  30. data/lib/generators/claude/install/templates/agents/fullstack-dev.md.tt +48 -0
  31. data/lib/generators/claude/install/templates/agents/rails-developer.md.tt +40 -0
  32. data/lib/generators/claude/install/templates/settings.local.json.tt +13 -0
  33. data/lib/generators/claude/rule/rule_generator.rb +175 -0
  34. data/lib/generators/claude/rule/templates/rule.md.tt +7 -0
  35. data/lib/generators/claude/rules_library/code-style.md +37 -0
  36. data/lib/generators/claude/rules_library/database.md +47 -0
  37. data/lib/generators/claude/rules_library/hotwire.md +56 -0
  38. data/lib/generators/claude/rules_library/security.md +54 -0
  39. data/lib/generators/claude/rules_library/testing.md +47 -0
  40. data/lib/generators/claude/skill/skill_generator.rb +196 -0
  41. data/lib/generators/claude/skill/templates/SKILL.md.tt +27 -0
  42. data/lib/generators/claude/skills_library/create-task-files/SKILL.md +311 -0
  43. data/lib/generators/claude/skills_library/create-task-files/templates/bug.md +60 -0
  44. data/lib/generators/claude/skills_library/create-task-files/templates/epic.md +47 -0
  45. data/lib/generators/claude/skills_library/create-task-files/templates/issue.md +45 -0
  46. data/lib/generators/claude/skills_library/create-task-files/templates/user-story.md +57 -0
  47. data/lib/generators/claude/skills_library/minitest-testing/SKILL.md +398 -0
  48. data/lib/generators/claude/skills_library/minitest-testing/references/examples.md +889 -0
  49. data/lib/generators/claude/skills_library/plan-feature/SKILL.md +253 -0
  50. data/lib/generators/claude/skills_library/rails-api-controllers/SKILL.md +1041 -0
  51. data/lib/generators/claude/skills_library/rails-api-controllers/references/api-documentation.md +422 -0
  52. data/lib/generators/claude/skills_library/rails-api-controllers/references/serialization.md +456 -0
  53. data/lib/generators/claude/skills_library/rails-auth-with-devise/SKILL.md +191 -0
  54. data/lib/generators/claude/skills_library/rails-auth-with-devise/references/advanced.md +331 -0
  55. data/lib/generators/claude/skills_library/rails-auth-with-devise/references/api-auth.md +266 -0
  56. data/lib/generators/claude/skills_library/rails-auth-with-devise/references/omniauth.md +194 -0
  57. data/lib/generators/claude/skills_library/rails-authorization-cancancan/SKILL.md +603 -0
  58. data/lib/generators/claude/skills_library/rails-authorization-cancancan/references/api-authorization.md +543 -0
  59. data/lib/generators/claude/skills_library/rails-authorization-cancancan/references/complex-permissions.md +572 -0
  60. data/lib/generators/claude/skills_library/rails-authorization-cancancan/references/multi-tenancy.md +373 -0
  61. data/lib/generators/claude/skills_library/rails-controllers/SKILL.md +514 -0
  62. data/lib/generators/claude/skills_library/rails-debugging/SKILL.md +260 -0
  63. data/lib/generators/claude/skills_library/rails-deployment/SKILL.md +437 -0
  64. data/lib/generators/claude/skills_library/rails-deployment/references/examples.md +901 -0
  65. data/lib/generators/claude/skills_library/rails-hotwire/SKILL.md +367 -0
  66. data/lib/generators/claude/skills_library/rails-jobs/MISSION_CONTROL_SETUP.md +639 -0
  67. data/lib/generators/claude/skills_library/rails-jobs/SKILL.md +704 -0
  68. data/lib/generators/claude/skills_library/rails-mailers/SKILL.md +549 -0
  69. data/lib/generators/claude/skills_library/rails-models/SKILL.md +379 -0
  70. data/lib/generators/claude/skills_library/rails-pagination-kaminari/SKILL.md +622 -0
  71. data/lib/generators/claude/skills_library/rails-pagination-kaminari/references/api-pagination.md +523 -0
  72. data/lib/generators/claude/skills_library/rails-pagination-kaminari/references/custom-themes.md +498 -0
  73. data/lib/generators/claude/skills_library/rails-pagination-kaminari/references/performance.md +478 -0
  74. data/lib/generators/claude/skills_library/rails-views/SKILL.md +508 -0
  75. data/lib/generators/claude/skills_library/refine-requirements/SKILL.md +226 -0
  76. data/lib/generators/claude/skills_library/refine-requirements/references/examples.md +344 -0
  77. data/lib/generators/claude/skills_library/refine-requirements/references/reference.md +298 -0
  78. data/lib/generators/claude/skills_library/rspec-testing/SKILL.md +572 -0
  79. data/lib/generators/claude/skills_library/rspec-testing/references/better_specs_guide.md +273 -0
  80. data/lib/generators/claude/skills_library/rspec-testing/references/thoughtbot_patterns.md +407 -0
  81. data/lib/generators/claude/skills_library/tailwindcss/SKILL.md +371 -0
  82. data/lib/generators/claude/views/views_generator.rb +113 -0
  83. data/lib/rails_claude_skills/railtie.rb +16 -0
  84. data/lib/rails_claude_skills/version.rb +5 -0
  85. data/lib/rails_claude_skills.rb +27 -0
  86. data/sig/rails_claude_skills.rbs +4 -0
  87. metadata +199 -0
@@ -0,0 +1,478 @@
1
+ # Performance Optimization for Kaminari Pagination
2
+
3
+ Strategies and best practices for optimizing pagination performance in Rails applications.
4
+
5
+ ## The COUNT Query Problem
6
+
7
+ By default, Kaminari executes two queries:
8
+ 1. The main SELECT query to fetch records
9
+ 2. A COUNT query to get total records
10
+
11
+ For large tables, the COUNT query can be expensive.
12
+
13
+ ```sql
14
+ -- Main query
15
+ SELECT * FROM posts ORDER BY created_at LIMIT 25 OFFSET 0;
16
+
17
+ -- Count query (expensive on large tables)
18
+ SELECT COUNT(*) FROM posts;
19
+ ```
20
+
21
+ ## Skip COUNT with without_count
22
+
23
+ ```ruby
24
+ # Controller
25
+ def index
26
+ @posts = Post.order(:created_at).page(params[:page]).without_count
27
+ end
28
+
29
+ # View - limited helpers available
30
+ <%= link_to_prev_page @posts, 'Previous' %>
31
+ <span>Page <%= @posts.current_page %></span>
32
+ <%= link_to_next_page @posts, 'Next' %>
33
+
34
+ # These won't work with without_count:
35
+ # - total_pages
36
+ # - total_count
37
+ # - numbered page links
38
+ ```
39
+
40
+ ## Conditional COUNT Query
41
+
42
+ Only run COUNT on first page or when explicitly requested:
43
+
44
+ ```ruby
45
+ # app/controllers/posts_controller.rb
46
+ def index
47
+ @posts = Post.order(:created_at).page(params[:page])
48
+
49
+ # Skip count after first page
50
+ @posts = @posts.without_count unless first_page?
51
+ end
52
+
53
+ private
54
+
55
+ def first_page?
56
+ params[:page].blank? || params[:page].to_i <= 1
57
+ end
58
+ ```
59
+
60
+ ## Caching Total Count
61
+
62
+ Cache the count query result:
63
+
64
+ ```ruby
65
+ # app/models/post.rb
66
+ class Post < ApplicationRecord
67
+ def self.cached_total_count
68
+ Rails.cache.fetch(['Post', 'total_count'], expires_in: 5.minutes) do
69
+ count
70
+ end
71
+ end
72
+ end
73
+
74
+ # app/controllers/posts_controller.rb
75
+ def index
76
+ @posts = Post.order(:created_at).page(params[:page]).without_count
77
+ @total_count = Post.cached_total_count
78
+ @total_pages = (@total_count.to_f / Post.default_per_page).ceil
79
+ end
80
+
81
+ # View
82
+ <div class="pagination-info">
83
+ Page <%= @posts.current_page %> of approximately <%= @total_pages %>
84
+ </div>
85
+ ```
86
+
87
+ ## Counter Cache for Associations
88
+
89
+ Use counter caches to avoid counting associated records:
90
+
91
+ ```ruby
92
+ # Migration
93
+ class AddPostsCountToCategories < ActiveRecord::Migration[7.0]
94
+ def change
95
+ add_column :categories, :posts_count, :integer, default: 0, null: false
96
+
97
+ # Populate existing counts
98
+ reversible do |dir|
99
+ dir.up do
100
+ Category.find_each do |category|
101
+ Category.reset_counters(category.id, :posts)
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ # Model
109
+ class Post < ApplicationRecord
110
+ belongs_to :category, counter_cache: true
111
+ end
112
+
113
+ # Controller - use counter cache instead of count
114
+ def index
115
+ @category = Category.find(params[:category_id])
116
+ @posts = @category.posts.page(params[:page])
117
+ @total_count = @category.posts_count # Fast! No COUNT query
118
+ end
119
+ ```
120
+
121
+ ## Database Indexes
122
+
123
+ Add indexes for paginated queries:
124
+
125
+ ```ruby
126
+ # Migration
127
+ class AddIndexesToPosts < ActiveRecord::Migration[7.0]
128
+ def change
129
+ # For ORDER BY created_at
130
+ add_index :posts, :created_at
131
+
132
+ # For filtered pagination
133
+ add_index :posts, [:category_id, :created_at]
134
+ add_index :posts, [:published, :created_at]
135
+
136
+ # Composite index for common queries
137
+ add_index :posts, [:user_id, :status, :created_at]
138
+ end
139
+ end
140
+ ```
141
+
142
+ ## Eager Loading (N+1 Prevention)
143
+
144
+ ```ruby
145
+ # Bad - N+1 queries
146
+ def index
147
+ @posts = Post.page(params[:page])
148
+ end
149
+
150
+ # View triggers N+1
151
+ <% @posts.each do |post| %>
152
+ <%= post.user.name %> # Query for each post!
153
+ <%= post.comments.count %> # Query for each post!
154
+ <% end %>
155
+
156
+ # Good - eager loading
157
+ def index
158
+ @posts = Post.includes(:user, :comments)
159
+ .page(params[:page])
160
+ end
161
+
162
+ # Multiple associations
163
+ @posts = Post.includes(:user, :category, comments: :user)
164
+ .page(params[:page])
165
+
166
+ # Nested associations
167
+ @posts = Post.includes(comments: [:user, :likes])
168
+ .page(params[:page])
169
+ ```
170
+
171
+ ## Select Only Needed Columns
172
+
173
+ ```ruby
174
+ # Bad - loads all columns
175
+ @posts = Post.page(params[:page])
176
+
177
+ # Good - only load needed columns
178
+ @posts = Post.select(:id, :title, :created_at, :user_id)
179
+ .page(params[:page])
180
+
181
+ # Even better with pluck for simple lists
182
+ @post_titles = Post.order(:created_at)
183
+ .page(params[:page])
184
+ .pluck(:id, :title)
185
+ ```
186
+
187
+ ## Pagination with Scopes
188
+
189
+ Use scopes to avoid repeated query building:
190
+
191
+ ```ruby
192
+ # app/models/post.rb
193
+ class Post < ApplicationRecord
194
+ scope :recent, -> { order(created_at: :desc) }
195
+ scope :published, -> { where(published: true) }
196
+ scope :by_category, ->(category_id) { where(category_id: category_id) if category_id.present? }
197
+ scope :paginated, ->(page, per_page = 25) { page(page).per(per_page) }
198
+
199
+ # Optimized scope with eager loading
200
+ scope :with_associations, -> { includes(:user, :category) }
201
+ end
202
+
203
+ # Controller
204
+ def index
205
+ @posts = Post.published
206
+ .by_category(params[:category_id])
207
+ .with_associations
208
+ .recent
209
+ .paginated(params[:page], 20)
210
+ end
211
+ ```
212
+
213
+ ## Cursor-Based Pagination
214
+
215
+ For large datasets or real-time feeds, use cursor-based pagination:
216
+
217
+ ```ruby
218
+ # app/controllers/posts_controller.rb
219
+ def index
220
+ @posts = fetch_posts_by_cursor
221
+ end
222
+
223
+ private
224
+
225
+ def fetch_posts_by_cursor
226
+ posts = Post.order(id: :desc)
227
+
228
+ if params[:after_id]
229
+ posts = posts.where('id < ?', params[:after_id])
230
+ end
231
+
232
+ posts.limit(20)
233
+ end
234
+
235
+ # View
236
+ <% if @posts.any? %>
237
+ <%= render @posts %>
238
+ <%= link_to 'Load More', posts_path(after_id: @posts.last.id),
239
+ class: 'load-more' %>
240
+ <% end %>
241
+ ```
242
+
243
+ Benefits of cursor pagination:
244
+ - No COUNT query needed
245
+ - Consistent results even when data changes
246
+ - Better performance on large offsets
247
+ - Works well with infinite scroll
248
+
249
+ ## Fragment Caching
250
+
251
+ Cache rendered pagination:
252
+
253
+ ```ruby
254
+ # View
255
+ <% cache ["posts-index", @posts.current_page, @posts.updated_at.to_i] do %>
256
+ <%= render @posts %>
257
+ <% end %>
258
+
259
+ <%= paginate @posts %>
260
+
261
+ # Russian Doll Caching
262
+ <% cache ["posts-index", @posts.current_page] do %>
263
+ <% @posts.each do |post| %>
264
+ <% cache post do %>
265
+ <%= render post %>
266
+ <% end %>
267
+ <% end %>
268
+ <% end %>
269
+ ```
270
+
271
+ ## Query Result Caching
272
+
273
+ ```ruby
274
+ # app/controllers/posts_controller.rb
275
+ def index
276
+ cache_key = ['posts-page', params[:page], params[:category_id]].compact.join('-')
277
+
278
+ @posts = Rails.cache.fetch(cache_key, expires_in: 5.minutes) do
279
+ Post.published
280
+ .by_category(params[:category_id])
281
+ .includes(:user)
282
+ .page(params[:page])
283
+ .to_a # Convert to array to cache results
284
+ end
285
+ end
286
+ ```
287
+
288
+ ## Approximate COUNT for Large Tables
289
+
290
+ For very large tables, use approximate counts:
291
+
292
+ ```ruby
293
+ # PostgreSQL
294
+ def approximate_count
295
+ result = ActiveRecord::Base.connection.execute(
296
+ "SELECT reltuples::bigint AS estimate FROM pg_class WHERE relname = 'posts'"
297
+ )
298
+ result[0]['estimate'].to_i
299
+ end
300
+
301
+ # Use in controller
302
+ def index
303
+ @posts = Post.page(params[:page]).without_count
304
+ @approximate_total = Post.approximate_count
305
+ @approximate_pages = (@approximate_total.to_f / Post.default_per_page).ceil
306
+ end
307
+ ```
308
+
309
+ ## Partial Pagination
310
+
311
+ Only paginate when necessary:
312
+
313
+ ```ruby
314
+ # app/controllers/posts_controller.rb
315
+ def index
316
+ @posts = Post.order(:created_at)
317
+
318
+ # Only paginate if there are many records
319
+ if @posts.count > 50
320
+ @posts = @posts.page(params[:page])
321
+ @use_pagination = true
322
+ end
323
+ end
324
+
325
+ # View
326
+ <%= render @posts %>
327
+ <%= paginate @posts if @use_pagination %>
328
+ ```
329
+
330
+ ## Database-Specific Optimizations
331
+
332
+ ### PostgreSQL
333
+
334
+ ```ruby
335
+ # Use DISTINCT ON for unique pagination
336
+ @posts = Post.select('DISTINCT ON (posts.user_id) posts.*')
337
+ .order('posts.user_id, posts.created_at DESC')
338
+ .page(params[:page])
339
+
340
+ # Use materialized views for complex queries
341
+ # db/migrate/xxx_create_popular_posts_view.rb
342
+ execute <<-SQL
343
+ CREATE MATERIALIZED VIEW popular_posts AS
344
+ SELECT posts.*, COUNT(likes.id) as likes_count
345
+ FROM posts
346
+ LEFT JOIN likes ON likes.post_id = posts.id
347
+ GROUP BY posts.id
348
+ ORDER BY likes_count DESC
349
+ SQL
350
+
351
+ # Refresh materialized view periodically
352
+ PopularPost.connection.execute('REFRESH MATERIALIZED VIEW popular_posts')
353
+ ```
354
+
355
+ ### MySQL
356
+
357
+ ```ruby
358
+ # Use SQL_CALC_FOUND_ROWS (MySQL only)
359
+ class Post < ApplicationRecord
360
+ def self.paginate_with_total(page, per_page = 25)
361
+ offset = (page - 1) * per_page
362
+
363
+ posts = connection.select_all(
364
+ "SELECT SQL_CALC_FOUND_ROWS * FROM posts LIMIT #{per_page} OFFSET #{offset}"
365
+ ).to_a
366
+
367
+ total = connection.select_value('SELECT FOUND_ROWS()')
368
+
369
+ Kaminari.paginate_array(posts, total_count: total).page(page).per(per_page)
370
+ end
371
+ end
372
+ ```
373
+
374
+ ## Monitoring Performance
375
+
376
+ ### Query Analysis
377
+
378
+ ```ruby
379
+ # development.rb
380
+ config.after_initialize do
381
+ ActiveSupport::Notifications.subscribe('sql.active_record') do |_, start, finish, _, details|
382
+ duration = ((finish - start) * 1000).round(2)
383
+ if duration > 100 # Log slow queries
384
+ Rails.logger.warn "SLOW QUERY (#{duration}ms): #{details[:sql]}"
385
+ end
386
+ end
387
+ end
388
+ ```
389
+
390
+ ### Bullet Gem
391
+
392
+ ```ruby
393
+ # Gemfile
394
+ group :development do
395
+ gem 'bullet'
396
+ end
397
+
398
+ # config/environments/development.rb
399
+ config.after_initialize do
400
+ Bullet.enable = true
401
+ Bullet.alert = true
402
+ Bullet.bullet_logger = true
403
+ Bullet.console = true
404
+ Bullet.rails_logger = true
405
+ end
406
+ ```
407
+
408
+ ### Benchmark Pagination
409
+
410
+ ```ruby
411
+ # Controller
412
+ def index
413
+ time = Benchmark.measure do
414
+ @posts = Post.includes(:user, :category)
415
+ .order(:created_at)
416
+ .page(params[:page])
417
+ end
418
+
419
+ Rails.logger.info "Pagination took: #{time.real} seconds"
420
+ end
421
+ ```
422
+
423
+ ## Best Practices Summary
424
+
425
+ 1. **Use indexes**: Add indexes for ORDER BY and WHERE clauses
426
+ 2. **Eager load associations**: Use `includes` to prevent N+1 queries
427
+ 3. **Skip COUNT when possible**: Use `without_count` for large datasets
428
+ 4. **Cache counts**: Cache expensive COUNT queries
429
+ 5. **Use counter caches**: For association counts
430
+ 6. **Select only needed columns**: Reduce data transfer
431
+ 7. **Consider cursor pagination**: For real-time feeds and large datasets
432
+ 8. **Fragment cache**: Cache rendered pagination results
433
+ 9. **Monitor query performance**: Use Bullet and query logs
434
+ 10. **Test with production data**: Pagination performance issues often only appear at scale
435
+
436
+ ## Performance Checklist
437
+
438
+ Before deploying pagination:
439
+
440
+ - [ ] Added indexes for ORDER BY columns
441
+ - [ ] Eager loaded all associations used in views
442
+ - [ ] Considered using `without_count` for large tables
443
+ - [ ] Tested with production-sized dataset
444
+ - [ ] Verified no N+1 queries with Bullet
445
+ - [ ] Measured query execution time
446
+ - [ ] Implemented caching strategy if needed
447
+ - [ ] Set reasonable `max_per_page` limit
448
+ - [ ] Tested pagination edge cases (first, last, empty)
449
+ - [ ] Verified mobile/responsive performance
450
+
451
+ ## Measuring Impact
452
+
453
+ ```ruby
454
+ # Before optimization
455
+ User.page(10).per(25)
456
+ # => 2 queries: SELECT (0.5ms), COUNT (150ms) - SLOW!
457
+
458
+ # After adding index
459
+ add_index :users, :created_at
460
+
461
+ User.order(:created_at).page(10).per(25)
462
+ # => 2 queries: SELECT (0.5ms), COUNT (0.8ms) - FAST!
463
+
464
+ # After using without_count
465
+ User.order(:created_at).page(10).per(25).without_count
466
+ # => 1 query: SELECT (0.5ms) - FASTEST!
467
+ ```
468
+
469
+ ## When to Optimize
470
+
471
+ Optimize pagination when:
472
+ - Tables have > 10,000 records
473
+ - COUNT queries take > 100ms
474
+ - Users report slow page loads
475
+ - Database CPU usage is high
476
+ - You're hitting query timeouts
477
+
478
+ Start simple, measure, then optimize based on real performance data.