agent-notes 2.0.4__py3-none-any.whl
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.
- agent_notes/VERSION +1 -0
- agent_notes/__init__.py +1 -0
- agent_notes/__main__.py +4 -0
- agent_notes/cli.py +348 -0
- agent_notes/commands/__init__.py +27 -0
- agent_notes/commands/_install_helpers.py +262 -0
- agent_notes/commands/build.py +170 -0
- agent_notes/commands/doctor.py +112 -0
- agent_notes/commands/info.py +95 -0
- agent_notes/commands/install.py +99 -0
- agent_notes/commands/list.py +169 -0
- agent_notes/commands/memory.py +430 -0
- agent_notes/commands/regenerate.py +152 -0
- agent_notes/commands/set_role.py +143 -0
- agent_notes/commands/uninstall.py +26 -0
- agent_notes/commands/update.py +169 -0
- agent_notes/commands/validate.py +199 -0
- agent_notes/commands/wizard.py +720 -0
- agent_notes/config.py +154 -0
- agent_notes/data/agents/agents.yaml +352 -0
- agent_notes/data/agents/analyst.md +45 -0
- agent_notes/data/agents/api-reviewer.md +47 -0
- agent_notes/data/agents/architect.md +46 -0
- agent_notes/data/agents/coder.md +28 -0
- agent_notes/data/agents/database-specialist.md +45 -0
- agent_notes/data/agents/debugger.md +47 -0
- agent_notes/data/agents/devil.md +47 -0
- agent_notes/data/agents/devops.md +38 -0
- agent_notes/data/agents/explorer.md +23 -0
- agent_notes/data/agents/integrations.md +44 -0
- agent_notes/data/agents/lead.md +216 -0
- agent_notes/data/agents/performance-profiler.md +44 -0
- agent_notes/data/agents/refactorer.md +48 -0
- agent_notes/data/agents/reviewer.md +44 -0
- agent_notes/data/agents/security-auditor.md +44 -0
- agent_notes/data/agents/system-auditor.md +38 -0
- agent_notes/data/agents/tech-writer.md +32 -0
- agent_notes/data/agents/test-runner.md +36 -0
- agent_notes/data/agents/test-writer.md +39 -0
- agent_notes/data/cli/claude.yaml +25 -0
- agent_notes/data/cli/copilot.yaml +18 -0
- agent_notes/data/cli/opencode.yaml +22 -0
- agent_notes/data/commands/brainstorm.md +8 -0
- agent_notes/data/commands/debug.md +9 -0
- agent_notes/data/commands/review.md +10 -0
- agent_notes/data/global-claude.md +290 -0
- agent_notes/data/global-copilot.md +27 -0
- agent_notes/data/global-opencode.md +40 -0
- agent_notes/data/hooks/session-context.md.tpl +19 -0
- agent_notes/data/models/claude-haiku-4-5.yaml +15 -0
- agent_notes/data/models/claude-opus-4-1.yaml +16 -0
- agent_notes/data/models/claude-opus-4-5.yaml +16 -0
- agent_notes/data/models/claude-opus-4-6.yaml +16 -0
- agent_notes/data/models/claude-opus-4-7.yaml +15 -0
- agent_notes/data/models/claude-sonnet-4-5.yaml +16 -0
- agent_notes/data/models/claude-sonnet-4-6.yaml +15 -0
- agent_notes/data/models/claude-sonnet-4.yaml +16 -0
- agent_notes/data/pricing.yaml +33 -0
- agent_notes/data/roles/orchestrator.yaml +5 -0
- agent_notes/data/roles/reasoner.yaml +5 -0
- agent_notes/data/roles/scout.yaml +5 -0
- agent_notes/data/roles/worker.yaml +5 -0
- agent_notes/data/rules/code-quality.md +9 -0
- agent_notes/data/rules/safety.md +10 -0
- agent_notes/data/scripts/cost-report +211 -0
- agent_notes/data/skills/brainstorming/SKILL.md +57 -0
- agent_notes/data/skills/code-review/SKILL.md +64 -0
- agent_notes/data/skills/debugging-protocol/SKILL.md +51 -0
- agent_notes/data/skills/docker-compose/SKILL.md +318 -0
- agent_notes/data/skills/docker-compose-advanced/SKILL.md +575 -0
- agent_notes/data/skills/docker-dockerfile/SKILL.md +385 -0
- agent_notes/data/skills/docker-dockerfile-languages/SKILL.md +293 -0
- agent_notes/data/skills/git/SKILL.md +87 -0
- agent_notes/data/skills/rails-active-storage/SKILL.md +321 -0
- agent_notes/data/skills/rails-broadcasting/SKILL.md +374 -0
- agent_notes/data/skills/rails-concerns/SKILL.md +806 -0
- agent_notes/data/skills/rails-controllers/SKILL.md +510 -0
- agent_notes/data/skills/rails-controllers-advanced/SKILL.md +441 -0
- agent_notes/data/skills/rails-helpers/SKILL.md +677 -0
- agent_notes/data/skills/rails-initializers/SKILL.md +79 -0
- agent_notes/data/skills/rails-javascript/SKILL.md +567 -0
- agent_notes/data/skills/rails-jobs/SKILL.md +700 -0
- agent_notes/data/skills/rails-kamal/SKILL.md +483 -0
- agent_notes/data/skills/rails-lib/SKILL.md +101 -0
- agent_notes/data/skills/rails-mailers/SKILL.md +321 -0
- agent_notes/data/skills/rails-migrations/SKILL.md +268 -0
- agent_notes/data/skills/rails-models/SKILL.md +459 -0
- agent_notes/data/skills/rails-models-advanced/SKILL.md +398 -0
- agent_notes/data/skills/rails-routes/SKILL.md +804 -0
- agent_notes/data/skills/rails-style/SKILL.md +538 -0
- agent_notes/data/skills/rails-testing-controllers/SKILL.md +343 -0
- agent_notes/data/skills/rails-testing-models/SKILL.md +296 -0
- agent_notes/data/skills/rails-testing-system/SKILL.md +375 -0
- agent_notes/data/skills/rails-validations/SKILL.md +108 -0
- agent_notes/data/skills/rails-view-components/SKILL.md +511 -0
- agent_notes/data/skills/rails-view-components-advanced/SKILL.md +376 -0
- agent_notes/data/skills/rails-views/SKILL.md +413 -0
- agent_notes/data/skills/rails-views-advanced/SKILL.md +450 -0
- agent_notes/data/skills/refactoring-protocol/SKILL.md +64 -0
- agent_notes/data/skills/tdd/SKILL.md +57 -0
- agent_notes/data/templates/__init__.py +1 -0
- agent_notes/data/templates/__pycache__/__init__.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__init__.py +1 -0
- agent_notes/data/templates/frontmatter/__pycache__/__init__.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__pycache__/claude.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__pycache__/cursor.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__pycache__/opencode.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/claude.py +44 -0
- agent_notes/data/templates/frontmatter/opencode.py +104 -0
- agent_notes/doctor_checks.py +189 -0
- agent_notes/domain/__init__.py +17 -0
- agent_notes/domain/agent.py +34 -0
- agent_notes/domain/cli_backend.py +40 -0
- agent_notes/domain/diagnostics.py +29 -0
- agent_notes/domain/diff.py +44 -0
- agent_notes/domain/model.py +27 -0
- agent_notes/domain/role.py +13 -0
- agent_notes/domain/rule.py +13 -0
- agent_notes/domain/skill.py +15 -0
- agent_notes/domain/state.py +46 -0
- agent_notes/install_state.py +11 -0
- agent_notes/registries/__init__.py +16 -0
- agent_notes/registries/_base.py +46 -0
- agent_notes/registries/agent_registry.py +107 -0
- agent_notes/registries/cli_registry.py +89 -0
- agent_notes/registries/model_registry.py +85 -0
- agent_notes/registries/role_registry.py +64 -0
- agent_notes/registries/rule_registry.py +80 -0
- agent_notes/registries/skill_registry.py +141 -0
- agent_notes/services/__init__.py +8 -0
- agent_notes/services/diagnostics/__init__.py +47 -0
- agent_notes/services/diagnostics/_checks.py +272 -0
- agent_notes/services/diagnostics/_display.py +346 -0
- agent_notes/services/diagnostics/_fix.py +169 -0
- agent_notes/services/diff.py +349 -0
- agent_notes/services/fs.py +195 -0
- agent_notes/services/install_state_builder.py +210 -0
- agent_notes/services/installer.py +293 -0
- agent_notes/services/memory_backend.py +155 -0
- agent_notes/services/rendering.py +329 -0
- agent_notes/services/session_context.py +23 -0
- agent_notes/services/settings_writer.py +79 -0
- agent_notes/services/state_store.py +249 -0
- agent_notes/services/ui.py +419 -0
- agent_notes/services/user_config.py +62 -0
- agent_notes/services/validation.py +67 -0
- agent_notes/state.py +21 -0
- agent_notes-2.0.4.dist-info/METADATA +14 -0
- agent_notes-2.0.4.dist-info/RECORD +162 -0
- agent_notes-2.0.4.dist-info/WHEEL +5 -0
- agent_notes-2.0.4.dist-info/entry_points.txt +2 -0
- agent_notes-2.0.4.dist-info/licenses/LICENSE +21 -0
- agent_notes-2.0.4.dist-info/top_level.txt +2 -0
- tests/conftest.py +20 -0
- tests/functional/__init__.py +0 -0
- tests/functional/test_build_commands.py +88 -0
- tests/functional/test_registries.py +128 -0
- tests/integration/__init__.py +0 -0
- tests/integration/test_build_output.py +129 -0
- tests/plugins/__init__.py +0 -0
- tests/plugins/test_agents.py +93 -0
- tests/plugins/test_skills.py +77 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rails-mailers
|
|
3
|
+
description: "Rails Action Mailer: templates, layouts, attachments, previews, and testing"
|
|
4
|
+
group: rails
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Mailers Guide
|
|
8
|
+
|
|
9
|
+
Comprehensive guide for Rails Action Mailer.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Philosophy
|
|
14
|
+
|
|
15
|
+
1. **Mailers are like controllers** - Thin, delegate to models
|
|
16
|
+
2. **Preview mailers in development** - Use mailer previews
|
|
17
|
+
3. **Test email delivery** - Test content, not delivery mechanism
|
|
18
|
+
4. **Layouts for consistency** - DRY up email HTML
|
|
19
|
+
5. **Plain text + HTML** - Always provide both formats
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## File Structure
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
app/mailers/
|
|
27
|
+
├── application_mailer.rb
|
|
28
|
+
├── user_mailer.rb
|
|
29
|
+
├── notification_mailer.rb
|
|
30
|
+
└── magic_link_mailer.rb
|
|
31
|
+
|
|
32
|
+
app/views/
|
|
33
|
+
├── layouts/
|
|
34
|
+
│ └── mailer.html.erb
|
|
35
|
+
└── user_mailer/
|
|
36
|
+
├── welcome.html.erb
|
|
37
|
+
└── welcome.text.erb
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Basic Mailer
|
|
43
|
+
|
|
44
|
+
### Application Mailer
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
# app/mailers/application_mailer.rb
|
|
48
|
+
class ApplicationMailer < ActionMailer::Base
|
|
49
|
+
default from: ENV.fetch("MAILER_FROM_ADDRESS", "App <noreply@example.com>")
|
|
50
|
+
|
|
51
|
+
layout "mailer"
|
|
52
|
+
|
|
53
|
+
helper ApplicationHelper, UsersHelper
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
def default_url_options
|
|
57
|
+
if Current.account
|
|
58
|
+
super.merge(script_name: Current.account.slug)
|
|
59
|
+
else
|
|
60
|
+
super
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Simple Mailer
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
# app/mailers/user_mailer.rb
|
|
70
|
+
class UserMailer < ApplicationMailer
|
|
71
|
+
def welcome(user)
|
|
72
|
+
@user = user
|
|
73
|
+
@login_url = new_session_url
|
|
74
|
+
|
|
75
|
+
mail to: @user.email, subject: "Welcome to #{app_name}!"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def password_reset(user, token)
|
|
79
|
+
@user = user
|
|
80
|
+
@token = token
|
|
81
|
+
@reset_url = edit_password_url(token: @token)
|
|
82
|
+
|
|
83
|
+
mail to: @user.email, subject: "Reset your password"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
def app_name
|
|
88
|
+
"My App"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Templates
|
|
96
|
+
|
|
97
|
+
### HTML Template
|
|
98
|
+
|
|
99
|
+
```erb
|
|
100
|
+
<%# app/views/user_mailer/welcome.html.erb %>
|
|
101
|
+
|
|
102
|
+
<h1>Welcome, <%= @user.name %>!</h1>
|
|
103
|
+
|
|
104
|
+
<p>Thanks for signing up. We're excited to have you on board.</p>
|
|
105
|
+
|
|
106
|
+
<p>
|
|
107
|
+
<%= link_to "Get Started", @login_url, class: "button" %>
|
|
108
|
+
</p>
|
|
109
|
+
|
|
110
|
+
<p>
|
|
111
|
+
If you have any questions, just reply to this email.
|
|
112
|
+
</p>
|
|
113
|
+
|
|
114
|
+
<p>
|
|
115
|
+
Thanks,<br>
|
|
116
|
+
The Team
|
|
117
|
+
</p>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Plain Text Template
|
|
121
|
+
|
|
122
|
+
```erb
|
|
123
|
+
<%# app/views/user_mailer/welcome.text.erb %>
|
|
124
|
+
|
|
125
|
+
Welcome, <%= @user.name %>!
|
|
126
|
+
|
|
127
|
+
Thanks for signing up. We're excited to have you on board.
|
|
128
|
+
|
|
129
|
+
Get started: <%= @login_url %>
|
|
130
|
+
|
|
131
|
+
If you have any questions, just reply to this email.
|
|
132
|
+
|
|
133
|
+
Thanks,
|
|
134
|
+
The Team
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Mailer Layout
|
|
138
|
+
|
|
139
|
+
```erb
|
|
140
|
+
<%# app/views/layouts/mailer.html.erb %>
|
|
141
|
+
|
|
142
|
+
<!DOCTYPE html>
|
|
143
|
+
<html>
|
|
144
|
+
<head>
|
|
145
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
146
|
+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
147
|
+
<style>
|
|
148
|
+
body { font-family: Arial, sans-serif; line-height: 1.6; }
|
|
149
|
+
.button { background: #007bff; color: white; padding: 10px 20px; text-decoration: none; }
|
|
150
|
+
</style>
|
|
151
|
+
</head>
|
|
152
|
+
<body>
|
|
153
|
+
<%= yield %>
|
|
154
|
+
|
|
155
|
+
<hr>
|
|
156
|
+
<p style="font-size: 12px; color: #666;">
|
|
157
|
+
You're receiving this email because you have an account at My App.
|
|
158
|
+
</p>
|
|
159
|
+
</body>
|
|
160
|
+
</html>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Mailer Methods
|
|
166
|
+
|
|
167
|
+
### With Attachments
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
def invoice(user, invoice)
|
|
171
|
+
@user = user
|
|
172
|
+
@invoice = invoice
|
|
173
|
+
|
|
174
|
+
attachments["invoice-#{@invoice.id}.pdf"] = @invoice.to_pdf
|
|
175
|
+
attachments.inline["logo.png"] = File.read(Rails.root.join("app/assets/images/logo.png"))
|
|
176
|
+
|
|
177
|
+
mail to: @user.email, subject: "Invoice ##{@invoice.id}"
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### With CC/BCC
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
def notification(user, admin)
|
|
185
|
+
@user = user
|
|
186
|
+
|
|
187
|
+
mail(
|
|
188
|
+
to: @user.email,
|
|
189
|
+
cc: admin.email,
|
|
190
|
+
bcc: "notifications@example.com",
|
|
191
|
+
subject: "Important Update"
|
|
192
|
+
)
|
|
193
|
+
end
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### With Custom Headers
|
|
197
|
+
|
|
198
|
+
```ruby
|
|
199
|
+
def custom_email(user)
|
|
200
|
+
@user = user
|
|
201
|
+
|
|
202
|
+
headers["X-Priority"] = "1"
|
|
203
|
+
headers["X-Mailer"] = "MyApp Mailer"
|
|
204
|
+
|
|
205
|
+
mail to: @user.email, subject: "Custom Email"
|
|
206
|
+
end
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Sending Emails
|
|
212
|
+
|
|
213
|
+
### Immediate Delivery
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
# Sends immediately
|
|
217
|
+
UserMailer.welcome(user).deliver_now
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Background Delivery
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
# Enqueues job
|
|
224
|
+
UserMailer.welcome(user).deliver_later
|
|
225
|
+
|
|
226
|
+
# With delay
|
|
227
|
+
UserMailer.welcome(user).deliver_later(wait: 1.hour)
|
|
228
|
+
|
|
229
|
+
# At specific time
|
|
230
|
+
UserMailer.welcome(user).deliver_later(wait_until: Time.current + 2.hours)
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### From Models
|
|
234
|
+
|
|
235
|
+
```ruby
|
|
236
|
+
class User < ApplicationRecord
|
|
237
|
+
after_create :send_welcome_email
|
|
238
|
+
|
|
239
|
+
private
|
|
240
|
+
def send_welcome_email
|
|
241
|
+
UserMailer.welcome(self).deliver_later
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Mailer Previews
|
|
249
|
+
|
|
250
|
+
### Preview Class
|
|
251
|
+
|
|
252
|
+
```ruby
|
|
253
|
+
# test/mailers/previews/user_mailer_preview.rb
|
|
254
|
+
class UserMailerPreview < ActionMailer::Preview
|
|
255
|
+
def welcome
|
|
256
|
+
UserMailer.welcome(User.first)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def password_reset
|
|
260
|
+
user = User.first
|
|
261
|
+
token = "sample-token-123"
|
|
262
|
+
UserMailer.password_reset(user, token)
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Visit: http://localhost:3000/rails/mailers
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Testing Mailers
|
|
272
|
+
|
|
273
|
+
### Basic Mailer Test
|
|
274
|
+
|
|
275
|
+
```ruby
|
|
276
|
+
class UserMailerTest < ActionMailer::TestCase
|
|
277
|
+
test "welcome email" do
|
|
278
|
+
user = users(:david)
|
|
279
|
+
|
|
280
|
+
email = UserMailer.welcome(user)
|
|
281
|
+
|
|
282
|
+
assert_emails 1 do
|
|
283
|
+
email.deliver_now
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
assert_equal [user.email], email.to
|
|
287
|
+
assert_equal "Welcome to My App!", email.subject
|
|
288
|
+
assert_match user.name, email.body.encoded
|
|
289
|
+
assert_match "Get Started", email.body.encoded
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
test "welcome email includes login URL" do
|
|
293
|
+
user = users(:david)
|
|
294
|
+
|
|
295
|
+
email = UserMailer.welcome(user)
|
|
296
|
+
|
|
297
|
+
assert_match new_session_url, email.body.encoded
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
test "email has both HTML and text parts" do
|
|
301
|
+
user = users(:david)
|
|
302
|
+
|
|
303
|
+
email = UserMailer.welcome(user)
|
|
304
|
+
|
|
305
|
+
assert_equal 2, email.parts.size
|
|
306
|
+
assert_equal "text/plain", email.text_part.content_type
|
|
307
|
+
assert_equal "text/html", email.html_part.content_type
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Summary
|
|
315
|
+
|
|
316
|
+
- **Structure**: Like controllers, thin and focused
|
|
317
|
+
- **Templates**: HTML + plain text versions
|
|
318
|
+
- **Layouts**: DRY up common email structure
|
|
319
|
+
- **Delivery**: Immediate or background (deliver_later)
|
|
320
|
+
- **Testing**: Test content, recipients, attachments
|
|
321
|
+
- **Previews**: Preview emails in development
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rails-migrations
|
|
3
|
+
description: "Rails database migrations: tables, columns, indexes, foreign keys, and best practices"
|
|
4
|
+
group: rails
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Migrations Guide
|
|
8
|
+
|
|
9
|
+
Comprehensive guide for Rails database migrations.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## File Structure
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
db/migrate/
|
|
17
|
+
├── 20250101120000_create_cards.rb
|
|
18
|
+
├── 20250102130000_add_status_to_cards.rb
|
|
19
|
+
└── 20250103140000_create_index_on_cards_board_id.rb
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Basic Migration Patterns
|
|
25
|
+
|
|
26
|
+
### Create Table
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
class CreateCards < ActiveRecord::Migration[8.0]
|
|
30
|
+
def change
|
|
31
|
+
create_table :cards, id: :uuid do |t|
|
|
32
|
+
t.uuid :account_id, null: false
|
|
33
|
+
t.uuid :board_id, null: false
|
|
34
|
+
t.uuid :creator_id, null: false
|
|
35
|
+
|
|
36
|
+
t.integer :number, null: false
|
|
37
|
+
t.string :title
|
|
38
|
+
t.text :description
|
|
39
|
+
t.string :status, default: "draft", null: false
|
|
40
|
+
t.string :color
|
|
41
|
+
|
|
42
|
+
t.timestamps
|
|
43
|
+
|
|
44
|
+
t.index ["account_id", "number"], unique: true
|
|
45
|
+
t.index ["board_id"]
|
|
46
|
+
t.index ["creator_id"]
|
|
47
|
+
t.index ["status"]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Add Column
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
class AddPublishedAtToCards < ActiveRecord::Migration[8.0]
|
|
57
|
+
def change
|
|
58
|
+
add_column :cards, :published_at, :datetime
|
|
59
|
+
add_index :cards, :published_at
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Remove Column
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
class RemoveDeprecatedFieldFromCards < ActiveRecord::Migration[8.0]
|
|
68
|
+
def change
|
|
69
|
+
remove_column :cards, :deprecated_field, :string
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Change Column
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
class ChangeCardsTitleToText < ActiveRecord::Migration[8.0]
|
|
78
|
+
def change
|
|
79
|
+
change_column :cards, :title, :text
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Or with up/down for safety
|
|
84
|
+
class ChangeCardsTitleToText < ActiveRecord::Migration[8.0]
|
|
85
|
+
def up
|
|
86
|
+
change_column :cards, :title, :text
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def down
|
|
90
|
+
change_column :cards, :title, :string
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Rename Column
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
class RenameCardsDescToDescription < ActiveRecord::Migration[8.0]
|
|
99
|
+
def change
|
|
100
|
+
rename_column :cards, :desc, :description
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Index Patterns
|
|
108
|
+
|
|
109
|
+
### Single Column Index
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
add_index :cards, :board_id
|
|
113
|
+
add_index :cards, :status
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Composite Index
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
add_index :cards, [:account_id, :number], unique: true
|
|
120
|
+
add_index :cards, [:board_id, :status]
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Named Index
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
add_index :cards, :email, unique: true, name: "idx_cards_on_email"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Partial Index (PostgreSQL)
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
add_index :cards, :published_at, where: "status = 'published'"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Remove Index
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
remove_index :cards, :board_id
|
|
139
|
+
remove_index :cards, name: "idx_cards_on_email"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Foreign Keys
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
class AddForeignKeys < ActiveRecord::Migration[8.0]
|
|
148
|
+
def change
|
|
149
|
+
add_foreign_key :cards, :boards
|
|
150
|
+
add_foreign_key :cards, :accounts
|
|
151
|
+
add_foreign_key :cards, :users, column: :creator_id
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Data Migrations
|
|
159
|
+
|
|
160
|
+
### Backfill Data
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
class BackfillCardNumbers < ActiveRecord::Migration[8.0]
|
|
164
|
+
def up
|
|
165
|
+
Card.where(number: nil).find_each do |card|
|
|
166
|
+
card.update!(number: card.account.increment!(:cards_count).cards_count)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def down
|
|
171
|
+
# Usually no rollback for data migrations
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## UUID Primary Keys
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
class EnableUuidExtension < ActiveRecord::Migration[8.0]
|
|
182
|
+
def change
|
|
183
|
+
enable_extension "pgcrypto" # PostgreSQL
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
class CreateCardsWithUuid < ActiveRecord::Migration[8.0]
|
|
188
|
+
def change
|
|
189
|
+
create_table :cards, id: :uuid do |t|
|
|
190
|
+
t.uuid :account_id, null: false
|
|
191
|
+
t.timestamps
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Reversible Migrations
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
class AddCheckConstraint < ActiveRecord::Migration[8.0]
|
|
203
|
+
def change
|
|
204
|
+
reversible do |dir|
|
|
205
|
+
dir.up do
|
|
206
|
+
execute <<-SQL
|
|
207
|
+
ALTER TABLE cards
|
|
208
|
+
ADD CONSTRAINT check_positive_number
|
|
209
|
+
CHECK (number > 0)
|
|
210
|
+
SQL
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
dir.down do
|
|
214
|
+
execute <<-SQL
|
|
215
|
+
ALTER TABLE cards
|
|
216
|
+
DROP CONSTRAINT check_positive_number
|
|
217
|
+
SQL
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Best Practices
|
|
227
|
+
|
|
228
|
+
### ✅ DO
|
|
229
|
+
|
|
230
|
+
1. **Add indexes for foreign keys**
|
|
231
|
+
```ruby
|
|
232
|
+
t.uuid :board_id, null: false
|
|
233
|
+
t.index [:board_id]
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
2. **Use null constraints**
|
|
237
|
+
```ruby
|
|
238
|
+
t.string :title, null: false
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
3. **Add default values**
|
|
242
|
+
```ruby
|
|
243
|
+
t.string :status, default: "draft", null: false
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
4. **Use change when possible**
|
|
247
|
+
```ruby
|
|
248
|
+
def change
|
|
249
|
+
add_column :cards, :color, :string
|
|
250
|
+
end
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### ❌ DON'T
|
|
254
|
+
|
|
255
|
+
1. **Modify old migrations** - Create new ones
|
|
256
|
+
2. **Remove columns in production without deprecation**
|
|
257
|
+
3. **Change column types without considering data loss**
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Summary
|
|
262
|
+
|
|
263
|
+
- **Versioned**: Each migration is timestamped
|
|
264
|
+
- **Reversible**: Use `change` when possible
|
|
265
|
+
- **Indexes**: Add for foreign keys and frequently queried columns
|
|
266
|
+
- **Constraints**: Use null, default, unique appropriately
|
|
267
|
+
- **Data Migrations**: Separate from schema migrations
|
|
268
|
+
- **UUID Keys**: Use for distributed systems
|