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,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module <%= domain_module_name %>
|
|
4
|
+
# Service for <%= class_name %> business logic
|
|
5
|
+
#
|
|
6
|
+
# This service encapsulates all business operations for <%= plural_name %>.
|
|
7
|
+
# Controllers should delegate to this service rather than directly
|
|
8
|
+
# manipulating <%= class_name %> models.
|
|
9
|
+
class <%= class_name %>Service < ApplicationService
|
|
10
|
+
# Create a new <%= file_name %>
|
|
11
|
+
#
|
|
12
|
+
# @param attributes [Hash] <%= class_name %> attributes
|
|
13
|
+
# @return [<%= class_name %>] Created <%= file_name %>
|
|
14
|
+
# @raise [ActiveDomain::Domain::AlreadyExistsError] If <%= file_name %> already exists
|
|
15
|
+
# @raise [ActiveDomain::Domain::ValidationError] If validation fails
|
|
16
|
+
def create_<%= file_name %>(attributes)
|
|
17
|
+
# Example business rule validation
|
|
18
|
+
# if <%= class_name %>.exists?(email: attributes[:email])
|
|
19
|
+
# raise ActiveDomain::Domain::AlreadyExistsError.new('<%= class_name %>', 'email', attributes[:email])
|
|
20
|
+
# end
|
|
21
|
+
|
|
22
|
+
<%= class_name %>.transaction do
|
|
23
|
+
<%= file_name %> = <%= class_name %>.create!(attributes)
|
|
24
|
+
|
|
25
|
+
# Event is published via ActiveRecord integration
|
|
26
|
+
# Or manually:
|
|
27
|
+
# event = build_event(<%= class_name %>CreatedEvent,
|
|
28
|
+
# event_type: '<%= file_name %>.created',
|
|
29
|
+
# aggregate_id: <%= file_name %>.id,
|
|
30
|
+
# aggregate_type: '<%= class_name %>',
|
|
31
|
+
# <%= file_name %>_id: <%= file_name %>.id
|
|
32
|
+
# )
|
|
33
|
+
# publish_after_commit(event)
|
|
34
|
+
|
|
35
|
+
log(:info, "<%= class_name %> created", <%= file_name %>_id: <%= file_name %>.id)
|
|
36
|
+
<%= file_name %>
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Update an existing <%= file_name %>
|
|
41
|
+
#
|
|
42
|
+
# @param <%= file_name %>_id [Integer, String] <%= class_name %> ID
|
|
43
|
+
# @param attributes [Hash] Attributes to update
|
|
44
|
+
# @return [<%= class_name %>] Updated <%= file_name %>
|
|
45
|
+
# @raise [ActiveRecord::RecordNotFound] If <%= file_name %> not found
|
|
46
|
+
# @raise [ActiveDomain::Domain::UnauthorizedError] If not authorized
|
|
47
|
+
def update_<%= file_name %>(<%= file_name %>_id, attributes)
|
|
48
|
+
<%= file_name %> = <%= class_name %>.find(<%= file_name %>_id)
|
|
49
|
+
|
|
50
|
+
# Authorization check
|
|
51
|
+
policy = <%= class_name %>Policy.new(current_user, <%= file_name %>)
|
|
52
|
+
authorize!(policy, :update?)
|
|
53
|
+
|
|
54
|
+
<%= class_name %>.transaction do
|
|
55
|
+
<%= file_name %>.update!(attributes)
|
|
56
|
+
|
|
57
|
+
# Event published via ActiveRecord integration
|
|
58
|
+
log(:info, "<%= class_name %> updated", <%= file_name %>_id: <%= file_name %>.id, changes: <%= file_name %>.saved_changes.keys)
|
|
59
|
+
<%= file_name %>
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Delete a <%= file_name %>
|
|
64
|
+
#
|
|
65
|
+
# @param <%= file_name %>_id [Integer, String] <%= class_name %> ID
|
|
66
|
+
# @return [Boolean] True if deleted
|
|
67
|
+
# @raise [ActiveRecord::RecordNotFound] If <%= file_name %> not found
|
|
68
|
+
# @raise [ActiveDomain::Domain::UnauthorizedError] If not authorized
|
|
69
|
+
def delete_<%= file_name %>(<%= file_name %>_id)
|
|
70
|
+
<%= file_name %> = <%= class_name %>.find(<%= file_name %>_id)
|
|
71
|
+
|
|
72
|
+
# Authorization check
|
|
73
|
+
policy = <%= class_name %>Policy.new(current_user, <%= file_name %>)
|
|
74
|
+
authorize!(policy, :destroy?)
|
|
75
|
+
|
|
76
|
+
<%= class_name %>.transaction do
|
|
77
|
+
<%= file_name %>.destroy!
|
|
78
|
+
|
|
79
|
+
# Event published via ActiveRecord integration
|
|
80
|
+
log(:info, "<%= class_name %> deleted", <%= file_name %>_id: <%= file_name %>_id)
|
|
81
|
+
true
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# List <%= plural_name %> with policy scope
|
|
86
|
+
#
|
|
87
|
+
# @param scope [ActiveRecord::Relation] Optional base scope
|
|
88
|
+
# @return [ActiveRecord::Relation] Scoped <%= plural_name %>
|
|
89
|
+
def list_<%= plural_name %>(scope = <%= class_name %>.all)
|
|
90
|
+
policy_scope(scope, <%= class_name %>Policy)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module <%= domain_module_name %>
|
|
4
|
+
# Setup event handlers for <%= file_name %> domain
|
|
5
|
+
#
|
|
6
|
+
# This file is automatically loaded by ActiveDomain::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
|
+
ActiveDomain::Event::Registration.register_standard_handlers(
|
|
12
|
+
domain: '<%= file_name %>',
|
|
13
|
+
events: %w[created updated deleted],
|
|
14
|
+
include_audit: true,
|
|
15
|
+
include_metrics: true
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Register custom handlers here
|
|
19
|
+
# Example:
|
|
20
|
+
#
|
|
21
|
+
# email_handler = <%= class_name %>EmailHandler.new
|
|
22
|
+
# ActiveDomain::Event.bus.subscribe('<%= file_name %>.created', email_handler)
|
|
23
|
+
# ActiveDomain::Event.bus.subscribe('<%= file_name %>.updated', email_handler)
|
|
24
|
+
|
|
25
|
+
Rails.logger.info "[<%= domain_module_name %>] Domain setup complete"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module SmartDomain
|
|
6
|
+
module Generators
|
|
7
|
+
# Generator for installing SmartDomain in a Rails application
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# rails generate smart_domain:install
|
|
11
|
+
#
|
|
12
|
+
# Creates:
|
|
13
|
+
# - config/initializers/smart_domain.rb
|
|
14
|
+
# - app/domains/ directory
|
|
15
|
+
# - app/events/ directory
|
|
16
|
+
# - app/handlers/ directory
|
|
17
|
+
# - app/policies/ directory
|
|
18
|
+
class InstallGenerator < Rails::Generators::Base
|
|
19
|
+
source_root File.expand_path("templates", __dir__)
|
|
20
|
+
|
|
21
|
+
desc "Install SmartDomain into your Rails application"
|
|
22
|
+
|
|
23
|
+
# Create initializer
|
|
24
|
+
def create_initializer
|
|
25
|
+
template "initializer.rb", "config/initializers/smart_domain.rb"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Create directory structure
|
|
29
|
+
def create_directory_structure
|
|
30
|
+
create_file "app/domains/.keep"
|
|
31
|
+
create_file "app/events/.keep"
|
|
32
|
+
create_file "app/handlers/.keep"
|
|
33
|
+
create_file "app/policies/.keep"
|
|
34
|
+
create_file "app/services/.keep"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Create base event class
|
|
38
|
+
def create_application_event
|
|
39
|
+
template "application_event.rb", "app/events/application_event.rb"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Create base policy class
|
|
43
|
+
def create_application_policy
|
|
44
|
+
template "application_policy.rb", "app/policies/application_policy.rb"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Create base service class
|
|
48
|
+
def create_application_service
|
|
49
|
+
template "application_service.rb", "app/services/application_service.rb"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Show post-install message
|
|
53
|
+
def show_readme
|
|
54
|
+
readme "README" if behavior == :invoke
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
===============================================================================
|
|
2
|
+
|
|
3
|
+
ActiveDomain has been installed!
|
|
4
|
+
|
|
5
|
+
Created files:
|
|
6
|
+
config/initializers/active_domain.rb - Configuration
|
|
7
|
+
app/events/application_event.rb - Base event class
|
|
8
|
+
app/policies/application_policy.rb - Base policy class
|
|
9
|
+
app/services/application_service.rb - Base service class
|
|
10
|
+
|
|
11
|
+
Created directories:
|
|
12
|
+
app/domains/ - Domain modules go here
|
|
13
|
+
app/events/ - Domain events go here
|
|
14
|
+
app/handlers/ - Custom event handlers go here
|
|
15
|
+
app/policies/ - Domain policies go here
|
|
16
|
+
app/services/ - Domain services go here
|
|
17
|
+
|
|
18
|
+
Next steps:
|
|
19
|
+
|
|
20
|
+
1. Review and configure config/initializers/active_domain.rb
|
|
21
|
+
|
|
22
|
+
2. Generate your first domain:
|
|
23
|
+
rails generate active_domain:domain User
|
|
24
|
+
|
|
25
|
+
3. Read the documentation:
|
|
26
|
+
https://github.com/rachidalmaach/active_domain
|
|
27
|
+
|
|
28
|
+
===============================================================================
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SmartDomain configuration
|
|
4
|
+
SmartDomain.configure do |config|
|
|
5
|
+
# Event bus adapter (:memory, :redis, :active_job)
|
|
6
|
+
# :memory is synchronous and suitable for development/testing
|
|
7
|
+
# :redis and :active_job are asynchronous and suitable for production
|
|
8
|
+
config.event_bus_adapter = :memory
|
|
9
|
+
|
|
10
|
+
# Enable automatic writes to audit_events table
|
|
11
|
+
# Set to true if you have an AuditEvent model for compliance
|
|
12
|
+
config.audit_table_enabled = false
|
|
13
|
+
|
|
14
|
+
# Enable multi-tenancy support
|
|
15
|
+
# Set to true if your application is multi-tenant
|
|
16
|
+
config.multi_tenancy_enabled = false
|
|
17
|
+
|
|
18
|
+
# Key used for tenant identification (e.g., :organization_id, :account_id)
|
|
19
|
+
config.tenant_key = :organization_id
|
|
20
|
+
|
|
21
|
+
# Use ActiveJob for asynchronous event handling
|
|
22
|
+
# Requires config.event_bus_adapter to support async (not :memory)
|
|
23
|
+
config.async_handlers = false
|
|
24
|
+
|
|
25
|
+
# Logger instance
|
|
26
|
+
config.logger = Rails.logger
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Example: Register standard handlers for a domain
|
|
30
|
+
# Uncomment and modify for your domains:
|
|
31
|
+
#
|
|
32
|
+
# SmartDomain::Event::Registration.register_standard_handlers(
|
|
33
|
+
# domain: 'user',
|
|
34
|
+
# events: %w[created updated deleted],
|
|
35
|
+
# include_audit: true,
|
|
36
|
+
# include_metrics: true
|
|
37
|
+
# )
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
|
|
5
|
+
module SmartDomain
|
|
6
|
+
# Configuration for SmartDomain gem.
|
|
7
|
+
#
|
|
8
|
+
# Configure the gem in an initializer:
|
|
9
|
+
#
|
|
10
|
+
# @example config/initializers/smart_domain.rb
|
|
11
|
+
# SmartDomain.configure do |config|
|
|
12
|
+
# config.event_bus_adapter = :memory
|
|
13
|
+
# config.audit_table_enabled = true
|
|
14
|
+
# config.multi_tenancy_enabled = true
|
|
15
|
+
# config.tenant_key = :organization_id
|
|
16
|
+
# config.async_handlers = false
|
|
17
|
+
# config.logger = Rails.logger
|
|
18
|
+
# end
|
|
19
|
+
class Configuration
|
|
20
|
+
# Event bus adapter to use (:memory, :redis, :active_job)
|
|
21
|
+
# @return [Symbol, Object] Adapter symbol or adapter instance
|
|
22
|
+
attr_accessor :event_bus_adapter
|
|
23
|
+
|
|
24
|
+
# Enable automatic writes to audit_events table
|
|
25
|
+
# @return [Boolean]
|
|
26
|
+
attr_accessor :audit_table_enabled
|
|
27
|
+
|
|
28
|
+
# Enable multi-tenancy support
|
|
29
|
+
# @return [Boolean]
|
|
30
|
+
attr_accessor :multi_tenancy_enabled
|
|
31
|
+
|
|
32
|
+
# Key used for tenant identification (e.g., :organization_id, :account_id)
|
|
33
|
+
# @return [Symbol]
|
|
34
|
+
attr_accessor :tenant_key
|
|
35
|
+
|
|
36
|
+
# Use ActiveJob for asynchronous event handling
|
|
37
|
+
# @return [Boolean]
|
|
38
|
+
attr_accessor :async_handlers
|
|
39
|
+
|
|
40
|
+
# Logger instance for SmartDomain
|
|
41
|
+
# @return [Logger]
|
|
42
|
+
attr_accessor :logger
|
|
43
|
+
|
|
44
|
+
# Initialize configuration with defaults
|
|
45
|
+
def initialize
|
|
46
|
+
@event_bus_adapter = nil # Will use Memory adapter
|
|
47
|
+
@audit_table_enabled = false
|
|
48
|
+
@multi_tenancy_enabled = false
|
|
49
|
+
@tenant_key = :organization_id
|
|
50
|
+
@async_handlers = false
|
|
51
|
+
@logger = Logger.new($stdout)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Check if audit table writes are enabled
|
|
55
|
+
# @return [Boolean]
|
|
56
|
+
def audit_table_enabled?
|
|
57
|
+
@audit_table_enabled == true
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Check if multi-tenancy is enabled
|
|
61
|
+
# @return [Boolean]
|
|
62
|
+
def multi_tenancy_enabled?
|
|
63
|
+
@multi_tenancy_enabled == true
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Check if async handlers are enabled
|
|
67
|
+
# @return [Boolean]
|
|
68
|
+
def async_handlers?
|
|
69
|
+
@async_handlers == true
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Get or create the configuration instance
|
|
74
|
+
# @return [SmartDomain::Configuration]
|
|
75
|
+
def self.configuration
|
|
76
|
+
@configuration ||= Configuration.new
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Configure SmartDomain
|
|
80
|
+
#
|
|
81
|
+
# @example
|
|
82
|
+
# SmartDomain.configure do |config|
|
|
83
|
+
# config.event_bus_adapter = :memory
|
|
84
|
+
# config.audit_table_enabled = true
|
|
85
|
+
# end
|
|
86
|
+
#
|
|
87
|
+
# @yield [Configuration] The configuration object
|
|
88
|
+
def self.configure
|
|
89
|
+
yield(configuration)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Reset configuration to defaults (useful for testing)
|
|
93
|
+
# @api private
|
|
94
|
+
def self.reset_configuration!
|
|
95
|
+
@configuration = Configuration.new
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SmartDomain
|
|
4
|
+
module Domain
|
|
5
|
+
# Base exception for all domain errors
|
|
6
|
+
#
|
|
7
|
+
# Domain exceptions represent business rule violations and should be
|
|
8
|
+
# handled differently from technical errors (like database errors).
|
|
9
|
+
#
|
|
10
|
+
# @example Define domain-specific exceptions
|
|
11
|
+
# class UserNotFoundError < SmartDomain::Domain::Error
|
|
12
|
+
# def initialize(user_id)
|
|
13
|
+
# super("User not found: #{user_id}")
|
|
14
|
+
# end
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# class UserAlreadyExistsError < SmartDomain::Domain::Error
|
|
18
|
+
# def initialize(email)
|
|
19
|
+
# super("User with email #{email} already exists")
|
|
20
|
+
# end
|
|
21
|
+
# end
|
|
22
|
+
class Error < StandardError
|
|
23
|
+
attr_reader :code, :details
|
|
24
|
+
|
|
25
|
+
# Initialize domain error
|
|
26
|
+
#
|
|
27
|
+
# @param message [String] Error message
|
|
28
|
+
# @param code [Symbol, nil] Error code for programmatic handling
|
|
29
|
+
# @param details [Hash, nil] Additional error details
|
|
30
|
+
def initialize(message, code: nil, details: {})
|
|
31
|
+
super(message)
|
|
32
|
+
@code = code
|
|
33
|
+
@details = details
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Convert error to hash (useful for API responses)
|
|
37
|
+
#
|
|
38
|
+
# @return [Hash] Error representation
|
|
39
|
+
def to_h
|
|
40
|
+
{
|
|
41
|
+
error: self.class.name,
|
|
42
|
+
message: message,
|
|
43
|
+
code: code,
|
|
44
|
+
details: details
|
|
45
|
+
}.compact
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
alias to_hash to_h
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Error raised when an entity is not found
|
|
52
|
+
#
|
|
53
|
+
# @example
|
|
54
|
+
# raise SmartDomain::Domain::NotFoundError.new('User', user_id)
|
|
55
|
+
class NotFoundError < Error
|
|
56
|
+
def initialize(entity_type, entity_id)
|
|
57
|
+
super(
|
|
58
|
+
"#{entity_type} not found: #{entity_id}",
|
|
59
|
+
code: :not_found,
|
|
60
|
+
details: { entity_type: entity_type, entity_id: entity_id }
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Error raised when an entity already exists
|
|
66
|
+
#
|
|
67
|
+
# @example
|
|
68
|
+
# raise SmartDomain::Domain::AlreadyExistsError.new('User', 'email', email)
|
|
69
|
+
class AlreadyExistsError < Error
|
|
70
|
+
def initialize(entity_type, attribute, value)
|
|
71
|
+
super(
|
|
72
|
+
"#{entity_type} with #{attribute} '#{value}' already exists",
|
|
73
|
+
code: :already_exists,
|
|
74
|
+
details: { entity_type: entity_type, attribute: attribute, value: value }
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Error raised when a business rule is violated
|
|
80
|
+
#
|
|
81
|
+
# @example
|
|
82
|
+
# raise SmartDomain::Domain::BusinessRuleError.new(
|
|
83
|
+
# 'Cannot delete user with active orders'
|
|
84
|
+
# )
|
|
85
|
+
class BusinessRuleError < Error
|
|
86
|
+
def initialize(message, code: :business_rule_violation, details: {})
|
|
87
|
+
super(message, code: code, details: details)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Error raised when an invalid state transition is attempted
|
|
92
|
+
#
|
|
93
|
+
# @example
|
|
94
|
+
# raise SmartDomain::Domain::InvalidStateError.new(
|
|
95
|
+
# 'User',
|
|
96
|
+
# from: 'suspended',
|
|
97
|
+
# to: 'active',
|
|
98
|
+
# reason: 'User must be pending to activate'
|
|
99
|
+
# )
|
|
100
|
+
class InvalidStateError < Error
|
|
101
|
+
def initialize(entity_type, from:, to:, reason: nil)
|
|
102
|
+
message = "Invalid state transition for #{entity_type}: #{from} -> #{to}"
|
|
103
|
+
message += " (#{reason})" if reason
|
|
104
|
+
|
|
105
|
+
super(
|
|
106
|
+
message,
|
|
107
|
+
code: :invalid_state,
|
|
108
|
+
details: { entity_type: entity_type, from: from, to: to, reason: reason }
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Error raised when validation fails
|
|
114
|
+
#
|
|
115
|
+
# @example
|
|
116
|
+
# raise SmartDomain::Domain::ValidationError.new(
|
|
117
|
+
# 'User validation failed',
|
|
118
|
+
# errors: { email: ['is required'], name: ['is too short'] }
|
|
119
|
+
# )
|
|
120
|
+
class ValidationError < Error
|
|
121
|
+
def initialize(message, errors: {})
|
|
122
|
+
super(
|
|
123
|
+
message,
|
|
124
|
+
code: :validation_failed,
|
|
125
|
+
details: { validation_errors: errors }
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Error raised when authorization fails
|
|
131
|
+
#
|
|
132
|
+
# @example
|
|
133
|
+
# raise SmartDomain::Domain::UnauthorizedError.new(
|
|
134
|
+
# 'User does not have permission to delete this resource'
|
|
135
|
+
# )
|
|
136
|
+
class UnauthorizedError < Error
|
|
137
|
+
def initialize(message = "Unauthorized", action: nil, resource: nil)
|
|
138
|
+
super(
|
|
139
|
+
message,
|
|
140
|
+
code: :unauthorized,
|
|
141
|
+
details: { action: action, resource: resource }.compact
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Error raised when a required dependency is not available
|
|
147
|
+
#
|
|
148
|
+
# @example
|
|
149
|
+
# raise SmartDomain::Domain::DependencyError.new(
|
|
150
|
+
# 'Redis',
|
|
151
|
+
# 'Redis is required for caching'
|
|
152
|
+
# )
|
|
153
|
+
class DependencyError < Error
|
|
154
|
+
def initialize(dependency_name, message = nil)
|
|
155
|
+
message ||= "Required dependency not available: #{dependency_name}"
|
|
156
|
+
super(
|
|
157
|
+
message,
|
|
158
|
+
code: :dependency_missing,
|
|
159
|
+
details: { dependency: dependency_name }
|
|
160
|
+
)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|