omgkit 2.1.1 → 2.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.
- package/package.json +1 -1
- package/plugin/skills/databases/mongodb/SKILL.md +81 -28
- package/plugin/skills/databases/prisma/SKILL.md +87 -32
- package/plugin/skills/databases/redis/SKILL.md +80 -27
- package/plugin/skills/devops/aws/SKILL.md +80 -26
- package/plugin/skills/devops/github-actions/SKILL.md +84 -32
- package/plugin/skills/devops/kubernetes/SKILL.md +94 -32
- package/plugin/skills/devops/performance-profiling/SKILL.md +59 -863
- package/plugin/skills/frameworks/django/SKILL.md +158 -24
- package/plugin/skills/frameworks/express/SKILL.md +153 -33
- package/plugin/skills/frameworks/fastapi/SKILL.md +153 -34
- package/plugin/skills/frameworks/laravel/SKILL.md +146 -33
- package/plugin/skills/frameworks/nestjs/SKILL.md +137 -25
- package/plugin/skills/frameworks/rails/SKILL.md +594 -28
- package/plugin/skills/frameworks/react/SKILL.md +94 -962
- package/plugin/skills/frameworks/spring/SKILL.md +528 -35
- package/plugin/skills/frameworks/vue/SKILL.md +147 -25
- package/plugin/skills/frontend/accessibility/SKILL.md +145 -36
- package/plugin/skills/frontend/frontend-design/SKILL.md +114 -29
- package/plugin/skills/frontend/responsive/SKILL.md +131 -28
- package/plugin/skills/frontend/shadcn-ui/SKILL.md +133 -43
- package/plugin/skills/frontend/tailwindcss/SKILL.md +105 -37
- package/plugin/skills/frontend/threejs/SKILL.md +110 -35
- package/plugin/skills/languages/javascript/SKILL.md +195 -34
- package/plugin/skills/methodology/brainstorming/SKILL.md +98 -30
- package/plugin/skills/methodology/defense-in-depth/SKILL.md +83 -37
- package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +92 -31
- package/plugin/skills/methodology/executing-plans/SKILL.md +117 -28
- package/plugin/skills/methodology/finishing-development-branch/SKILL.md +111 -32
- package/plugin/skills/methodology/problem-solving/SKILL.md +65 -311
- package/plugin/skills/methodology/receiving-code-review/SKILL.md +76 -27
- package/plugin/skills/methodology/requesting-code-review/SKILL.md +93 -22
- package/plugin/skills/methodology/root-cause-tracing/SKILL.md +75 -40
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +75 -224
- package/plugin/skills/methodology/systematic-debugging/SKILL.md +81 -35
- package/plugin/skills/methodology/test-driven-development/SKILL.md +120 -26
- package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +88 -35
- package/plugin/skills/methodology/token-optimization/SKILL.md +73 -34
- package/plugin/skills/methodology/verification-before-completion/SKILL.md +128 -28
- package/plugin/skills/methodology/writing-plans/SKILL.md +105 -20
- package/plugin/skills/omega/omega-architecture/SKILL.md +178 -40
- package/plugin/skills/omega/omega-coding/SKILL.md +247 -41
- package/plugin/skills/omega/omega-sprint/SKILL.md +208 -46
- package/plugin/skills/omega/omega-testing/SKILL.md +253 -42
- package/plugin/skills/omega/omega-thinking/SKILL.md +263 -51
- package/plugin/skills/security/better-auth/SKILL.md +83 -34
- package/plugin/skills/security/oauth/SKILL.md +118 -35
- package/plugin/skills/security/owasp/SKILL.md +112 -35
- package/plugin/skills/testing/playwright/SKILL.md +141 -38
- package/plugin/skills/testing/pytest/SKILL.md +137 -38
- package/plugin/skills/testing/vitest/SKILL.md +124 -39
- package/plugin/skills/tools/document-processing/SKILL.md +111 -838
- package/plugin/skills/tools/image-processing/SKILL.md +126 -659
- package/plugin/skills/tools/mcp-development/SKILL.md +85 -758
- package/plugin/skills/tools/media-processing/SKILL.md +118 -735
- package/plugin/stdrules/SKILL_STANDARDS.md +490 -0
|
@@ -1,56 +1,622 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: rails
|
|
3
|
-
description: Ruby on Rails development
|
|
3
|
+
description: Enterprise Ruby on Rails development with Active Record, API mode, testing, and production patterns
|
|
4
|
+
category: frameworks
|
|
5
|
+
triggers:
|
|
6
|
+
- rails
|
|
7
|
+
- ruby on rails
|
|
8
|
+
- ror
|
|
9
|
+
- active record
|
|
10
|
+
- ruby api
|
|
11
|
+
- rails api
|
|
12
|
+
- ruby web
|
|
13
|
+
- rubygems
|
|
4
14
|
---
|
|
5
15
|
|
|
6
|
-
# Ruby on Rails
|
|
16
|
+
# Ruby on Rails
|
|
7
17
|
|
|
8
|
-
|
|
18
|
+
Enterprise-grade **Ruby on Rails development** following industry best practices. This skill covers Active Record, API mode, service objects, authentication, testing with RSpec, background jobs, and production deployment configurations used by top engineering teams.
|
|
19
|
+
|
|
20
|
+
## Purpose
|
|
21
|
+
|
|
22
|
+
Build scalable Ruby applications with confidence:
|
|
23
|
+
|
|
24
|
+
- Design clean model architectures with Active Record
|
|
25
|
+
- Implement REST APIs with Rails API mode
|
|
26
|
+
- Use service objects and concerns for clean code
|
|
27
|
+
- Handle authentication with JWT or Devise
|
|
28
|
+
- Write comprehensive tests with RSpec
|
|
29
|
+
- Deploy production-ready applications
|
|
30
|
+
- Leverage background jobs with Sidekiq
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
### 1. Model Design and Associations
|
|
9
35
|
|
|
10
|
-
### Model
|
|
11
36
|
```ruby
|
|
37
|
+
# app/models/user.rb
|
|
12
38
|
class User < ApplicationRecord
|
|
13
|
-
|
|
14
|
-
|
|
39
|
+
has_secure_password
|
|
40
|
+
|
|
41
|
+
# Associations
|
|
42
|
+
has_many :memberships, dependent: :destroy
|
|
43
|
+
has_many :organizations, through: :memberships
|
|
44
|
+
has_many :owned_organizations, class_name: 'Organization', foreign_key: :owner_id
|
|
45
|
+
has_many :projects, foreign_key: :created_by
|
|
46
|
+
|
|
47
|
+
# Validations
|
|
48
|
+
validates :email, presence: true, uniqueness: { case_sensitive: false },
|
|
49
|
+
format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
50
|
+
validates :name, presence: true, length: { minimum: 2, maximum: 100 }
|
|
51
|
+
validates :password, length: { minimum: 8 }, if: -> { new_record? || password.present? }
|
|
52
|
+
validates :role, inclusion: { in: %w[admin user guest] }
|
|
53
|
+
|
|
54
|
+
# Callbacks
|
|
55
|
+
before_save :downcase_email
|
|
56
|
+
|
|
57
|
+
# Scopes
|
|
58
|
+
scope :active, -> { where(is_active: true) }
|
|
59
|
+
scope :admins, -> { where(role: 'admin') }
|
|
60
|
+
scope :search, ->(query) {
|
|
61
|
+
return all if query.blank?
|
|
62
|
+
where('name ILIKE :q OR email ILIKE :q', q: "%#{query}%")
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Enums
|
|
66
|
+
enum :role, { guest: 'guest', user: 'user', admin: 'admin' }, default: :user
|
|
67
|
+
|
|
68
|
+
# Instance methods
|
|
69
|
+
def admin?
|
|
70
|
+
role == 'admin'
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def member_of?(organization)
|
|
74
|
+
organizations.exists?(organization.id)
|
|
75
|
+
end
|
|
15
76
|
|
|
16
|
-
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def downcase_email
|
|
80
|
+
self.email = email.downcase
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# app/models/organization.rb
|
|
86
|
+
class Organization < ApplicationRecord
|
|
87
|
+
belongs_to :owner, class_name: 'User'
|
|
88
|
+
has_many :memberships, dependent: :destroy
|
|
89
|
+
has_many :members, through: :memberships, source: :user
|
|
90
|
+
has_many :projects, dependent: :destroy
|
|
91
|
+
|
|
92
|
+
validates :name, presence: true, length: { maximum: 255 }
|
|
93
|
+
validates :slug, presence: true, uniqueness: true,
|
|
94
|
+
format: { with: /\A[a-z0-9-]+\z/ }
|
|
95
|
+
|
|
96
|
+
scope :for_user, ->(user) { joins(:memberships).where(memberships: { user_id: user.id }) }
|
|
97
|
+
|
|
98
|
+
before_validation :generate_slug, on: :create
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def generate_slug
|
|
103
|
+
self.slug ||= name&.parameterize
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# app/models/project.rb
|
|
109
|
+
class Project < ApplicationRecord
|
|
110
|
+
belongs_to :organization
|
|
111
|
+
belongs_to :creator, class_name: 'User', foreign_key: :created_by
|
|
112
|
+
has_many :tasks, dependent: :destroy
|
|
113
|
+
|
|
114
|
+
validates :name, presence: true, length: { maximum: 255 }
|
|
115
|
+
validates :name, uniqueness: { scope: :organization_id }
|
|
116
|
+
|
|
117
|
+
enum :status, { draft: 'draft', active: 'active', completed: 'completed', archived: 'archived' }
|
|
118
|
+
|
|
119
|
+
scope :active, -> { where(status: :active) }
|
|
120
|
+
|
|
121
|
+
include Discard::Model
|
|
122
|
+
default_scope -> { kept }
|
|
17
123
|
end
|
|
18
124
|
```
|
|
19
125
|
|
|
20
|
-
###
|
|
126
|
+
### 2. Serializers
|
|
127
|
+
|
|
21
128
|
```ruby
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
129
|
+
# app/serializers/user_serializer.rb
|
|
130
|
+
class UserSerializer
|
|
131
|
+
include JSONAPI::Serializer
|
|
132
|
+
|
|
133
|
+
attributes :id, :email, :name, :role, :is_active, :created_at, :updated_at
|
|
134
|
+
|
|
135
|
+
attribute :organization_count do |user|
|
|
136
|
+
user.organizations.count
|
|
26
137
|
end
|
|
27
138
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
139
|
+
has_many :organizations, serializer: OrganizationSerializer, if: Proc.new { |_record, params|
|
|
140
|
+
params && params[:include_organizations]
|
|
141
|
+
}
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# app/serializers/organization_serializer.rb
|
|
146
|
+
class OrganizationSerializer
|
|
147
|
+
include JSONAPI::Serializer
|
|
148
|
+
|
|
149
|
+
attributes :id, :name, :slug, :created_at
|
|
150
|
+
|
|
151
|
+
attribute :member_count do |organization|
|
|
152
|
+
organization.members.count
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
belongs_to :owner, serializer: UserSerializer
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# app/serializers/pagination_serializer.rb
|
|
160
|
+
class PaginationSerializer
|
|
161
|
+
def initialize(collection, serializer_class, options = {})
|
|
162
|
+
@collection = collection
|
|
163
|
+
@serializer_class = serializer_class
|
|
164
|
+
@options = options
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def as_json
|
|
168
|
+
{
|
|
169
|
+
data: serialized_data,
|
|
170
|
+
pagination: {
|
|
171
|
+
current_page: @collection.current_page,
|
|
172
|
+
per_page: @collection.limit_value,
|
|
173
|
+
total: @collection.total_count,
|
|
174
|
+
total_pages: @collection.total_pages,
|
|
175
|
+
has_more: @collection.current_page < @collection.total_pages
|
|
176
|
+
}
|
|
177
|
+
}
|
|
35
178
|
end
|
|
36
179
|
|
|
37
180
|
private
|
|
38
181
|
|
|
39
|
-
def
|
|
40
|
-
|
|
182
|
+
def serialized_data
|
|
183
|
+
@serializer_class.new(@collection, @options).serializable_hash[:data]
|
|
41
184
|
end
|
|
42
185
|
end
|
|
43
186
|
```
|
|
44
187
|
|
|
45
|
-
###
|
|
188
|
+
### 3. Controllers
|
|
189
|
+
|
|
46
190
|
```ruby
|
|
47
|
-
|
|
48
|
-
|
|
191
|
+
# app/controllers/application_controller.rb
|
|
192
|
+
class ApplicationController < ActionController::API
|
|
193
|
+
include ActionController::HttpAuthentication::Token::ControllerMethods
|
|
194
|
+
|
|
195
|
+
before_action :authenticate_user!
|
|
196
|
+
|
|
197
|
+
rescue_from ActiveRecord::RecordNotFound, with: :not_found
|
|
198
|
+
rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity
|
|
199
|
+
|
|
200
|
+
private
|
|
201
|
+
|
|
202
|
+
def authenticate_user!
|
|
203
|
+
authenticate_or_request_with_http_token do |token, _options|
|
|
204
|
+
@current_user = JsonWebToken.decode(token)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def current_user
|
|
209
|
+
@current_user
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def authorize_admin!
|
|
213
|
+
render_forbidden unless current_user.admin?
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def not_found(exception)
|
|
217
|
+
render json: { error: { code: 'NOT_FOUND', message: exception.message } }, status: :not_found
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def unprocessable_entity(exception)
|
|
221
|
+
render json: { error: { code: 'VALIDATION_ERROR', details: exception.record.errors } }, status: :unprocessable_entity
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def render_forbidden
|
|
225
|
+
render json: { error: { code: 'FORBIDDEN', message: 'Access denied' } }, status: :forbidden
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# app/controllers/api/v1/users_controller.rb
|
|
231
|
+
module Api
|
|
232
|
+
module V1
|
|
233
|
+
class UsersController < ApplicationController
|
|
234
|
+
before_action :authorize_admin!, except: [:me, :update_me]
|
|
235
|
+
before_action :set_user, only: [:show, :update, :destroy]
|
|
236
|
+
|
|
237
|
+
def index
|
|
238
|
+
users = User.active.search(params[:search])
|
|
239
|
+
users = users.where(role: params[:role]) if params[:role].present?
|
|
240
|
+
users = users.page(params[:page]).per(params[:per_page] || 20)
|
|
241
|
+
|
|
242
|
+
render json: PaginationSerializer.new(users, UserSerializer).as_json
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def show
|
|
246
|
+
render json: UserSerializer.new(@user, include_organizations: true).serializable_hash
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def create
|
|
250
|
+
user = User.new(user_params)
|
|
251
|
+
user.save!
|
|
252
|
+
render json: UserSerializer.new(user).serializable_hash, status: :created
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def update
|
|
256
|
+
@user.update!(user_params)
|
|
257
|
+
render json: UserSerializer.new(@user).serializable_hash
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def destroy
|
|
261
|
+
@user.destroy
|
|
262
|
+
head :no_content
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def me
|
|
266
|
+
render json: UserSerializer.new(current_user, include_organizations: true).serializable_hash
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
private
|
|
270
|
+
|
|
271
|
+
def set_user
|
|
272
|
+
@user = User.find(params[:id])
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def user_params
|
|
276
|
+
params.require(:user).permit(:name, :email, :password, :role, :is_active)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# app/controllers/api/v1/auth_controller.rb
|
|
284
|
+
module Api
|
|
285
|
+
module V1
|
|
286
|
+
class AuthController < ApplicationController
|
|
287
|
+
skip_before_action :authenticate_user!, only: [:register, :login]
|
|
288
|
+
|
|
289
|
+
def register
|
|
290
|
+
user = User.new(register_params)
|
|
291
|
+
user.save!
|
|
292
|
+
|
|
293
|
+
token = JsonWebToken.encode(user_id: user.id)
|
|
294
|
+
render json: { user: UserSerializer.new(user).serializable_hash, token: token }, status: :created
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def login
|
|
298
|
+
user = User.find_by(email: params[:email]&.downcase)
|
|
299
|
+
|
|
300
|
+
if user&.authenticate(params[:password]) && user.is_active?
|
|
301
|
+
token = JsonWebToken.encode(user_id: user.id)
|
|
302
|
+
render json: { user: UserSerializer.new(user).serializable_hash, token: token }
|
|
303
|
+
else
|
|
304
|
+
render json: { error: { code: 'UNAUTHORIZED', message: 'Invalid credentials' } }, status: :unauthorized
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
private
|
|
309
|
+
|
|
310
|
+
def register_params
|
|
311
|
+
params.require(:user).permit(:name, :email, :password)
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### 4. Service Objects
|
|
319
|
+
|
|
320
|
+
```ruby
|
|
321
|
+
# app/services/application_service.rb
|
|
322
|
+
class ApplicationService
|
|
323
|
+
def self.call(...)
|
|
324
|
+
new(...).call
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
# app/services/users/create_user_service.rb
|
|
330
|
+
module Users
|
|
331
|
+
class CreateUserService < ApplicationService
|
|
332
|
+
def initialize(params)
|
|
333
|
+
@params = params
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def call
|
|
337
|
+
user = User.new(@params)
|
|
338
|
+
user.save!
|
|
339
|
+
|
|
340
|
+
SendWelcomeEmailJob.perform_later(user.id)
|
|
341
|
+
|
|
342
|
+
Result.success(user: user)
|
|
343
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
344
|
+
Result.failure(errors: e.record.errors)
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
# app/services/organizations/create_organization_service.rb
|
|
351
|
+
module Organizations
|
|
352
|
+
class CreateOrganizationService < ApplicationService
|
|
353
|
+
def initialize(owner:, params:)
|
|
354
|
+
@owner = owner
|
|
355
|
+
@params = params
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def call
|
|
359
|
+
ActiveRecord::Base.transaction do
|
|
360
|
+
organization = Organization.create!(@params.merge(owner: @owner))
|
|
361
|
+
Membership.create!(user: @owner, organization: organization, role: :owner)
|
|
362
|
+
|
|
363
|
+
Result.success(organization: organization)
|
|
364
|
+
end
|
|
365
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
366
|
+
Result.failure(errors: e.record.errors)
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
# app/services/result.rb
|
|
373
|
+
class Result
|
|
374
|
+
attr_reader :data, :errors
|
|
375
|
+
|
|
376
|
+
def initialize(success:, data: {}, errors: nil)
|
|
377
|
+
@success = success
|
|
378
|
+
@data = data
|
|
379
|
+
@errors = errors
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def success?
|
|
383
|
+
@success
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def failure?
|
|
387
|
+
!@success
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def self.success(data = {})
|
|
391
|
+
new(success: true, data: data)
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def self.failure(errors:)
|
|
395
|
+
new(success: false, errors: errors)
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### 5. JWT Authentication
|
|
401
|
+
|
|
402
|
+
```ruby
|
|
403
|
+
# lib/json_web_token.rb
|
|
404
|
+
class JsonWebToken
|
|
405
|
+
SECRET_KEY = Rails.application.credentials.secret_key_base
|
|
406
|
+
|
|
407
|
+
def self.encode(payload, exp = 24.hours.from_now)
|
|
408
|
+
payload[:exp] = exp.to_i
|
|
409
|
+
JWT.encode(payload, SECRET_KEY)
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def self.decode(token)
|
|
413
|
+
decoded = JWT.decode(token, SECRET_KEY)[0]
|
|
414
|
+
User.find(decoded['user_id'])
|
|
415
|
+
rescue JWT::DecodeError, ActiveRecord::RecordNotFound
|
|
416
|
+
nil
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### 6. Background Jobs
|
|
422
|
+
|
|
423
|
+
```ruby
|
|
424
|
+
# app/jobs/application_job.rb
|
|
425
|
+
class ApplicationJob < ActiveJob::Base
|
|
426
|
+
queue_as :default
|
|
427
|
+
|
|
428
|
+
retry_on StandardError, wait: :exponentially_longer, attempts: 3
|
|
429
|
+
discard_on ActiveJob::DeserializationError
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
# app/jobs/send_welcome_email_job.rb
|
|
434
|
+
class SendWelcomeEmailJob < ApplicationJob
|
|
435
|
+
queue_as :mailers
|
|
436
|
+
|
|
437
|
+
def perform(user_id)
|
|
438
|
+
user = User.find(user_id)
|
|
439
|
+
UserMailer.welcome_email(user).deliver_now
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### 7. Testing with RSpec
|
|
445
|
+
|
|
446
|
+
```ruby
|
|
447
|
+
# spec/models/user_spec.rb
|
|
448
|
+
require 'rails_helper'
|
|
449
|
+
|
|
450
|
+
RSpec.describe User, type: :model do
|
|
451
|
+
describe 'validations' do
|
|
452
|
+
subject { build(:user) }
|
|
453
|
+
|
|
454
|
+
it { should validate_presence_of(:email) }
|
|
455
|
+
it { should validate_uniqueness_of(:email).case_insensitive }
|
|
456
|
+
it { should validate_presence_of(:name) }
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
describe 'associations' do
|
|
460
|
+
it { should have_many(:memberships).dependent(:destroy) }
|
|
461
|
+
it { should have_many(:organizations).through(:memberships) }
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
describe 'scopes' do
|
|
465
|
+
describe '.active' do
|
|
466
|
+
it 'returns only active users' do
|
|
467
|
+
active_user = create(:user, is_active: true)
|
|
468
|
+
inactive_user = create(:user, is_active: false)
|
|
469
|
+
|
|
470
|
+
expect(User.active).to include(active_user)
|
|
471
|
+
expect(User.active).not_to include(inactive_user)
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
describe '.search' do
|
|
476
|
+
it 'searches by name and email' do
|
|
477
|
+
user = create(:user, name: 'John Doe', email: 'john@example.com')
|
|
478
|
+
|
|
479
|
+
expect(User.search('john')).to include(user)
|
|
480
|
+
expect(User.search('jane')).not_to include(user)
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
describe '#admin?' do
|
|
486
|
+
it 'returns true for admin users' do
|
|
487
|
+
admin = build(:user, role: :admin)
|
|
488
|
+
expect(admin.admin?).to be true
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
# spec/requests/api/v1/users_spec.rb
|
|
495
|
+
require 'rails_helper'
|
|
496
|
+
|
|
497
|
+
RSpec.describe 'Api::V1::Users', type: :request do
|
|
498
|
+
let(:admin) { create(:user, role: :admin) }
|
|
499
|
+
let(:auth_headers) { { 'Authorization' => "Bearer #{JsonWebToken.encode(user_id: admin.id)}" } }
|
|
500
|
+
|
|
501
|
+
describe 'GET /api/v1/users' do
|
|
502
|
+
before { create_list(:user, 5) }
|
|
503
|
+
|
|
504
|
+
it 'returns paginated users for admin' do
|
|
505
|
+
get '/api/v1/users', headers: auth_headers
|
|
506
|
+
|
|
507
|
+
expect(response).to have_http_status(:ok)
|
|
508
|
+
expect(json_response['data']).to be_an(Array)
|
|
509
|
+
expect(json_response['pagination']).to include('current_page', 'total')
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
it 'returns 403 for non-admin' do
|
|
513
|
+
user = create(:user)
|
|
514
|
+
headers = { 'Authorization' => "Bearer #{JsonWebToken.encode(user_id: user.id)}" }
|
|
515
|
+
|
|
516
|
+
get '/api/v1/users', headers: headers
|
|
517
|
+
|
|
518
|
+
expect(response).to have_http_status(:forbidden)
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
describe 'POST /api/v1/users' do
|
|
523
|
+
let(:valid_params) { { user: { name: 'New User', email: 'new@example.com', password: 'SecurePass123!' } } }
|
|
524
|
+
|
|
525
|
+
it 'creates a new user' do
|
|
526
|
+
expect {
|
|
527
|
+
post '/api/v1/users', params: valid_params, headers: auth_headers
|
|
528
|
+
}.to change(User, :count).by(1)
|
|
529
|
+
|
|
530
|
+
expect(response).to have_http_status(:created)
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def json_response
|
|
535
|
+
JSON.parse(response.body)
|
|
536
|
+
end
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
# spec/factories/users.rb
|
|
541
|
+
FactoryBot.define do
|
|
542
|
+
factory :user do
|
|
543
|
+
sequence(:email) { |n| "user#{n}@example.com" }
|
|
544
|
+
name { Faker::Name.name }
|
|
545
|
+
password { 'Password123!' }
|
|
546
|
+
role { :user }
|
|
547
|
+
is_active { true }
|
|
548
|
+
|
|
549
|
+
trait :admin do
|
|
550
|
+
role { :admin }
|
|
551
|
+
end
|
|
552
|
+
end
|
|
553
|
+
end
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
## Use Cases
|
|
557
|
+
|
|
558
|
+
### API Rate Limiting
|
|
559
|
+
|
|
560
|
+
```ruby
|
|
561
|
+
# config/initializers/rack_attack.rb
|
|
562
|
+
class Rack::Attack
|
|
563
|
+
throttle('requests by ip', limit: 100, period: 1.minute) do |request|
|
|
564
|
+
request.ip
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
throttle('login attempts', limit: 5, period: 1.minute) do |request|
|
|
568
|
+
if request.path == '/api/v1/auth/login' && request.post?
|
|
569
|
+
request.ip
|
|
570
|
+
end
|
|
571
|
+
end
|
|
572
|
+
end
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
### Caching with Redis
|
|
576
|
+
|
|
577
|
+
```ruby
|
|
578
|
+
# app/controllers/api/v1/organizations_controller.rb
|
|
579
|
+
def show
|
|
580
|
+
@organization = Rails.cache.fetch("organization:#{params[:id]}", expires_in: 5.minutes) do
|
|
581
|
+
Organization.includes(:owner, :members).find(params[:id])
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
render json: OrganizationSerializer.new(@organization).serializable_hash
|
|
49
585
|
end
|
|
50
586
|
```
|
|
51
587
|
|
|
52
588
|
## Best Practices
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
- Use
|
|
589
|
+
|
|
590
|
+
### Do's
|
|
591
|
+
|
|
592
|
+
- Use UUID primary keys for public APIs
|
|
593
|
+
- Use service objects for business logic
|
|
594
|
+
- Use serializers for consistent responses
|
|
595
|
+
- Use concerns for shared behavior
|
|
596
|
+
- Use scopes for reusable queries
|
|
597
|
+
- Use background jobs for heavy operations
|
|
598
|
+
- Use strong parameters
|
|
599
|
+
- Write comprehensive tests with RSpec
|
|
600
|
+
- Use database indexes for performance
|
|
601
|
+
- Use soft deletes for important data
|
|
602
|
+
|
|
603
|
+
### Don'ts
|
|
604
|
+
|
|
605
|
+
- Don't put business logic in controllers
|
|
606
|
+
- Don't use N+1 queries
|
|
607
|
+
- Don't skip validations
|
|
608
|
+
- Don't ignore security headers
|
|
609
|
+
- Don't expose internal errors
|
|
610
|
+
- Don't use callbacks for business logic
|
|
611
|
+
- Don't skip authentication
|
|
612
|
+
- Don't ignore test coverage
|
|
613
|
+
- Don't use sync operations for heavy tasks
|
|
614
|
+
- Don't forget rate limiting
|
|
615
|
+
|
|
616
|
+
## References
|
|
617
|
+
|
|
618
|
+
- [Rails Guides](https://guides.rubyonrails.org/)
|
|
619
|
+
- [RSpec Documentation](https://rspec.info/)
|
|
620
|
+
- [Rails API Best Practices](https://github.com/rubocop/rubocop-rails)
|
|
621
|
+
- [JSON:API Serializer](https://github.com/jsonapi-serializer/jsonapi-serializer)
|
|
622
|
+
- [Sidekiq Documentation](https://github.com/mperham/sidekiq)
|