smart_domain 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/CHANGELOG.md +25 -0
  4. data/LICENSE +21 -0
  5. data/README.md +1219 -0
  6. data/Rakefile +12 -0
  7. data/examples/blog_app/.dockerignore +51 -0
  8. data/examples/blog_app/.github/dependabot.yml +12 -0
  9. data/examples/blog_app/.github/workflows/ci.yml +67 -0
  10. data/examples/blog_app/.gitignore +30 -0
  11. data/examples/blog_app/.kamal/hooks/docker-setup.sample +3 -0
  12. data/examples/blog_app/.kamal/hooks/post-app-boot.sample +3 -0
  13. data/examples/blog_app/.kamal/hooks/post-deploy.sample +14 -0
  14. data/examples/blog_app/.kamal/hooks/post-proxy-reboot.sample +3 -0
  15. data/examples/blog_app/.kamal/hooks/pre-app-boot.sample +3 -0
  16. data/examples/blog_app/.kamal/hooks/pre-build.sample +51 -0
  17. data/examples/blog_app/.kamal/hooks/pre-connect.sample +47 -0
  18. data/examples/blog_app/.kamal/hooks/pre-deploy.sample +122 -0
  19. data/examples/blog_app/.kamal/hooks/pre-proxy-reboot.sample +3 -0
  20. data/examples/blog_app/.kamal/secrets +20 -0
  21. data/examples/blog_app/.rubocop.yml +8 -0
  22. data/examples/blog_app/.ruby-version +1 -0
  23. data/examples/blog_app/Dockerfile +76 -0
  24. data/examples/blog_app/Gemfile +63 -0
  25. data/examples/blog_app/Gemfile.lock +408 -0
  26. data/examples/blog_app/README.md +24 -0
  27. data/examples/blog_app/README_EXAMPLE.md +328 -0
  28. data/examples/blog_app/Rakefile +6 -0
  29. data/examples/blog_app/app/assets/images/.keep +0 -0
  30. data/examples/blog_app/app/assets/stylesheets/application.css +10 -0
  31. data/examples/blog_app/app/controllers/api/base_controller.rb +61 -0
  32. data/examples/blog_app/app/controllers/api/v1/posts_controller.rb +158 -0
  33. data/examples/blog_app/app/controllers/api/v1/users_controller.rb +98 -0
  34. data/examples/blog_app/app/controllers/application_controller.rb +7 -0
  35. data/examples/blog_app/app/controllers/concerns/.keep +0 -0
  36. data/examples/blog_app/app/domains/.keep +0 -0
  37. data/examples/blog_app/app/domains/exceptions.rb +19 -0
  38. data/examples/blog_app/app/domains/post_management/events/post_created_event.rb +15 -0
  39. data/examples/blog_app/app/domains/post_management/events/post_deleted_event.rb +13 -0
  40. data/examples/blog_app/app/domains/post_management/events/post_updated_event.rb +13 -0
  41. data/examples/blog_app/app/domains/post_management/handlers/post_notification_handler.rb +33 -0
  42. data/examples/blog_app/app/domains/post_management/models/post.rb +21 -0
  43. data/examples/blog_app/app/domains/post_management/policies/post_policy.rb +49 -0
  44. data/examples/blog_app/app/domains/post_management/post_service.rb +93 -0
  45. data/examples/blog_app/app/domains/post_management/setup.rb +25 -0
  46. data/examples/blog_app/app/domains/user_management/events/user_created_event.rb +15 -0
  47. data/examples/blog_app/app/domains/user_management/events/user_deleted_event.rb +13 -0
  48. data/examples/blog_app/app/domains/user_management/events/user_updated_event.rb +13 -0
  49. data/examples/blog_app/app/domains/user_management/handlers/user_welcome_handler.rb +21 -0
  50. data/examples/blog_app/app/domains/user_management/models/user.rb +11 -0
  51. data/examples/blog_app/app/domains/user_management/policies/user_policy.rb +49 -0
  52. data/examples/blog_app/app/domains/user_management/setup.rb +25 -0
  53. data/examples/blog_app/app/domains/user_management/user_service.rb +93 -0
  54. data/examples/blog_app/app/events/.keep +0 -0
  55. data/examples/blog_app/app/events/application_event.rb +18 -0
  56. data/examples/blog_app/app/handlers/.keep +0 -0
  57. data/examples/blog_app/app/helpers/application_helper.rb +2 -0
  58. data/examples/blog_app/app/javascript/application.js +3 -0
  59. data/examples/blog_app/app/javascript/controllers/application.js +9 -0
  60. data/examples/blog_app/app/javascript/controllers/hello_controller.js +7 -0
  61. data/examples/blog_app/app/javascript/controllers/index.js +4 -0
  62. data/examples/blog_app/app/jobs/application_job.rb +7 -0
  63. data/examples/blog_app/app/mailers/application_mailer.rb +4 -0
  64. data/examples/blog_app/app/models/application_record.rb +3 -0
  65. data/examples/blog_app/app/models/concerns/.keep +0 -0
  66. data/examples/blog_app/app/models/organization.rb +6 -0
  67. data/examples/blog_app/app/policies/.keep +0 -0
  68. data/examples/blog_app/app/policies/application_policy.rb +23 -0
  69. data/examples/blog_app/app/services/.keep +0 -0
  70. data/examples/blog_app/app/services/application_service.rb +24 -0
  71. data/examples/blog_app/app/views/layouts/application.html.erb +29 -0
  72. data/examples/blog_app/app/views/layouts/mailer.html.erb +13 -0
  73. data/examples/blog_app/app/views/layouts/mailer.text.erb +1 -0
  74. data/examples/blog_app/app/views/pwa/manifest.json.erb +22 -0
  75. data/examples/blog_app/app/views/pwa/service-worker.js +26 -0
  76. data/examples/blog_app/bin/brakeman +7 -0
  77. data/examples/blog_app/bin/bundler-audit +6 -0
  78. data/examples/blog_app/bin/ci +6 -0
  79. data/examples/blog_app/bin/dev +2 -0
  80. data/examples/blog_app/bin/docker-entrypoint +8 -0
  81. data/examples/blog_app/bin/importmap +4 -0
  82. data/examples/blog_app/bin/jobs +6 -0
  83. data/examples/blog_app/bin/kamal +27 -0
  84. data/examples/blog_app/bin/rails +4 -0
  85. data/examples/blog_app/bin/rake +4 -0
  86. data/examples/blog_app/bin/rubocop +8 -0
  87. data/examples/blog_app/bin/setup +35 -0
  88. data/examples/blog_app/bin/thrust +5 -0
  89. data/examples/blog_app/config/application.rb +52 -0
  90. data/examples/blog_app/config/boot.rb +4 -0
  91. data/examples/blog_app/config/bundler-audit.yml +5 -0
  92. data/examples/blog_app/config/cable.yml +17 -0
  93. data/examples/blog_app/config/cache.yml +16 -0
  94. data/examples/blog_app/config/ci.rb +19 -0
  95. data/examples/blog_app/config/credentials.yml.enc +1 -0
  96. data/examples/blog_app/config/database.yml +41 -0
  97. data/examples/blog_app/config/deploy.yml +120 -0
  98. data/examples/blog_app/config/environment.rb +5 -0
  99. data/examples/blog_app/config/environments/development.rb +78 -0
  100. data/examples/blog_app/config/environments/production.rb +90 -0
  101. data/examples/blog_app/config/environments/test.rb +53 -0
  102. data/examples/blog_app/config/importmap.rb +7 -0
  103. data/examples/blog_app/config/initializers/active_domain.rb +37 -0
  104. data/examples/blog_app/config/initializers/assets.rb +7 -0
  105. data/examples/blog_app/config/initializers/content_security_policy.rb +29 -0
  106. data/examples/blog_app/config/initializers/filter_parameter_logging.rb +8 -0
  107. data/examples/blog_app/config/initializers/inflections.rb +16 -0
  108. data/examples/blog_app/config/locales/en.yml +31 -0
  109. data/examples/blog_app/config/master.key +1 -0
  110. data/examples/blog_app/config/puma.rb +42 -0
  111. data/examples/blog_app/config/queue.yml +18 -0
  112. data/examples/blog_app/config/recurring.yml +15 -0
  113. data/examples/blog_app/config/routes.rb +27 -0
  114. data/examples/blog_app/config/storage.yml +27 -0
  115. data/examples/blog_app/config.ru +6 -0
  116. data/examples/blog_app/db/cable_schema.rb +11 -0
  117. data/examples/blog_app/db/cache_schema.rb +12 -0
  118. data/examples/blog_app/db/migrate/20251230112502_create_organizations.rb +9 -0
  119. data/examples/blog_app/db/migrate/20251230112503_create_users.rb +12 -0
  120. data/examples/blog_app/db/migrate/20251230112504_create_posts.rb +14 -0
  121. data/examples/blog_app/db/queue_schema.rb +129 -0
  122. data/examples/blog_app/db/schema.rb +46 -0
  123. data/examples/blog_app/db/seeds.rb +9 -0
  124. data/examples/blog_app/lib/api_demo.rb +175 -0
  125. data/examples/blog_app/lib/demo.rb +150 -0
  126. data/examples/blog_app/lib/tasks/.keep +0 -0
  127. data/examples/blog_app/log/.keep +0 -0
  128. data/examples/blog_app/public/400.html +135 -0
  129. data/examples/blog_app/public/404.html +135 -0
  130. data/examples/blog_app/public/406-unsupported-browser.html +135 -0
  131. data/examples/blog_app/public/422.html +135 -0
  132. data/examples/blog_app/public/500.html +135 -0
  133. data/examples/blog_app/public/icon.png +0 -0
  134. data/examples/blog_app/public/icon.svg +3 -0
  135. data/examples/blog_app/public/robots.txt +1 -0
  136. data/examples/blog_app/script/.keep +0 -0
  137. data/examples/blog_app/storage/.keep +0 -0
  138. data/examples/blog_app/tmp/.keep +0 -0
  139. data/examples/blog_app/vendor/.keep +0 -0
  140. data/examples/blog_app/vendor/javascript/.keep +0 -0
  141. data/lib/generators/active_domain/domain/domain_generator.rb +116 -0
  142. data/lib/generators/active_domain/domain/templates/events/created_event.rb.tt +15 -0
  143. data/lib/generators/active_domain/domain/templates/events/deleted_event.rb.tt +13 -0
  144. data/lib/generators/active_domain/domain/templates/events/updated_event.rb.tt +13 -0
  145. data/lib/generators/active_domain/domain/templates/policy.rb.tt +49 -0
  146. data/lib/generators/active_domain/domain/templates/service.rb.tt +93 -0
  147. data/lib/generators/active_domain/domain/templates/setup.rb.tt +27 -0
  148. data/lib/generators/active_domain/install/install_generator.rb +58 -0
  149. data/lib/generators/active_domain/install/templates/README +28 -0
  150. data/lib/generators/active_domain/install/templates/application_event.rb +18 -0
  151. data/lib/generators/active_domain/install/templates/application_policy.rb +23 -0
  152. data/lib/generators/active_domain/install/templates/application_service.rb +24 -0
  153. data/lib/generators/active_domain/install/templates/initializer.rb +37 -0
  154. data/lib/smart_domain/configuration.rb +97 -0
  155. data/lib/smart_domain/domain/exceptions.rb +164 -0
  156. data/lib/smart_domain/domain/policy.rb +215 -0
  157. data/lib/smart_domain/domain/service.rb +230 -0
  158. data/lib/smart_domain/event/adapters/memory.rb +110 -0
  159. data/lib/smart_domain/event/base.rb +176 -0
  160. data/lib/smart_domain/event/handler.rb +98 -0
  161. data/lib/smart_domain/event/mixins.rb +156 -0
  162. data/lib/smart_domain/event/registration.rb +136 -0
  163. data/lib/smart_domain/generators/domain_generator.rb +4 -0
  164. data/lib/smart_domain/generators/install_generator.rb +4 -0
  165. data/lib/smart_domain/handlers/audit_handler.rb +216 -0
  166. data/lib/smart_domain/handlers/metrics_handler.rb +104 -0
  167. data/lib/smart_domain/integration/active_record.rb +169 -0
  168. data/lib/smart_domain/integration/multi_tenancy.rb +115 -0
  169. data/lib/smart_domain/railtie.rb +62 -0
  170. data/lib/smart_domain/tasks/domains.rake +43 -0
  171. data/lib/smart_domain/version.rb +5 -0
  172. data/lib/smart_domain.rb +77 -0
  173. data/smart_domain.gemspec +53 -0
  174. metadata +391 -0
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Event published when a post is created
4
+ class PostCreatedEvent < ApplicationEvent
5
+ include SmartDomain::Event::ActorMixin
6
+
7
+ attribute :post_id, :string
8
+
9
+ validates :post_id, presence: true
10
+
11
+ # Add domain-specific attributes here
12
+ # Example:
13
+ # attribute :email, :string
14
+ # attribute :name, :string
15
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Event published when a post is deleted
4
+ class PostDeletedEvent < ApplicationEvent
5
+ include SmartDomain::Event::ActorMixin
6
+
7
+ attribute :post_id, :string
8
+
9
+ validates :post_id, presence: true
10
+
11
+ # Add domain-specific attributes here if needed
12
+ # These should be attributes you want to preserve after deletion
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Event published when a post is updated
4
+ class PostUpdatedEvent < ApplicationEvent
5
+ include SmartDomain::Event::ActorMixin
6
+ include SmartDomain::Event::ChangeTrackingMixin
7
+
8
+ attribute :post_id, :string
9
+
10
+ validates :post_id, presence: true
11
+
12
+ # Add domain-specific attributes here if needed
13
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Custom event handler for notifying subscribers when posts are published
4
+ #
5
+ # This handler demonstrates conditional handling based on event data.
6
+ class PostNotificationHandler < SmartDomain::Event::Handler
7
+ def can_handle?(event_type)
8
+ event_type == "post.updated"
9
+ end
10
+
11
+ def handle(event)
12
+ # Only send notifications if the post was just published
13
+ if post_was_published?(event)
14
+ Rails.logger.info "[PostNotificationHandler] Post '#{event.post_id}' was published!"
15
+ Rails.logger.info "[PostNotificationHandler] Would notify subscribers here"
16
+
17
+ # In a real application, this would:
18
+ # - Find all subscribers
19
+ # - Send push notifications
20
+ # - Send email digests
21
+ # - Update RSS feeds
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def post_was_published?(event)
28
+ # Check if 'published' changed from false to true
29
+ event.respond_to?(:changed_fields) &&
30
+ event.changed_fields.include?("published") &&
31
+ event.new_values["published"] == true
32
+ end
33
+ end
@@ -0,0 +1,21 @@
1
+ class Post < ApplicationRecord
2
+ include SmartDomain::Integration::ActiveRecord
3
+ include SmartDomain::Integration::TenantScoped
4
+
5
+ belongs_to :organization
6
+ belongs_to :user
7
+
8
+ validates :title, presence: true
9
+ validates :body, presence: true
10
+
11
+ scope :published, -> { where(published: true) }
12
+ scope :draft, -> { where(published: false) }
13
+
14
+ def publish!
15
+ update!(published: true, published_at: Time.current)
16
+ end
17
+
18
+ def unpublish!
19
+ update!(published: false, published_at: nil)
20
+ end
21
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Authorization policy for Post
4
+ #
5
+ # Define authorization rules for post operations.
6
+ # These methods are called from services and controllers.
7
+ class PostPolicy < ApplicationPolicy
8
+ # Can user view list of posts?
9
+ def index?
10
+ user_present?
11
+ end
12
+
13
+ # Can user view this post?
14
+ def show?
15
+ user_present? && (admin? || same_organization?)
16
+ end
17
+
18
+ # Can user create a post?
19
+ def create?
20
+ user_present?
21
+ end
22
+
23
+ # Can user update this post?
24
+ def update?
25
+ user_present? && (admin? || owner? || same_organization?)
26
+ end
27
+
28
+ # Can user delete this post?
29
+ def destroy?
30
+ user_present? && (admin? || owner?)
31
+ end
32
+
33
+ # Scope for index queries
34
+ #
35
+ # Returns only posts the user is authorized to see
36
+ class Scope < Scope
37
+ def resolve
38
+ if user.nil?
39
+ scope.none
40
+ elsif user.respond_to?(:admin?) && user.admin?
41
+ scope.all
42
+ elsif user.respond_to?(:organization_id)
43
+ scope.where(organization_id: user.organization_id)
44
+ else
45
+ scope.where(user_id: user.id)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PostManagement
4
+ # Service for Post business logic
5
+ #
6
+ # This service encapsulates all business operations for posts.
7
+ # Controllers should delegate to this service rather than directly
8
+ # manipulating Post models.
9
+ class PostService < ApplicationService
10
+ # Create a new post
11
+ #
12
+ # @param attributes [Hash] Post attributes
13
+ # @return [Post] Created post
14
+ # @raise [SmartDomain::Domain::AlreadyExistsError] If post already exists
15
+ # @raise [SmartDomain::Domain::ValidationError] If validation fails
16
+ def create_post(attributes)
17
+ # Example business rule validation
18
+ # if Post.exists?(email: attributes[:email])
19
+ # raise SmartDomain::Domain::AlreadyExistsError.new('Post', 'email', attributes[:email])
20
+ # end
21
+
22
+ Post.transaction do
23
+ post = Post.create!(attributes)
24
+
25
+ # Event is published via ActiveRecord integration
26
+ # Or manually:
27
+ # event = build_event(PostCreatedEvent,
28
+ # event_type: 'post.created',
29
+ # aggregate_id: post.id,
30
+ # aggregate_type: 'Post',
31
+ # post_id: post.id
32
+ # )
33
+ # publish_after_commit(event)
34
+
35
+ log(:info, "Post created", post_id: post.id)
36
+ post
37
+ end
38
+ end
39
+
40
+ # Update an existing post
41
+ #
42
+ # @param post [Post, Integer, String] Post object or ID
43
+ # @param attributes [Hash] Attributes to update
44
+ # @return [Post] Updated post
45
+ # @raise [ActiveRecord::RecordNotFound] If post not found
46
+ # @raise [SmartDomain::Domain::UnauthorizedError] If not authorized
47
+ def update_post(post, attributes)
48
+ post = Post.find(post) unless post.is_a?(Post)
49
+
50
+ # Authorization check
51
+ policy = PostPolicy.new(current_user, post)
52
+ authorize!(policy, :update?)
53
+
54
+ Post.transaction do
55
+ post.update!(attributes)
56
+
57
+ # Event published via ActiveRecord integration
58
+ log(:info, "Post updated", post_id: post.id, changes: post.saved_changes.keys)
59
+ post
60
+ end
61
+ end
62
+
63
+ # Delete a post
64
+ #
65
+ # @param post [Post, Integer, String] Post object or ID
66
+ # @return [Boolean] True if deleted
67
+ # @raise [ActiveRecord::RecordNotFound] If post not found
68
+ # @raise [SmartDomain::Domain::UnauthorizedError] If not authorized
69
+ def delete_post(post)
70
+ post = Post.find(post) unless post.is_a?(Post)
71
+
72
+ # Authorization check
73
+ policy = PostPolicy.new(current_user, post)
74
+ authorize!(policy, :destroy?)
75
+
76
+ Post.transaction do
77
+ post.destroy!
78
+
79
+ # Event published via ActiveRecord integration
80
+ log(:info, "Post deleted", post_id: post.id)
81
+ true
82
+ end
83
+ end
84
+
85
+ # List posts with policy scope
86
+ #
87
+ # @param scope [ActiveRecord::Relation] Optional base scope
88
+ # @return [ActiveRecord::Relation] Scoped posts
89
+ def list_posts(scope = Post.all)
90
+ policy_scope(scope, PostPolicy)
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PostManagement
4
+ # Setup event handlers for post domain
5
+ #
6
+ # This file is automatically loaded by SmartDomain::Railtie
7
+ # when the Rails application starts.
8
+ def self.setup!
9
+ # Register standard handlers (audit and metrics)
10
+ # This one line replaces ~50 lines of boilerplate!
11
+ SmartDomain::Event::Registration.register_standard_handlers(
12
+ domain: 'post',
13
+ events: %w[created updated deleted],
14
+ include_audit: true,
15
+ include_metrics: true
16
+ )
17
+
18
+ # Register custom handlers
19
+ notification_handler = PostNotificationHandler.new
20
+ SmartDomain::Event.bus.subscribe('post.updated', notification_handler)
21
+
22
+ Rails.logger.info "[PostManagement] Domain setup complete"
23
+ Rails.logger.info "[PostManagement] Registered handlers: audit, metrics, notifications"
24
+ end
25
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Event published when a user is created
4
+ class UserCreatedEvent < ApplicationEvent
5
+ include SmartDomain::Event::ActorMixin
6
+
7
+ attribute :user_id, :string
8
+
9
+ validates :user_id, presence: true
10
+
11
+ # Add domain-specific attributes here
12
+ # Example:
13
+ # attribute :email, :string
14
+ # attribute :name, :string
15
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Event published when a user is deleted
4
+ class UserDeletedEvent < ApplicationEvent
5
+ include SmartDomain::Event::ActorMixin
6
+
7
+ attribute :user_id, :string
8
+
9
+ validates :user_id, presence: true
10
+
11
+ # Add domain-specific attributes here if needed
12
+ # These should be attributes you want to preserve after deletion
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Event published when a user is updated
4
+ class UserUpdatedEvent < ApplicationEvent
5
+ include SmartDomain::Event::ActorMixin
6
+ include SmartDomain::Event::ChangeTrackingMixin
7
+
8
+ attribute :user_id, :string
9
+
10
+ validates :user_id, presence: true
11
+
12
+ # Add domain-specific attributes here if needed
13
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: truell
2
+
3
+ # Custom event handler for sending welcome emails
4
+ #
5
+ # This handler demonstrates how to create custom domain-specific handlers
6
+ # that respond to specific events and trigger side effects.
7
+ class UserWelcomeHandler < SmartDomain::Event::Handler
8
+ def can_handle?(event_type)
9
+ event_type == "user.created"
10
+ end
11
+
12
+ def handle(event)
13
+ Rails.logger.info "[UserWelcomeHandler] Sending welcome email to #{event.email}"
14
+
15
+ # In a real application, this would call a mailer:
16
+ # UserMailer.welcome_email(event.user_id).deliver_later
17
+
18
+ # For this example, we'll just log
19
+ Rails.logger.info "[UserWelcomeHandler] Welcome email sent to #{event.email}"
20
+ end
21
+ end
@@ -0,0 +1,11 @@
1
+ class User < ApplicationRecord
2
+ include SmartDomain::Integration::ActiveRecord
3
+ include SmartDomain::Integration::TenantScoped
4
+
5
+ belongs_to :organization
6
+ has_many :posts, dependent: :destroy
7
+
8
+ validates :email, presence: true, uniqueness: { scope: :organization_id }
9
+ validates :name, presence: true
10
+ validates :role, inclusion: { in: %w[admin editor viewer] }
11
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Authorization policy for User
4
+ #
5
+ # Define authorization rules for user operations.
6
+ # These methods are called from services and controllers.
7
+ class UserPolicy < ApplicationPolicy
8
+ # Can user view list of users?
9
+ def index?
10
+ user_present?
11
+ end
12
+
13
+ # Can user view this user?
14
+ def show?
15
+ user_present? && (admin? || same_organization?)
16
+ end
17
+
18
+ # Can user create a user?
19
+ def create?
20
+ user_present?
21
+ end
22
+
23
+ # Can user update this user?
24
+ def update?
25
+ user_present? && (admin? || owner? || same_organization?)
26
+ end
27
+
28
+ # Can user delete this user?
29
+ def destroy?
30
+ user_present? && (admin? || owner?)
31
+ end
32
+
33
+ # Scope for index queries
34
+ #
35
+ # Returns only users the user is authorized to see
36
+ class Scope < Scope
37
+ def resolve
38
+ if user.nil?
39
+ scope.none
40
+ elsif user.respond_to?(:admin?) && user.admin?
41
+ scope.all
42
+ elsif user.respond_to?(:organization_id)
43
+ scope.where(organization_id: user.organization_id)
44
+ else
45
+ scope.where(user_id: user.id)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UserManagement
4
+ # Setup event handlers for user domain
5
+ #
6
+ # This file is automatically loaded by SmartDomain::Railtie
7
+ # when the Rails application starts.
8
+ def self.setup!
9
+ # Register standard handlers (audit and metrics)
10
+ # This one line replaces ~50 lines of boilerplate!
11
+ SmartDomain::Event::Registration.register_standard_handlers(
12
+ domain: 'user',
13
+ events: %w[created updated deleted],
14
+ include_audit: true,
15
+ include_metrics: true
16
+ )
17
+
18
+ # Register custom handlers
19
+ welcome_handler = UserWelcomeHandler.new
20
+ SmartDomain::Event.bus.subscribe('user.created', welcome_handler)
21
+
22
+ Rails.logger.info "[UserManagement] Domain setup complete"
23
+ Rails.logger.info "[UserManagement] Registered handlers: audit, metrics, welcome email"
24
+ end
25
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UserManagement
4
+ # Service for User business logic
5
+ #
6
+ # This service encapsulates all business operations for users.
7
+ # Controllers should delegate to this service rather than directly
8
+ # manipulating User models.
9
+ class UserService < ApplicationService
10
+ # Create a new user
11
+ #
12
+ # @param attributes [Hash] User attributes
13
+ # @return [User] Created user
14
+ # @raise [SmartDomain::Domain::AlreadyExistsError] If user already exists
15
+ # @raise [SmartDomain::Domain::ValidationError] If validation fails
16
+ def create_user(attributes)
17
+ # Example business rule validation
18
+ # if User.exists?(email: attributes[:email])
19
+ # raise SmartDomain::Domain::AlreadyExistsError.new('User', 'email', attributes[:email])
20
+ # end
21
+
22
+ User.transaction do
23
+ user = User.create!(attributes)
24
+
25
+ # Event is published via ActiveRecord integration
26
+ # Or manually:
27
+ # event = build_event(UserCreatedEvent,
28
+ # event_type: 'user.created',
29
+ # aggregate_id: user.id,
30
+ # aggregate_type: 'User',
31
+ # user_id: user.id
32
+ # )
33
+ # publish_after_commit(event)
34
+
35
+ log(:info, "User created", user_id: user.id)
36
+ user
37
+ end
38
+ end
39
+
40
+ # Update an existing user
41
+ #
42
+ # @param user [User, Integer, String] User object or ID
43
+ # @param attributes [Hash] Attributes to update
44
+ # @return [User] Updated user
45
+ # @raise [ActiveRecord::RecordNotFound] If user not found
46
+ # @raise [SmartDomain::Domain::UnauthorizedError] If not authorized
47
+ def update_user(user, attributes)
48
+ user = User.find(user) unless user.is_a?(User)
49
+
50
+ # Authorization check
51
+ policy = UserPolicy.new(current_user, user)
52
+ authorize!(policy, :update?)
53
+
54
+ User.transaction do
55
+ user.update!(attributes)
56
+
57
+ # Event published via ActiveRecord integration
58
+ log(:info, "User updated", user_id: user.id, changes: user.saved_changes.keys)
59
+ user
60
+ end
61
+ end
62
+
63
+ # Delete a user
64
+ #
65
+ # @param user [User, Integer, String] User object or ID
66
+ # @return [Boolean] True if deleted
67
+ # @raise [ActiveRecord::RecordNotFound] If user not found
68
+ # @raise [SmartDomain::Domain::UnauthorizedError] If not authorized
69
+ def delete_user(user)
70
+ user = User.find(user) unless user.is_a?(User)
71
+
72
+ # Authorization check
73
+ policy = UserPolicy.new(current_user, user)
74
+ authorize!(policy, :destroy?)
75
+
76
+ User.transaction do
77
+ user.destroy!
78
+
79
+ # Event published via ActiveRecord integration
80
+ log(:info, "User deleted", user_id: user.id)
81
+ true
82
+ end
83
+ end
84
+
85
+ # List users with policy scope
86
+ #
87
+ # @param scope [ActiveRecord::Relation] Optional base scope
88
+ # @return [ActiveRecord::Relation] Scoped users
89
+ def list_users(scope = User.all)
90
+ policy_scope(scope, UserPolicy)
91
+ end
92
+ end
93
+ end
File without changes
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Base class for all domain events in this application
4
+ #
5
+ # Inherit from this class to create domain-specific events
6
+ #
7
+ # Example:
8
+ # class UserCreatedEvent < ApplicationEvent
9
+ # attribute :user_id, :string
10
+ # attribute :email, :string
11
+ # validates :user_id, :email, presence: true
12
+ # end
13
+ class ApplicationEvent < SmartDomain::Event::Base
14
+ # Add application-wide event fields here
15
+ # Example:
16
+ # attribute :tenant_id, :string
17
+ # validates :tenant_id, presence: true
18
+ end
File without changes
@@ -0,0 +1,2 @@
1
+ module ApplicationHelper
2
+ end
@@ -0,0 +1,3 @@
1
+ // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
2
+ import "@hotwired/turbo-rails"
3
+ import "controllers"
@@ -0,0 +1,9 @@
1
+ import { Application } from "@hotwired/stimulus"
2
+
3
+ const application = Application.start()
4
+
5
+ // Configure Stimulus development experience
6
+ application.debug = false
7
+ window.Stimulus = application
8
+
9
+ export { application }
@@ -0,0 +1,7 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ connect() {
5
+ this.element.textContent = "Hello World!"
6
+ }
7
+ }
@@ -0,0 +1,4 @@
1
+ // Import and register all your controllers from the importmap via controllers/**/*_controller
2
+ import { application } from "controllers/application"
3
+ import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
4
+ eagerLoadControllersFrom("controllers", application)
@@ -0,0 +1,7 @@
1
+ class ApplicationJob < ActiveJob::Base
2
+ # Automatically retry jobs that encountered a deadlock
3
+ # retry_on ActiveRecord::Deadlocked
4
+
5
+ # Most jobs are safe to ignore if the underlying records are no longer available
6
+ # discard_on ActiveJob::DeserializationError
7
+ end
@@ -0,0 +1,4 @@
1
+ class ApplicationMailer < ActionMailer::Base
2
+ default from: "from@example.com"
3
+ layout "mailer"
4
+ end
@@ -0,0 +1,3 @@
1
+ class ApplicationRecord < ActiveRecord::Base
2
+ primary_abstract_class
3
+ end
File without changes
@@ -0,0 +1,6 @@
1
+ class Organization < ApplicationRecord
2
+ has_many :users, dependent: :destroy
3
+ has_many :posts, dependent: :destroy
4
+
5
+ validates :name, presence: true, uniqueness: true
6
+ end
File without changes
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Base class for all domain policies in this application
4
+ #
5
+ # Inherit from this class to create domain-specific policies
6
+ #
7
+ # Example:
8
+ # class UserPolicy < ApplicationPolicy
9
+ # def update?
10
+ # user.admin? || owner?
11
+ # end
12
+ #
13
+ # def destroy?
14
+ # user.admin?
15
+ # end
16
+ # end
17
+ class ApplicationPolicy < SmartDomain::Domain::Policy
18
+ # Add application-wide policy methods here
19
+ # Example:
20
+ # def manager?
21
+ # user.present? && user.role == 'manager'
22
+ # end
23
+ end
File without changes
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Base class for all domain services in this application
4
+ #
5
+ # Inherit from this class to create domain-specific services
6
+ #
7
+ # Example:
8
+ # class UserService < ApplicationService
9
+ # def create_user(attributes)
10
+ # User.transaction do
11
+ # user = User.create!(attributes)
12
+ # event = build_event(UserCreatedEvent, ...)
13
+ # publish_after_commit(event)
14
+ # user
15
+ # end
16
+ # end
17
+ # end
18
+ class ApplicationService < SmartDomain::Domain::Service
19
+ # Add application-wide service methods here
20
+ # Example:
21
+ # def current_organization
22
+ # @current_organization ||= Organization.find(current_organization_id)
23
+ # end
24
+ end