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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +25 -0
- data/LICENSE +21 -0
- data/README.md +1219 -0
- data/Rakefile +12 -0
- data/examples/blog_app/.dockerignore +51 -0
- data/examples/blog_app/.github/dependabot.yml +12 -0
- data/examples/blog_app/.github/workflows/ci.yml +67 -0
- data/examples/blog_app/.gitignore +30 -0
- data/examples/blog_app/.kamal/hooks/docker-setup.sample +3 -0
- data/examples/blog_app/.kamal/hooks/post-app-boot.sample +3 -0
- data/examples/blog_app/.kamal/hooks/post-deploy.sample +14 -0
- data/examples/blog_app/.kamal/hooks/post-proxy-reboot.sample +3 -0
- data/examples/blog_app/.kamal/hooks/pre-app-boot.sample +3 -0
- data/examples/blog_app/.kamal/hooks/pre-build.sample +51 -0
- data/examples/blog_app/.kamal/hooks/pre-connect.sample +47 -0
- data/examples/blog_app/.kamal/hooks/pre-deploy.sample +122 -0
- data/examples/blog_app/.kamal/hooks/pre-proxy-reboot.sample +3 -0
- data/examples/blog_app/.kamal/secrets +20 -0
- data/examples/blog_app/.rubocop.yml +8 -0
- data/examples/blog_app/.ruby-version +1 -0
- data/examples/blog_app/Dockerfile +76 -0
- data/examples/blog_app/Gemfile +63 -0
- data/examples/blog_app/Gemfile.lock +408 -0
- data/examples/blog_app/README.md +24 -0
- data/examples/blog_app/README_EXAMPLE.md +328 -0
- data/examples/blog_app/Rakefile +6 -0
- data/examples/blog_app/app/assets/images/.keep +0 -0
- data/examples/blog_app/app/assets/stylesheets/application.css +10 -0
- data/examples/blog_app/app/controllers/api/base_controller.rb +61 -0
- data/examples/blog_app/app/controllers/api/v1/posts_controller.rb +158 -0
- data/examples/blog_app/app/controllers/api/v1/users_controller.rb +98 -0
- data/examples/blog_app/app/controllers/application_controller.rb +7 -0
- data/examples/blog_app/app/controllers/concerns/.keep +0 -0
- data/examples/blog_app/app/domains/.keep +0 -0
- data/examples/blog_app/app/domains/exceptions.rb +19 -0
- data/examples/blog_app/app/domains/post_management/events/post_created_event.rb +15 -0
- data/examples/blog_app/app/domains/post_management/events/post_deleted_event.rb +13 -0
- data/examples/blog_app/app/domains/post_management/events/post_updated_event.rb +13 -0
- data/examples/blog_app/app/domains/post_management/handlers/post_notification_handler.rb +33 -0
- data/examples/blog_app/app/domains/post_management/models/post.rb +21 -0
- data/examples/blog_app/app/domains/post_management/policies/post_policy.rb +49 -0
- data/examples/blog_app/app/domains/post_management/post_service.rb +93 -0
- data/examples/blog_app/app/domains/post_management/setup.rb +25 -0
- data/examples/blog_app/app/domains/user_management/events/user_created_event.rb +15 -0
- data/examples/blog_app/app/domains/user_management/events/user_deleted_event.rb +13 -0
- data/examples/blog_app/app/domains/user_management/events/user_updated_event.rb +13 -0
- data/examples/blog_app/app/domains/user_management/handlers/user_welcome_handler.rb +21 -0
- data/examples/blog_app/app/domains/user_management/models/user.rb +11 -0
- data/examples/blog_app/app/domains/user_management/policies/user_policy.rb +49 -0
- data/examples/blog_app/app/domains/user_management/setup.rb +25 -0
- data/examples/blog_app/app/domains/user_management/user_service.rb +93 -0
- data/examples/blog_app/app/events/.keep +0 -0
- data/examples/blog_app/app/events/application_event.rb +18 -0
- data/examples/blog_app/app/handlers/.keep +0 -0
- data/examples/blog_app/app/helpers/application_helper.rb +2 -0
- data/examples/blog_app/app/javascript/application.js +3 -0
- data/examples/blog_app/app/javascript/controllers/application.js +9 -0
- data/examples/blog_app/app/javascript/controllers/hello_controller.js +7 -0
- data/examples/blog_app/app/javascript/controllers/index.js +4 -0
- data/examples/blog_app/app/jobs/application_job.rb +7 -0
- data/examples/blog_app/app/mailers/application_mailer.rb +4 -0
- data/examples/blog_app/app/models/application_record.rb +3 -0
- data/examples/blog_app/app/models/concerns/.keep +0 -0
- data/examples/blog_app/app/models/organization.rb +6 -0
- data/examples/blog_app/app/policies/.keep +0 -0
- data/examples/blog_app/app/policies/application_policy.rb +23 -0
- data/examples/blog_app/app/services/.keep +0 -0
- data/examples/blog_app/app/services/application_service.rb +24 -0
- data/examples/blog_app/app/views/layouts/application.html.erb +29 -0
- data/examples/blog_app/app/views/layouts/mailer.html.erb +13 -0
- data/examples/blog_app/app/views/layouts/mailer.text.erb +1 -0
- data/examples/blog_app/app/views/pwa/manifest.json.erb +22 -0
- data/examples/blog_app/app/views/pwa/service-worker.js +26 -0
- data/examples/blog_app/bin/brakeman +7 -0
- data/examples/blog_app/bin/bundler-audit +6 -0
- data/examples/blog_app/bin/ci +6 -0
- data/examples/blog_app/bin/dev +2 -0
- data/examples/blog_app/bin/docker-entrypoint +8 -0
- data/examples/blog_app/bin/importmap +4 -0
- data/examples/blog_app/bin/jobs +6 -0
- data/examples/blog_app/bin/kamal +27 -0
- data/examples/blog_app/bin/rails +4 -0
- data/examples/blog_app/bin/rake +4 -0
- data/examples/blog_app/bin/rubocop +8 -0
- data/examples/blog_app/bin/setup +35 -0
- data/examples/blog_app/bin/thrust +5 -0
- data/examples/blog_app/config/application.rb +52 -0
- data/examples/blog_app/config/boot.rb +4 -0
- data/examples/blog_app/config/bundler-audit.yml +5 -0
- data/examples/blog_app/config/cable.yml +17 -0
- data/examples/blog_app/config/cache.yml +16 -0
- data/examples/blog_app/config/ci.rb +19 -0
- data/examples/blog_app/config/credentials.yml.enc +1 -0
- data/examples/blog_app/config/database.yml +41 -0
- data/examples/blog_app/config/deploy.yml +120 -0
- data/examples/blog_app/config/environment.rb +5 -0
- data/examples/blog_app/config/environments/development.rb +78 -0
- data/examples/blog_app/config/environments/production.rb +90 -0
- data/examples/blog_app/config/environments/test.rb +53 -0
- data/examples/blog_app/config/importmap.rb +7 -0
- data/examples/blog_app/config/initializers/active_domain.rb +37 -0
- data/examples/blog_app/config/initializers/assets.rb +7 -0
- data/examples/blog_app/config/initializers/content_security_policy.rb +29 -0
- data/examples/blog_app/config/initializers/filter_parameter_logging.rb +8 -0
- data/examples/blog_app/config/initializers/inflections.rb +16 -0
- data/examples/blog_app/config/locales/en.yml +31 -0
- data/examples/blog_app/config/master.key +1 -0
- data/examples/blog_app/config/puma.rb +42 -0
- data/examples/blog_app/config/queue.yml +18 -0
- data/examples/blog_app/config/recurring.yml +15 -0
- data/examples/blog_app/config/routes.rb +27 -0
- data/examples/blog_app/config/storage.yml +27 -0
- data/examples/blog_app/config.ru +6 -0
- data/examples/blog_app/db/cable_schema.rb +11 -0
- data/examples/blog_app/db/cache_schema.rb +12 -0
- data/examples/blog_app/db/migrate/20251230112502_create_organizations.rb +9 -0
- data/examples/blog_app/db/migrate/20251230112503_create_users.rb +12 -0
- data/examples/blog_app/db/migrate/20251230112504_create_posts.rb +14 -0
- data/examples/blog_app/db/queue_schema.rb +129 -0
- data/examples/blog_app/db/schema.rb +46 -0
- data/examples/blog_app/db/seeds.rb +9 -0
- data/examples/blog_app/lib/api_demo.rb +175 -0
- data/examples/blog_app/lib/demo.rb +150 -0
- data/examples/blog_app/lib/tasks/.keep +0 -0
- data/examples/blog_app/log/.keep +0 -0
- data/examples/blog_app/public/400.html +135 -0
- data/examples/blog_app/public/404.html +135 -0
- data/examples/blog_app/public/406-unsupported-browser.html +135 -0
- data/examples/blog_app/public/422.html +135 -0
- data/examples/blog_app/public/500.html +135 -0
- data/examples/blog_app/public/icon.png +0 -0
- data/examples/blog_app/public/icon.svg +3 -0
- data/examples/blog_app/public/robots.txt +1 -0
- data/examples/blog_app/script/.keep +0 -0
- data/examples/blog_app/storage/.keep +0 -0
- data/examples/blog_app/tmp/.keep +0 -0
- data/examples/blog_app/vendor/.keep +0 -0
- data/examples/blog_app/vendor/javascript/.keep +0 -0
- data/lib/generators/active_domain/domain/domain_generator.rb +116 -0
- data/lib/generators/active_domain/domain/templates/events/created_event.rb.tt +15 -0
- data/lib/generators/active_domain/domain/templates/events/deleted_event.rb.tt +13 -0
- data/lib/generators/active_domain/domain/templates/events/updated_event.rb.tt +13 -0
- data/lib/generators/active_domain/domain/templates/policy.rb.tt +49 -0
- data/lib/generators/active_domain/domain/templates/service.rb.tt +93 -0
- data/lib/generators/active_domain/domain/templates/setup.rb.tt +27 -0
- data/lib/generators/active_domain/install/install_generator.rb +58 -0
- data/lib/generators/active_domain/install/templates/README +28 -0
- data/lib/generators/active_domain/install/templates/application_event.rb +18 -0
- data/lib/generators/active_domain/install/templates/application_policy.rb +23 -0
- data/lib/generators/active_domain/install/templates/application_service.rb +24 -0
- data/lib/generators/active_domain/install/templates/initializer.rb +37 -0
- data/lib/smart_domain/configuration.rb +97 -0
- data/lib/smart_domain/domain/exceptions.rb +164 -0
- data/lib/smart_domain/domain/policy.rb +215 -0
- data/lib/smart_domain/domain/service.rb +230 -0
- data/lib/smart_domain/event/adapters/memory.rb +110 -0
- data/lib/smart_domain/event/base.rb +176 -0
- data/lib/smart_domain/event/handler.rb +98 -0
- data/lib/smart_domain/event/mixins.rb +156 -0
- data/lib/smart_domain/event/registration.rb +136 -0
- data/lib/smart_domain/generators/domain_generator.rb +4 -0
- data/lib/smart_domain/generators/install_generator.rb +4 -0
- data/lib/smart_domain/handlers/audit_handler.rb +216 -0
- data/lib/smart_domain/handlers/metrics_handler.rb +104 -0
- data/lib/smart_domain/integration/active_record.rb +169 -0
- data/lib/smart_domain/integration/multi_tenancy.rb +115 -0
- data/lib/smart_domain/railtie.rb +62 -0
- data/lib/smart_domain/tasks/domains.rake +43 -0
- data/lib/smart_domain/version.rb +5 -0
- data/lib/smart_domain.rb +77 -0
- data/smart_domain.gemspec +53 -0
- 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,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
|
|
File without changes
|
|
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
|