anima-core 0.2.1 → 1.0.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/.reek.yml +27 -1
- data/CHANGELOG.md +19 -0
- data/README.md +213 -43
- data/agents/codebase-analyzer.md +88 -0
- data/agents/codebase-pattern-finder.md +83 -0
- data/agents/documentation-researcher.md +59 -0
- data/agents/thoughts-analyzer.md +102 -0
- data/agents/web-search-researcher.md +71 -0
- data/anima-core.gemspec +3 -0
- data/app/channels/session_channel.rb +195 -45
- data/app/decorators/user_message_decorator.rb +16 -5
- data/app/jobs/agent_request_job.rb +55 -2
- data/app/jobs/analytical_brain_job.rb +33 -0
- data/app/jobs/count_event_tokens_job.rb +15 -4
- data/app/models/concerns/event/broadcasting.rb +81 -0
- data/app/models/event.rb +20 -1
- data/app/models/goal.rb +91 -0
- data/app/models/session.rb +366 -21
- data/config/application.rb +2 -0
- data/config/initializers/event_subscribers.rb +0 -1
- data/config/routes.rb +0 -6
- data/db/migrate/20260313010000_add_status_to_events.rb +8 -0
- data/db/migrate/20260313020000_add_processing_to_sessions.rb +7 -0
- data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
- data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
- data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
- data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
- data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
- data/db/migrate/20260315140843_create_goals.rb +16 -0
- data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
- data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
- data/lib/agent_loop.rb +65 -6
- data/lib/agents/definition.rb +116 -0
- data/lib/agents/registry.rb +106 -0
- data/lib/analytical_brain/runner.rb +276 -0
- data/lib/analytical_brain/tools/activate_skill.rb +52 -0
- data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
- data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
- data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
- data/lib/analytical_brain/tools/finish_goal.rb +62 -0
- data/lib/analytical_brain/tools/read_workflow.rb +58 -0
- data/lib/analytical_brain/tools/rename_session.rb +63 -0
- data/lib/analytical_brain/tools/set_goal.rb +60 -0
- data/lib/analytical_brain/tools/update_goal.rb +60 -0
- data/lib/analytical_brain.rb +23 -0
- data/lib/anima/cli/mcp/secrets.rb +76 -0
- data/lib/anima/cli/mcp.rb +197 -0
- data/lib/anima/cli.rb +5 -40
- data/lib/anima/installer.rb +168 -0
- data/lib/anima/settings.rb +226 -0
- data/lib/anima/version.rb +1 -1
- data/lib/anima.rb +9 -0
- data/lib/credential_store.rb +103 -0
- data/lib/environment_probe.rb +232 -0
- data/lib/events/subscribers/persister.rb +1 -0
- data/lib/events/user_message.rb +17 -0
- data/lib/llm/client.rb +29 -10
- data/lib/mcp/client_manager.rb +86 -0
- data/lib/mcp/config.rb +213 -0
- data/lib/mcp/health_check.rb +77 -0
- data/lib/mcp/secrets.rb +73 -0
- data/lib/mcp/stdio_transport.rb +206 -0
- data/lib/providers/anthropic.rb +11 -20
- data/lib/shell_session.rb +11 -10
- data/lib/skills/definition.rb +97 -0
- data/lib/skills/registry.rb +105 -0
- data/lib/tools/edit.rb +226 -0
- data/lib/tools/mcp_tool.rb +114 -0
- data/lib/tools/read.rb +151 -0
- data/lib/tools/registry.rb +14 -12
- data/lib/tools/request_feature.rb +121 -0
- data/lib/tools/return_result.rb +81 -0
- data/lib/tools/spawn_specialist.rb +109 -0
- data/lib/tools/spawn_subagent.rb +111 -0
- data/lib/tools/subagent_prompts.rb +12 -0
- data/lib/tools/web_get.rb +8 -9
- data/lib/tools/write.rb +86 -0
- data/lib/tui/app.rb +985 -26
- data/lib/tui/cable_client.rb +69 -31
- data/lib/tui/message_store.rb +103 -8
- data/lib/tui/screens/chat.rb +293 -45
- data/lib/workflows/definition.rb +97 -0
- data/lib/workflows/registry.rb +89 -0
- data/skills/activerecord/SKILL.md +255 -0
- data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
- data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
- data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
- data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
- data/skills/activerecord/examples/associations/self_referential.rb +302 -0
- data/skills/activerecord/examples/associations/through_associations.rb +203 -0
- data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
- data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
- data/skills/activerecord/examples/basics/inheritance.rb +377 -0
- data/skills/activerecord/examples/basics/type_casting.rb +317 -0
- data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
- data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
- data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
- data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
- data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
- data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
- data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
- data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
- data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
- data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
- data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
- data/skills/activerecord/examples/querying/optimization.rb +275 -0
- data/skills/activerecord/examples/querying/scopes.rb +260 -0
- data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
- data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
- data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
- data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
- data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
- data/skills/activerecord/references/associations.md +709 -0
- data/skills/activerecord/references/basics.md +622 -0
- data/skills/activerecord/references/callbacks.md +738 -0
- data/skills/activerecord/references/migrations.md +657 -0
- data/skills/activerecord/references/querying.md +655 -0
- data/skills/activerecord/references/validations.md +596 -0
- data/skills/dragonruby/SKILL.md +250 -0
- data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
- data/skills/dragonruby/examples/audio/background_music.rb +29 -0
- data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
- data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
- data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
- data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
- data/skills/dragonruby/examples/core/hello_world.rb +24 -0
- data/skills/dragonruby/examples/core/labels.rb +22 -0
- data/skills/dragonruby/examples/core/sprites.rb +35 -0
- data/skills/dragonruby/examples/core/state_management.rb +29 -0
- data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
- data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
- data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
- data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
- data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
- data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
- data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
- data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
- data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
- data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
- data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
- data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
- data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
- data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
- data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
- data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
- data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
- data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
- data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
- data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
- data/skills/dragonruby/examples/input/controller_input.rb +28 -0
- data/skills/dragonruby/examples/input/directional_input.rb +24 -0
- data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
- data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
- data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
- data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
- data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
- data/skills/dragonruby/examples/rendering/labels.rb +32 -0
- data/skills/dragonruby/examples/rendering/layering.rb +51 -0
- data/skills/dragonruby/examples/rendering/solids.rb +61 -0
- data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
- data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
- data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
- data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
- data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
- data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
- data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
- data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
- data/skills/dragonruby/references/audio.md +396 -0
- data/skills/dragonruby/references/core.md +385 -0
- data/skills/dragonruby/references/distribution.md +434 -0
- data/skills/dragonruby/references/entities.md +516 -0
- data/skills/dragonruby/references/game-logic/persistence.md +386 -0
- data/skills/dragonruby/references/game-logic/state.md +389 -0
- data/skills/dragonruby/references/input.md +414 -0
- data/skills/dragonruby/references/rendering/animation.md +467 -0
- data/skills/dragonruby/references/rendering/primitives.md +403 -0
- data/skills/dragonruby/references/scenes.md +443 -0
- data/skills/draper-decorators/SKILL.md +344 -0
- data/skills/draper-decorators/examples/application_decorator.rb +61 -0
- data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
- data/skills/draper-decorators/examples/model_decorator.rb +152 -0
- data/skills/draper-decorators/references/anti-patterns.md +640 -0
- data/skills/draper-decorators/references/patterns.md +507 -0
- data/skills/draper-decorators/references/testing.md +559 -0
- data/skills/gh-issue.md +182 -0
- data/skills/mcp-server/SKILL.md +177 -0
- data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
- data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
- data/skills/mcp-server/examples/http_client.rb +48 -0
- data/skills/mcp-server/examples/http_server.rb +97 -0
- data/skills/mcp-server/examples/rails_integration.rb +88 -0
- data/skills/mcp-server/examples/stdio_server.rb +108 -0
- data/skills/mcp-server/examples/streaming_client.rb +95 -0
- data/skills/mcp-server/references/gotchas.md +183 -0
- data/skills/mcp-server/references/prompts.md +98 -0
- data/skills/mcp-server/references/resources.md +53 -0
- data/skills/mcp-server/references/server.md +140 -0
- data/skills/mcp-server/references/tools.md +146 -0
- data/skills/mcp-server/references/transport.md +104 -0
- data/skills/ratatui-ruby/SKILL.md +315 -0
- data/skills/ratatui-ruby/references/core-concepts.md +340 -0
- data/skills/ratatui-ruby/references/events.md +387 -0
- data/skills/ratatui-ruby/references/frameworks.md +522 -0
- data/skills/ratatui-ruby/references/layout.md +423 -0
- data/skills/ratatui-ruby/references/styling.md +268 -0
- data/skills/ratatui-ruby/references/testing.md +433 -0
- data/skills/ratatui-ruby/references/widgets.md +532 -0
- data/skills/rspec/SKILL.md +340 -0
- data/skills/rspec/examples/core/basic_structure.rb +69 -0
- data/skills/rspec/examples/core/configuration.rb +126 -0
- data/skills/rspec/examples/core/hooks.rb +126 -0
- data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
- data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
- data/skills/rspec/examples/core/shared_examples.rb +145 -0
- data/skills/rspec/examples/factory_bot/associations.rb +314 -0
- data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
- data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
- data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
- data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
- data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
- data/skills/rspec/examples/factory_bot/traits.rb +293 -0
- data/skills/rspec/examples/factory_bot/transients.rb +229 -0
- data/skills/rspec/examples/matchers/change.rb +115 -0
- data/skills/rspec/examples/matchers/collections.rb +154 -0
- data/skills/rspec/examples/matchers/comparisons.rb +79 -0
- data/skills/rspec/examples/matchers/composing.rb +155 -0
- data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
- data/skills/rspec/examples/matchers/equality.rb +58 -0
- data/skills/rspec/examples/matchers/errors.rb +136 -0
- data/skills/rspec/examples/matchers/output.rb +103 -0
- data/skills/rspec/examples/matchers/predicates.rb +87 -0
- data/skills/rspec/examples/matchers/truthiness.rb +101 -0
- data/skills/rspec/examples/matchers/types.rb +82 -0
- data/skills/rspec/examples/matchers/yield.rb +147 -0
- data/skills/rspec/examples/mocks/any_instance.rb +172 -0
- data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
- data/skills/rspec/examples/mocks/constants.rb +177 -0
- data/skills/rspec/examples/mocks/doubles.rb +139 -0
- data/skills/rspec/examples/mocks/expectations.rb +137 -0
- data/skills/rspec/examples/mocks/message_chains.rb +173 -0
- data/skills/rspec/examples/mocks/ordering.rb +144 -0
- data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
- data/skills/rspec/examples/mocks/responses.rb +223 -0
- data/skills/rspec/examples/mocks/spies.rb +149 -0
- data/skills/rspec/examples/mocks/stubbing.rb +133 -0
- data/skills/rspec/examples/rails/channels.rb +250 -0
- data/skills/rspec/examples/rails/controller_specs.rb +302 -0
- data/skills/rspec/examples/rails/helper_specs.rb +245 -0
- data/skills/rspec/examples/rails/job_specs.rb +256 -0
- data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
- data/skills/rspec/examples/rails/matchers.rb +374 -0
- data/skills/rspec/examples/rails/model_specs.rb +193 -0
- data/skills/rspec/examples/rails/request_specs.rb +275 -0
- data/skills/rspec/examples/rails/routing_specs.rb +276 -0
- data/skills/rspec/examples/rails/system_specs.rb +294 -0
- data/skills/rspec/examples/rails/transactions.rb +254 -0
- data/skills/rspec/examples/rails/view_specs.rb +252 -0
- data/skills/rspec/references/core.md +816 -0
- data/skills/rspec/references/factory_bot.md +641 -0
- data/skills/rspec/references/matchers.md +516 -0
- data/skills/rspec/references/mocks.md +381 -0
- data/skills/rspec/references/rails.md +528 -0
- data/templates/soul.md +40 -0
- data/workflows/commit.md +45 -0
- data/workflows/create_handoff.md +98 -0
- data/workflows/create_note.md +82 -0
- data/workflows/create_plan.md +457 -0
- data/workflows/decompose_ticket.md +109 -0
- data/workflows/feature.md +91 -0
- data/workflows/implement_plan.md +87 -0
- data/workflows/iterate_plan.md +247 -0
- data/workflows/research_codebase.md +210 -0
- data/workflows/resume_handoff.md +217 -0
- data/workflows/review_pr.md +320 -0
- data/workflows/thoughts_init.md +71 -0
- data/workflows/validate_plan.md +166 -0
- metadata +290 -3
- data/app/controllers/api/sessions_controller.rb +0 -25
- data/lib/events/subscribers/action_cable_bridge.rb +0 -59
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# Self-Referential Association Examples
|
|
2
|
+
# Model relates to itself for hierarchies and graphs
|
|
3
|
+
|
|
4
|
+
# ============================================
|
|
5
|
+
# Simple Hierarchy: Manager/Subordinates
|
|
6
|
+
# ============================================
|
|
7
|
+
|
|
8
|
+
class Employee < ApplicationRecord
|
|
9
|
+
# Employee has one manager (who is also an Employee)
|
|
10
|
+
belongs_to :manager,
|
|
11
|
+
class_name: "Employee",
|
|
12
|
+
optional: true,
|
|
13
|
+
inverse_of: :subordinates
|
|
14
|
+
|
|
15
|
+
# Employee has many subordinates (who are also Employees)
|
|
16
|
+
has_many :subordinates,
|
|
17
|
+
class_name: "Employee",
|
|
18
|
+
foreign_key: "manager_id",
|
|
19
|
+
dependent: :nullify,
|
|
20
|
+
inverse_of: :manager
|
|
21
|
+
|
|
22
|
+
validates :name, presence: true
|
|
23
|
+
|
|
24
|
+
# Convenience methods
|
|
25
|
+
def top_manager?
|
|
26
|
+
manager.nil?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def direct_reports_count
|
|
30
|
+
subordinates.count
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Migration
|
|
35
|
+
# create_table :employees do |t|
|
|
36
|
+
# t.string :name, null: false
|
|
37
|
+
# t.belongs_to :manager, foreign_key: { to_table: :employees }
|
|
38
|
+
# t.timestamps
|
|
39
|
+
# end
|
|
40
|
+
|
|
41
|
+
# Usage
|
|
42
|
+
ceo = Employee.create!(name: "CEO")
|
|
43
|
+
vp = Employee.create!(name: "VP Engineering", manager: ceo)
|
|
44
|
+
dev = Employee.create!(name: "Developer", manager: vp)
|
|
45
|
+
|
|
46
|
+
dev.manager # => VP Engineering
|
|
47
|
+
vp.subordinates # => [Developer]
|
|
48
|
+
ceo.subordinates # => [VP Engineering]
|
|
49
|
+
|
|
50
|
+
# ============================================
|
|
51
|
+
# Friendship: Many-to-Many Self-Join
|
|
52
|
+
# ============================================
|
|
53
|
+
|
|
54
|
+
class User < ApplicationRecord
|
|
55
|
+
has_many :friendships, dependent: :destroy
|
|
56
|
+
has_many :friends, through: :friendships
|
|
57
|
+
|
|
58
|
+
# Inverse friendships (where user is the friend)
|
|
59
|
+
has_many :inverse_friendships,
|
|
60
|
+
class_name: "Friendship",
|
|
61
|
+
foreign_key: "friend_id",
|
|
62
|
+
dependent: :destroy
|
|
63
|
+
has_many :inverse_friends,
|
|
64
|
+
through: :inverse_friendships,
|
|
65
|
+
source: :user
|
|
66
|
+
|
|
67
|
+
def all_friends
|
|
68
|
+
friends + inverse_friends
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def friend_with?(other_user)
|
|
72
|
+
friends.include?(other_user) || inverse_friends.include?(other_user)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
class Friendship < ApplicationRecord
|
|
77
|
+
belongs_to :user
|
|
78
|
+
belongs_to :friend, class_name: "User"
|
|
79
|
+
|
|
80
|
+
validates :user_id, uniqueness: { scope: :friend_id }
|
|
81
|
+
validate :not_self_friend
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def not_self_friend
|
|
86
|
+
errors.add(:friend, "can't be yourself") if user_id == friend_id
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Migration
|
|
91
|
+
# create_table :friendships do |t|
|
|
92
|
+
# t.belongs_to :user, null: false, foreign_key: true
|
|
93
|
+
# t.belongs_to :friend, null: false, foreign_key: { to_table: :users }
|
|
94
|
+
# t.timestamps
|
|
95
|
+
# end
|
|
96
|
+
# add_index :friendships, [:user_id, :friend_id], unique: true
|
|
97
|
+
|
|
98
|
+
# Usage
|
|
99
|
+
alice = User.create!(name: "Alice")
|
|
100
|
+
bob = User.create!(name: "Bob")
|
|
101
|
+
|
|
102
|
+
Friendship.create!(user: alice, friend: bob)
|
|
103
|
+
alice.friends.include?(bob) # => true
|
|
104
|
+
bob.inverse_friends.include?(alice) # => true
|
|
105
|
+
|
|
106
|
+
# ============================================
|
|
107
|
+
# Bidirectional Friendship (Mutual)
|
|
108
|
+
# ============================================
|
|
109
|
+
|
|
110
|
+
class User < ApplicationRecord
|
|
111
|
+
has_many :friendships, dependent: :destroy
|
|
112
|
+
has_many :friends, through: :friendships
|
|
113
|
+
|
|
114
|
+
def befriend(other_user)
|
|
115
|
+
return if self == other_user
|
|
116
|
+
return if friend_with?(other_user)
|
|
117
|
+
|
|
118
|
+
# Create bidirectional friendship
|
|
119
|
+
transaction do
|
|
120
|
+
friendships.create!(friend: other_user)
|
|
121
|
+
other_user.friendships.create!(friend: self)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def unfriend(other_user)
|
|
126
|
+
transaction do
|
|
127
|
+
friendships.find_by(friend: other_user)&.destroy
|
|
128
|
+
other_user.friendships.find_by(friend: self)&.destroy
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def friend_with?(other_user)
|
|
133
|
+
friends.exists?(id: other_user.id)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# ============================================
|
|
138
|
+
# Tree Structure: Parent/Children
|
|
139
|
+
# ============================================
|
|
140
|
+
|
|
141
|
+
class Category < ApplicationRecord
|
|
142
|
+
belongs_to :parent,
|
|
143
|
+
class_name: "Category",
|
|
144
|
+
optional: true,
|
|
145
|
+
inverse_of: :children,
|
|
146
|
+
counter_cache: :children_count
|
|
147
|
+
|
|
148
|
+
has_many :children,
|
|
149
|
+
class_name: "Category",
|
|
150
|
+
foreign_key: "parent_id",
|
|
151
|
+
dependent: :destroy,
|
|
152
|
+
inverse_of: :parent
|
|
153
|
+
|
|
154
|
+
validates :name, presence: true
|
|
155
|
+
|
|
156
|
+
scope :roots, -> { where(parent_id: nil) }
|
|
157
|
+
scope :leaves, -> { where(children_count: 0) }
|
|
158
|
+
|
|
159
|
+
def root?
|
|
160
|
+
parent_id.nil?
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def leaf?
|
|
164
|
+
children.empty?
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def ancestors
|
|
168
|
+
node = self
|
|
169
|
+
result = []
|
|
170
|
+
while node.parent
|
|
171
|
+
result << node.parent
|
|
172
|
+
node = node.parent
|
|
173
|
+
end
|
|
174
|
+
result.reverse
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def descendants
|
|
178
|
+
children.flat_map { |c| [c] + c.descendants }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def depth
|
|
182
|
+
ancestors.size
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Migration
|
|
187
|
+
# create_table :categories do |t|
|
|
188
|
+
# t.string :name, null: false
|
|
189
|
+
# t.belongs_to :parent, foreign_key: { to_table: :categories }
|
|
190
|
+
# t.integer :children_count, default: 0, null: false
|
|
191
|
+
# t.timestamps
|
|
192
|
+
# end
|
|
193
|
+
|
|
194
|
+
# Usage
|
|
195
|
+
electronics = Category.create!(name: "Electronics")
|
|
196
|
+
phones = Category.create!(name: "Phones", parent: electronics)
|
|
197
|
+
smartphones = Category.create!(name: "Smartphones", parent: phones)
|
|
198
|
+
|
|
199
|
+
smartphones.ancestors # => [Electronics, Phones]
|
|
200
|
+
electronics.descendants # => [Phones, Smartphones]
|
|
201
|
+
Category.roots # => [Electronics]
|
|
202
|
+
|
|
203
|
+
# ============================================
|
|
204
|
+
# Adjacency List with Recursive CTE
|
|
205
|
+
# ============================================
|
|
206
|
+
|
|
207
|
+
class Category < ApplicationRecord
|
|
208
|
+
# ... same associations as above ...
|
|
209
|
+
|
|
210
|
+
# PostgreSQL recursive query for full tree
|
|
211
|
+
def self.tree_for(root_id)
|
|
212
|
+
sql = <<~SQL
|
|
213
|
+
WITH RECURSIVE category_tree AS (
|
|
214
|
+
SELECT id, name, parent_id, 0 AS depth
|
|
215
|
+
FROM categories
|
|
216
|
+
WHERE id = :root_id
|
|
217
|
+
UNION ALL
|
|
218
|
+
SELECT c.id, c.name, c.parent_id, ct.depth + 1
|
|
219
|
+
FROM categories c
|
|
220
|
+
INNER JOIN category_tree ct ON c.parent_id = ct.id
|
|
221
|
+
)
|
|
222
|
+
SELECT * FROM category_tree ORDER BY depth, name
|
|
223
|
+
SQL
|
|
224
|
+
|
|
225
|
+
find_by_sql([sql, { root_id: }])
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def full_path
|
|
229
|
+
self.class.ancestors_for(id).pluck(:name).join(" > ")
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def self.ancestors_for(category_id)
|
|
233
|
+
sql = <<~SQL
|
|
234
|
+
WITH RECURSIVE ancestors AS (
|
|
235
|
+
SELECT id, name, parent_id, 0 AS depth
|
|
236
|
+
FROM categories
|
|
237
|
+
WHERE id = :category_id
|
|
238
|
+
UNION ALL
|
|
239
|
+
SELECT c.id, c.name, c.parent_id, a.depth + 1
|
|
240
|
+
FROM categories c
|
|
241
|
+
INNER JOIN ancestors a ON c.id = a.parent_id
|
|
242
|
+
)
|
|
243
|
+
SELECT * FROM ancestors ORDER BY depth DESC
|
|
244
|
+
SQL
|
|
245
|
+
|
|
246
|
+
find_by_sql([sql, { category_id: }])
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# ============================================
|
|
251
|
+
# Follower/Following Pattern
|
|
252
|
+
# ============================================
|
|
253
|
+
|
|
254
|
+
class User < ApplicationRecord
|
|
255
|
+
# Users I follow
|
|
256
|
+
has_many :active_follows,
|
|
257
|
+
class_name: "Follow",
|
|
258
|
+
foreign_key: "follower_id",
|
|
259
|
+
dependent: :destroy
|
|
260
|
+
has_many :following, through: :active_follows, source: :followed
|
|
261
|
+
|
|
262
|
+
# Users who follow me
|
|
263
|
+
has_many :passive_follows,
|
|
264
|
+
class_name: "Follow",
|
|
265
|
+
foreign_key: "followed_id",
|
|
266
|
+
dependent: :destroy
|
|
267
|
+
has_many :followers, through: :passive_follows, source: :follower
|
|
268
|
+
|
|
269
|
+
def follow(other_user)
|
|
270
|
+
following << other_user unless self == other_user
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def unfollow(other_user)
|
|
274
|
+
active_follows.find_by(followed: other_user)&.destroy
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def following?(other_user)
|
|
278
|
+
following.exists?(id: other_user.id)
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
class Follow < ApplicationRecord
|
|
283
|
+
belongs_to :follower, class_name: "User"
|
|
284
|
+
belongs_to :followed, class_name: "User"
|
|
285
|
+
|
|
286
|
+
validates :follower_id, uniqueness: { scope: :followed_id }
|
|
287
|
+
validate :not_self_follow
|
|
288
|
+
|
|
289
|
+
private
|
|
290
|
+
|
|
291
|
+
def not_self_follow
|
|
292
|
+
errors.add(:followed, "can't follow yourself") if follower_id == followed_id
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Migration
|
|
297
|
+
# create_table :follows do |t|
|
|
298
|
+
# t.belongs_to :follower, null: false, foreign_key: { to_table: :users }
|
|
299
|
+
# t.belongs_to :followed, null: false, foreign_key: { to_table: :users }
|
|
300
|
+
# t.timestamps
|
|
301
|
+
# end
|
|
302
|
+
# add_index :follows, [:follower_id, :followed_id], unique: true
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Through Association Examples
|
|
2
|
+
# Many-to-many relationships with join models
|
|
3
|
+
|
|
4
|
+
# ============================================
|
|
5
|
+
# has_many :through - Standard Pattern
|
|
6
|
+
# ============================================
|
|
7
|
+
|
|
8
|
+
# Physician <-> Appointment <-> Patient
|
|
9
|
+
#
|
|
10
|
+
# ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
11
|
+
# │ Physician │ │ Appointment │ │ Patient │
|
|
12
|
+
# ├──────────────┤ ├──────────────┤ ├──────────────┤
|
|
13
|
+
# │ id │◄──────│ physician_id │ │ id │
|
|
14
|
+
# │ name │ │ patient_id │──────►│ name │
|
|
15
|
+
# │ │ │ scheduled_at │ │ │
|
|
16
|
+
# └──────────────┘ └──────────────┘ └──────────────┘
|
|
17
|
+
|
|
18
|
+
class Physician < ApplicationRecord
|
|
19
|
+
has_many :appointments, dependent: :destroy
|
|
20
|
+
has_many :patients, through: :appointments
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class Appointment < ApplicationRecord
|
|
24
|
+
belongs_to :physician
|
|
25
|
+
belongs_to :patient
|
|
26
|
+
|
|
27
|
+
# Join model can have attributes
|
|
28
|
+
validates :scheduled_at, presence: true
|
|
29
|
+
validate :no_double_booking
|
|
30
|
+
|
|
31
|
+
scope :upcoming, -> { where("scheduled_at > ?", Time.current) }
|
|
32
|
+
scope :past, -> { where("scheduled_at <= ?", Time.current) }
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def no_double_booking
|
|
37
|
+
return unless scheduled_at
|
|
38
|
+
|
|
39
|
+
conflict = Appointment.where(physician:, scheduled_at:).where.not(id:)
|
|
40
|
+
errors.add(:scheduled_at, "physician already has appointment") if conflict.exists?
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class Patient < ApplicationRecord
|
|
45
|
+
has_many :appointments, dependent: :destroy
|
|
46
|
+
has_many :physicians, through: :appointments
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Migrations
|
|
50
|
+
# create_table :appointments do |t|
|
|
51
|
+
# t.belongs_to :physician, null: false, foreign_key: true
|
|
52
|
+
# t.belongs_to :patient, null: false, foreign_key: true
|
|
53
|
+
# t.datetime :scheduled_at, null: false
|
|
54
|
+
# t.text :notes
|
|
55
|
+
# t.timestamps
|
|
56
|
+
# end
|
|
57
|
+
# add_index :appointments, [:physician_id, :scheduled_at], unique: true
|
|
58
|
+
|
|
59
|
+
# ============================================
|
|
60
|
+
# Usage Examples
|
|
61
|
+
# ============================================
|
|
62
|
+
|
|
63
|
+
# Creating through association
|
|
64
|
+
physician = Physician.find(1)
|
|
65
|
+
patient = Patient.find(1)
|
|
66
|
+
|
|
67
|
+
# Via join model
|
|
68
|
+
appointment = Appointment.create!(
|
|
69
|
+
physician:,
|
|
70
|
+
patient:,
|
|
71
|
+
scheduled_at: 1.day.from_now
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Shortcut - creates join record automatically
|
|
75
|
+
physician.patients << patient # Creates Appointment
|
|
76
|
+
|
|
77
|
+
# With join model attributes
|
|
78
|
+
physician.appointments.create!(
|
|
79
|
+
patient:,
|
|
80
|
+
scheduled_at: 2.days.from_now,
|
|
81
|
+
notes: "Follow-up visit"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Querying through
|
|
85
|
+
physician.patients.where(name: "John")
|
|
86
|
+
physician.appointments.upcoming
|
|
87
|
+
|
|
88
|
+
# ============================================
|
|
89
|
+
# has_one :through
|
|
90
|
+
# ============================================
|
|
91
|
+
|
|
92
|
+
# Supplier -> Account -> AccountHistory
|
|
93
|
+
|
|
94
|
+
class Supplier < ApplicationRecord
|
|
95
|
+
has_one :account
|
|
96
|
+
has_one :account_history, through: :account
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
class Account < ApplicationRecord
|
|
100
|
+
belongs_to :supplier
|
|
101
|
+
has_one :account_history
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
class AccountHistory < ApplicationRecord
|
|
105
|
+
belongs_to :account
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Usage
|
|
109
|
+
supplier = Supplier.first
|
|
110
|
+
supplier.account_history # One query through Account
|
|
111
|
+
|
|
112
|
+
# ============================================
|
|
113
|
+
# Nested has_many :through
|
|
114
|
+
# ============================================
|
|
115
|
+
|
|
116
|
+
# Document -> Section -> Paragraph
|
|
117
|
+
|
|
118
|
+
class Document < ApplicationRecord
|
|
119
|
+
has_many :sections
|
|
120
|
+
has_many :paragraphs, through: :sections
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
class Section < ApplicationRecord
|
|
124
|
+
belongs_to :document
|
|
125
|
+
has_many :paragraphs
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
class Paragraph < ApplicationRecord
|
|
129
|
+
belongs_to :section
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Access all paragraphs in document
|
|
133
|
+
document.paragraphs
|
|
134
|
+
|
|
135
|
+
# ============================================
|
|
136
|
+
# Inverse Of - Critical for Nested Attributes
|
|
137
|
+
# ============================================
|
|
138
|
+
|
|
139
|
+
class Invoice < ApplicationRecord
|
|
140
|
+
has_many :line_items, inverse_of: :invoice # REQUIRED!
|
|
141
|
+
accepts_nested_attributes_for :line_items
|
|
142
|
+
|
|
143
|
+
validates :total, presence: true
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
class LineItem < ApplicationRecord
|
|
147
|
+
belongs_to :invoice
|
|
148
|
+
validates :invoice, presence: true # Fails without inverse_of!
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Without inverse_of:
|
|
152
|
+
# Invoice.create!(line_items_attributes: [{...}])
|
|
153
|
+
# => ValidationFailed: Invoice can't be blank
|
|
154
|
+
|
|
155
|
+
# With inverse_of:
|
|
156
|
+
# Invoice.create!(line_items_attributes: [{...}])
|
|
157
|
+
# => Success! Rails knows line_item.invoice is the parent
|
|
158
|
+
|
|
159
|
+
# ============================================
|
|
160
|
+
# Source Option - When Names Don't Match
|
|
161
|
+
# ============================================
|
|
162
|
+
|
|
163
|
+
class Person < ApplicationRecord
|
|
164
|
+
has_many :readings
|
|
165
|
+
has_many :articles, through: :readings, source: :post # post, not article
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
class Reading < ApplicationRecord
|
|
169
|
+
belongs_to :person
|
|
170
|
+
belongs_to :post # Not called "article"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# ============================================
|
|
174
|
+
# HABTM vs Through Comparison
|
|
175
|
+
# ============================================
|
|
176
|
+
|
|
177
|
+
# HABTM - Simple but limited
|
|
178
|
+
class Assembly < ApplicationRecord
|
|
179
|
+
has_and_belongs_to_many :parts
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
class Part < ApplicationRecord
|
|
183
|
+
has_and_belongs_to_many :assemblies
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# has_many :through - Flexible (PREFERRED)
|
|
187
|
+
class Assembly < ApplicationRecord
|
|
188
|
+
has_many :assembly_parts
|
|
189
|
+
has_many :parts, through: :assembly_parts
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
class AssemblyPart < ApplicationRecord
|
|
193
|
+
belongs_to :assembly
|
|
194
|
+
belongs_to :part
|
|
195
|
+
|
|
196
|
+
# Can add attributes later!
|
|
197
|
+
# quantity, position, notes, etc.
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
class Part < ApplicationRecord
|
|
201
|
+
has_many :assembly_parts
|
|
202
|
+
has_many :assemblies, through: :assembly_parts
|
|
203
|
+
end
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# ActiveRecord CRUD Operations Examples
|
|
2
|
+
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# CREATE
|
|
5
|
+
# =============================================================================
|
|
6
|
+
|
|
7
|
+
# Two-step creation
|
|
8
|
+
user = User.new
|
|
9
|
+
user.name = "Alice"
|
|
10
|
+
user.email = "alice@example.com"
|
|
11
|
+
if user.save
|
|
12
|
+
puts "User created: #{user.id}"
|
|
13
|
+
else
|
|
14
|
+
puts "Errors: #{user.errors.full_messages}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# One-step creation
|
|
18
|
+
user = User.create(name: "Bob", email: "bob@example.com")
|
|
19
|
+
if user.persisted?
|
|
20
|
+
puts "User created: #{user.id}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# With block
|
|
24
|
+
user = User.create(email: "charlie@example.com") do |u|
|
|
25
|
+
u.name = "Charlie"
|
|
26
|
+
u.role = :admin
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Bang methods (raise on failure)
|
|
30
|
+
begin
|
|
31
|
+
user = User.create!(name: "", email: "invalid")
|
|
32
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
33
|
+
puts "Validation failed: #{e.message}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Bulk insert (skips validations and callbacks)
|
|
37
|
+
User.insert_all([
|
|
38
|
+
{ name: "User 1", email: "user1@example.com", created_at: Time.current, updated_at: Time.current },
|
|
39
|
+
{ name: "User 2", email: "user2@example.com", created_at: Time.current, updated_at: Time.current }
|
|
40
|
+
])
|
|
41
|
+
|
|
42
|
+
# Upsert (insert or update on conflict)
|
|
43
|
+
User.upsert_all(
|
|
44
|
+
[{ email: "alice@example.com", name: "Alice Updated" }],
|
|
45
|
+
unique_by: :email
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# =============================================================================
|
|
49
|
+
# READ
|
|
50
|
+
# =============================================================================
|
|
51
|
+
|
|
52
|
+
# find - raises RecordNotFound if not found
|
|
53
|
+
user = User.find(1)
|
|
54
|
+
|
|
55
|
+
# find with multiple IDs
|
|
56
|
+
users = User.find([1, 2, 3]) # Returns array, raises if ANY not found
|
|
57
|
+
|
|
58
|
+
# find_by - returns nil if not found
|
|
59
|
+
user = User.find_by(email: "alice@example.com")
|
|
60
|
+
user = User.find_by(status: :active, role: :admin)
|
|
61
|
+
|
|
62
|
+
# find_by! - raises if not found
|
|
63
|
+
user = User.find_by!(email: "nonexistent@example.com")
|
|
64
|
+
|
|
65
|
+
# where - returns Relation
|
|
66
|
+
users = User.where(active: true)
|
|
67
|
+
users = User.where("created_at > ?", 1.week.ago)
|
|
68
|
+
users = User.where(role: [:admin, :moderator])
|
|
69
|
+
|
|
70
|
+
# Chainable queries
|
|
71
|
+
users = User
|
|
72
|
+
.where(active: true)
|
|
73
|
+
.where.not(role: :guest)
|
|
74
|
+
.order(created_at: :desc)
|
|
75
|
+
.limit(10)
|
|
76
|
+
|
|
77
|
+
# First/last
|
|
78
|
+
user = User.first
|
|
79
|
+
user = User.last
|
|
80
|
+
user = User.order(:name).first(5) # First 5 by name
|
|
81
|
+
|
|
82
|
+
# Take (no order guarantee, faster)
|
|
83
|
+
user = User.take
|
|
84
|
+
users = User.take(3)
|
|
85
|
+
|
|
86
|
+
# =============================================================================
|
|
87
|
+
# UPDATE
|
|
88
|
+
# =============================================================================
|
|
89
|
+
|
|
90
|
+
# Standard update (with validations and callbacks)
|
|
91
|
+
user = User.find(1)
|
|
92
|
+
user.update(name: "New Name")
|
|
93
|
+
|
|
94
|
+
# Update with bang (raises on failure)
|
|
95
|
+
user.update!(name: "New Name")
|
|
96
|
+
|
|
97
|
+
# Multiple attributes
|
|
98
|
+
user.update(
|
|
99
|
+
name: "New Name",
|
|
100
|
+
email: "newemail@example.com",
|
|
101
|
+
settings: { theme: "dark" }
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Update attribute (skips validations, runs callbacks)
|
|
105
|
+
user.update_attribute(:verified, true)
|
|
106
|
+
|
|
107
|
+
# Update column (skips validations AND callbacks)
|
|
108
|
+
user.update_column(:login_count, user.login_count + 1)
|
|
109
|
+
|
|
110
|
+
# Update columns (multiple)
|
|
111
|
+
user.update_columns(
|
|
112
|
+
login_count: user.login_count + 1,
|
|
113
|
+
last_login_at: Time.current
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Conditional update
|
|
117
|
+
user.update(status: :premium) if user.eligible_for_premium?
|
|
118
|
+
|
|
119
|
+
# Bulk update (skips validations and callbacks)
|
|
120
|
+
User.where(status: :trial).update_all(status: :expired)
|
|
121
|
+
User.where("last_login_at < ?", 1.year.ago).update_all(active: false)
|
|
122
|
+
|
|
123
|
+
# Update with SQL expression
|
|
124
|
+
User.update_all("login_count = login_count + 1")
|
|
125
|
+
|
|
126
|
+
# =============================================================================
|
|
127
|
+
# DELETE / DESTROY
|
|
128
|
+
# =============================================================================
|
|
129
|
+
|
|
130
|
+
# Destroy (with callbacks and dependent associations)
|
|
131
|
+
user = User.find(1)
|
|
132
|
+
user.destroy
|
|
133
|
+
puts user.destroyed? # => true
|
|
134
|
+
puts user.frozen? # => true
|
|
135
|
+
|
|
136
|
+
# Delete (skip callbacks, orphans dependent records)
|
|
137
|
+
user = User.find(2)
|
|
138
|
+
user.delete
|
|
139
|
+
|
|
140
|
+
# Bulk destroy (with callbacks)
|
|
141
|
+
User.where(status: :banned).destroy_all
|
|
142
|
+
|
|
143
|
+
# Bulk delete (without callbacks)
|
|
144
|
+
User.where(status: :banned).delete_all
|
|
145
|
+
|
|
146
|
+
# Delete by ID
|
|
147
|
+
User.destroy(5) # With callbacks
|
|
148
|
+
User.delete(5) # Without callbacks
|
|
149
|
+
|
|
150
|
+
# =============================================================================
|
|
151
|
+
# FIND OR CREATE
|
|
152
|
+
# =============================================================================
|
|
153
|
+
|
|
154
|
+
# Find or create
|
|
155
|
+
user = User.find_or_create_by(email: "alice@example.com")
|
|
156
|
+
|
|
157
|
+
# With additional attributes via block
|
|
158
|
+
user = User.find_or_create_by(email: "alice@example.com") do |u|
|
|
159
|
+
u.name = "Alice"
|
|
160
|
+
u.role = :member
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Find or initialize (doesn't save)
|
|
164
|
+
user = User.find_or_initialize_by(email: "newuser@example.com")
|
|
165
|
+
user.name = "New User"
|
|
166
|
+
user.save if user.new_record?
|
|
167
|
+
|
|
168
|
+
# Create or find (handles race conditions - requires unique constraint)
|
|
169
|
+
user = User.create_or_find_by(email: "alice@example.com") do |u|
|
|
170
|
+
u.name = "Alice"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# =============================================================================
|
|
174
|
+
# PRACTICAL PATTERNS
|
|
175
|
+
# =============================================================================
|
|
176
|
+
|
|
177
|
+
# Safe lookup with fallback
|
|
178
|
+
def find_user(id)
|
|
179
|
+
User.find_by(id:) || User.new(name: "Guest")
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Idempotent create
|
|
183
|
+
def ensure_default_category
|
|
184
|
+
Category.find_or_create_by!(name: "Uncategorized") do |c|
|
|
185
|
+
c.slug = "uncategorized"
|
|
186
|
+
c.position = 0
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Soft delete pattern
|
|
191
|
+
class User < ApplicationRecord
|
|
192
|
+
scope :active, -> { where(deleted_at: nil) }
|
|
193
|
+
scope :deleted, -> { where.not(deleted_at: nil) }
|
|
194
|
+
|
|
195
|
+
def soft_delete
|
|
196
|
+
update(deleted_at: Time.current)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def restore
|
|
200
|
+
update(deleted_at: nil)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Increment/decrement counters
|
|
205
|
+
user.increment!(:login_count)
|
|
206
|
+
user.decrement!(:credits)
|
|
207
|
+
|
|
208
|
+
# Toggle boolean
|
|
209
|
+
user.toggle!(:email_notifications)
|