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,46 @@
1
+ # This file is auto-generated from the current state of the database. Instead
2
+ # of editing this file, please use the migrations feature of Active Record to
3
+ # incrementally modify your database, and then regenerate this schema definition.
4
+ #
5
+ # This file is the source Rails uses to define your schema when running `bin/rails
6
+ # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
7
+ # be faster and is potentially less error prone than running all of your
8
+ # migrations from scratch. Old migrations may fail to apply correctly if those
9
+ # migrations use external dependencies or application code.
10
+ #
11
+ # It's strongly recommended that you check this file into your version control system.
12
+
13
+ ActiveRecord::Schema[8.1].define(version: 2025_12_30_112504) do
14
+ create_table "organizations", force: :cascade do |t|
15
+ t.datetime "created_at", null: false
16
+ t.string "name"
17
+ t.datetime "updated_at", null: false
18
+ end
19
+
20
+ create_table "posts", force: :cascade do |t|
21
+ t.text "body"
22
+ t.datetime "created_at", null: false
23
+ t.integer "organization_id", null: false
24
+ t.boolean "published"
25
+ t.datetime "published_at"
26
+ t.string "title"
27
+ t.datetime "updated_at", null: false
28
+ t.integer "user_id", null: false
29
+ t.index ["organization_id"], name: "index_posts_on_organization_id"
30
+ t.index ["user_id"], name: "index_posts_on_user_id"
31
+ end
32
+
33
+ create_table "users", force: :cascade do |t|
34
+ t.datetime "created_at", null: false
35
+ t.string "email"
36
+ t.string "name"
37
+ t.integer "organization_id", null: false
38
+ t.string "role"
39
+ t.datetime "updated_at", null: false
40
+ t.index ["organization_id"], name: "index_users_on_organization_id"
41
+ end
42
+
43
+ add_foreign_key "posts", "organizations"
44
+ add_foreign_key "posts", "users"
45
+ add_foreign_key "users", "organizations"
46
+ end
@@ -0,0 +1,9 @@
1
+ # This file should ensure the existence of records required to run the application in every environment (production,
2
+ # development, test). The code here should be idempotent so that it can be executed at any point in every environment.
3
+ # The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup).
4
+ #
5
+ # Example:
6
+ #
7
+ # ["Action", "Comedy", "Drama", "Horror"].each do |genre_name|
8
+ # MovieGenre.find_or_create_by!(name: genre_name)
9
+ # end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ # API Demo script showing controller → service → events flow
4
+ #
5
+ # Run with: rails runner lib/api_demo.rb
6
+
7
+ puts "\n" + "=" * 80
8
+ puts "SmartDomain API Demo - Controller → Service → Events"
9
+ puts "=" * 80 + "\n\n"
10
+
11
+ # Clean up
12
+ Organization.destroy_all
13
+ puts "Cleaned up existing data\n\n"
14
+
15
+ # Create organization
16
+ org = Organization.create!(name: "API Demo Corp")
17
+ puts "Step 1: Created organization: #{org.name}"
18
+ puts " - ID: #{org.id}\n\n"
19
+
20
+ # Simulate API request context
21
+ module ApiContext
22
+ class << self
23
+ attr_accessor :current_organization, :current_user
24
+ end
25
+ end
26
+
27
+ ApiContext.current_organization = org
28
+
29
+ # Create admin user
30
+ admin = org.users.create!(
31
+ email: "admin@apidemo.com",
32
+ name: "API Admin",
33
+ role: "admin"
34
+ )
35
+
36
+ ApiContext.current_user = admin
37
+ puts "Step 2: Created admin user: #{admin.name} <#{admin.email}>\n\n"
38
+
39
+ # Simulate UsersController#create
40
+ puts "Step 3: Simulating POST /api/v1/users (UsersController#create)"
41
+ puts " - Controller receives request"
42
+ puts " - Controller delegates to UserManagement::UserService"
43
+
44
+ SmartDomain::Integration::TenantContext.with_tenant(org.id) do
45
+ service = UserManagement::UserService.new(
46
+ current_user: admin,
47
+ organization_id: org.id
48
+ )
49
+
50
+ user = service.create_user(
51
+ email: "john@apidemo.com",
52
+ name: "John Doe",
53
+ role: "editor"
54
+ )
55
+
56
+ puts " - Service created user: #{user.name}"
57
+ puts " - Service published UserCreatedEvent"
58
+ puts " - Handlers triggered: AuditHandler, MetricsHandler, UserWelcomeHandler"
59
+ puts " - Controller returns JSON response\n\n"
60
+
61
+ # Simulate PostsController#create
62
+ puts "Step 4: Simulating POST /api/v1/posts (PostsController#create)"
63
+ puts " - Controller receives request"
64
+ puts " - Controller delegates to PostManagement::PostService"
65
+
66
+ post_service = PostManagement::PostService.new(
67
+ current_user: user,
68
+ organization_id: org.id
69
+ )
70
+
71
+ post = post_service.create_post(
72
+ title: "My First API Post",
73
+ body: "This post was created via the API!",
74
+ user_id: user.id,
75
+ organization_id: org.id,
76
+ published: false
77
+ )
78
+
79
+ puts " - Service created post: #{post.title}"
80
+ puts " - Service published PostCreatedEvent"
81
+ puts " - Handlers triggered: AuditHandler, MetricsHandler"
82
+ puts " - Controller returns JSON response\n\n"
83
+
84
+ # Simulate PostsController#publish
85
+ puts "Step 5: Simulating POST /api/v1/posts/#{post.id}/publish"
86
+ puts " - Controller receives request"
87
+ puts " - Controller checks authorization (PostPolicy)"
88
+ puts " - Controller calls post.publish!"
89
+
90
+ old_published = post.published
91
+ old_published_at = post.published_at
92
+ post.publish!
93
+
94
+ # Manually publish event (as controller would)
95
+ event = PostUpdatedEvent.new(
96
+ event_type: "post.updated",
97
+ aggregate_id: post.id.to_s,
98
+ aggregate_type: "Post",
99
+ organization_id: org.id.to_s,
100
+ actor_id: user.id.to_s,
101
+ actor_email: user.email,
102
+ post_id: post.id.to_s,
103
+ changed_fields: ["published", "published_at"],
104
+ old_values: { "published" => old_published, "published_at" => old_published_at },
105
+ new_values: { "published" => true, "published_at" => post.published_at.iso8601 }
106
+ )
107
+
108
+ SmartDomain::Event.bus.publish(event)
109
+
110
+ puts " - Controller published PostUpdatedEvent with change tracking"
111
+ puts " - Changed fields: published, published_at"
112
+ puts " - Handlers triggered: AuditHandler, MetricsHandler, PostNotificationHandler"
113
+ puts " - Controller returns JSON response\n\n"
114
+
115
+ # Simulate GET /api/v1/posts (index)
116
+ puts "Step 6: Simulating GET /api/v1/posts (PostsController#index)"
117
+ puts " - Controller receives request"
118
+ puts " - Controller queries with TenantContext.with_tenant"
119
+
120
+ posts = Post.all
121
+ puts " - Found #{posts.count} post(s) in organization #{org.name}"
122
+ posts.each do |p|
123
+ puts " * #{p.title} (#{p.published? ? 'Published' : 'Draft'})"
124
+ end
125
+ puts " - Controller returns JSON response\n\n"
126
+
127
+ # Simulate authorization failure
128
+ puts "Step 7: Demonstrating authorization with policies"
129
+
130
+ # Create viewer user
131
+ viewer = org.users.create!(
132
+ email: "viewer@apidemo.com",
133
+ name: "Viewer User",
134
+ role: "viewer"
135
+ )
136
+
137
+ admin_policy = PostPolicy.new(admin, post)
138
+ viewer_policy = PostPolicy.new(viewer, post)
139
+
140
+ puts " - Admin attempts to delete post:"
141
+ puts " PostPolicy.new(admin, post).destroy? => #{admin_policy.destroy?}"
142
+
143
+ puts " - Viewer attempts to delete post:"
144
+ puts " PostPolicy.new(viewer, post).destroy? => #{viewer_policy.destroy?}"
145
+ puts " → Would raise SmartDomain::Domain::Exceptions::UnauthorizedError"
146
+ puts " → Controller catches and returns 403 Forbidden\n\n"
147
+ end
148
+
149
+ puts "=" * 80
150
+ puts "API Demo Summary"
151
+ puts "=" * 80
152
+ puts "\nComplete request flow demonstrated:"
153
+ puts "1. ✓ HTTP Request → Controller (Interface Layer)"
154
+ puts "2. ✓ Controller → Domain Service (Business Logic Layer)"
155
+ puts "3. ✓ Service → Model + Events (Domain Layer)"
156
+ puts "4. ✓ Events → Handlers (Side Effects Layer)"
157
+ puts "5. ✓ Controller ← JSON Response"
158
+ puts "\nKey patterns:"
159
+ puts "- Controllers are thin adapters (interface layer)"
160
+ puts "- Domain logic lives in services (framework-agnostic)"
161
+ puts "- Events published automatically or manually"
162
+ puts "- Policies enforce authorization"
163
+ puts "- Multi-tenancy ensures data isolation"
164
+ puts "\nAPI Endpoints available:"
165
+ puts "- GET /api/v1/users"
166
+ puts "- POST /api/v1/users"
167
+ puts "- PATCH /api/v1/users/:id"
168
+ puts "- DELETE /api/v1/users/:id"
169
+ puts "- GET /api/v1/posts"
170
+ puts "- POST /api/v1/posts"
171
+ puts "- PATCH /api/v1/posts/:id"
172
+ puts "- DELETE /api/v1/posts/:id"
173
+ puts "- POST /api/v1/posts/:id/publish"
174
+ puts "- POST /api/v1/posts/:id/unpublish"
175
+ puts "=" * 80 + "\n\n"
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Demo script showing SmartDomain features
4
+ #
5
+ # Run with: rails runner lib/demo.rb
6
+
7
+ puts "\n" + "=" * 80
8
+ puts "SmartDomain Gem - Feature Demonstration"
9
+ puts "=" * 80 + "\n\n"
10
+
11
+ # Clean up any existing data
12
+ Organization.destroy_all
13
+ puts "Cleaned up existing data\n\n"
14
+
15
+ # Step 1: Create an organization (multi-tenancy)
16
+ puts "Step 1: Creating organization (multi-tenancy setup)..."
17
+ org = Organization.create!(name: "Acme Corp")
18
+ puts "✓ Created organization: #{org.name} (ID: #{org.id})\n\n"
19
+
20
+ # Step 2: Create a user using the domain service
21
+ puts "Step 2: Creating user via UserService (demonstrates domain service + events)..."
22
+ SmartDomain::Integration::TenantContext.with_tenant(org.id) do
23
+ service = UserManagement::UserService.new(
24
+ current_user: nil,
25
+ organization_id: org.id
26
+ )
27
+
28
+ user_attributes = {
29
+ email: "john@acme.com",
30
+ name: "John Doe",
31
+ role: "admin",
32
+ organization_id: org.id
33
+ }
34
+
35
+ user = service.create_user(user_attributes)
36
+ puts "✓ Created user: #{user.name} <#{user.email}>"
37
+ puts " - Role: #{user.role}"
38
+ puts " - Organization: #{org.name}"
39
+ puts " - Events published: user.created"
40
+ puts " - Handlers triggered: AuditHandler, MetricsHandler, UserWelcomeHandler"
41
+ puts "\n"
42
+
43
+ # Step 3: Create a post
44
+ puts "Step 3: Creating a blog post..."
45
+ post_service = PostManagement::PostService.new(
46
+ current_user: user,
47
+ organization_id: org.id
48
+ )
49
+
50
+ post = post_service.create_post({
51
+ title: "Getting Started with SmartDomain",
52
+ body: "SmartDomain brings DDD and EDA patterns to Rails...",
53
+ user_id: user.id,
54
+ organization_id: org.id,
55
+ published: false
56
+ })
57
+
58
+ puts "✓ Created post: #{post.title}"
59
+ puts " - Author: #{post.user.name}"
60
+ puts " - Status: Draft"
61
+ puts " - Events published: post.created"
62
+ puts "\n"
63
+
64
+ # Step 4: Publish the post (triggers update event)
65
+ puts "Step 4: Publishing the post (demonstrates change tracking)..."
66
+ post.publish!
67
+
68
+ # Create event with change tracking
69
+ event = PostUpdatedEvent.new(
70
+ event_type: "post.updated",
71
+ aggregate_id: post.id.to_s,
72
+ aggregate_type: "Post",
73
+ organization_id: org.id.to_s,
74
+ actor_id: user.id.to_s,
75
+ actor_email: user.email,
76
+ post_id: post.id.to_s,
77
+ changed_fields: ["published", "published_at"],
78
+ old_values: { "published" => false, "published_at" => nil },
79
+ new_values: { "published" => true, "published_at" => post.published_at.iso8601 }
80
+ )
81
+
82
+ SmartDomain::Event.bus.publish(event)
83
+
84
+ puts "✓ Published post: #{post.title}"
85
+ puts " - Published at: #{post.published_at}"
86
+ puts " - Events published: post.updated"
87
+ puts " - Changed fields: published, published_at"
88
+ puts " - Handlers triggered: AuditHandler, MetricsHandler, PostNotificationHandler"
89
+ puts "\n"
90
+
91
+ # Step 5: Demonstrate authorization with policies
92
+ puts "Step 5: Demonstrating authorization policies..."
93
+
94
+ # Create a viewer user
95
+ viewer = User.create!(
96
+ email: "jane@acme.com",
97
+ name: "Jane Smith",
98
+ role: "viewer",
99
+ organization_id: org.id
100
+ )
101
+
102
+ admin_policy = PostPolicy.new(user, post)
103
+ viewer_policy = PostPolicy.new(viewer, post)
104
+
105
+ puts "✓ Authorization checks:"
106
+ puts " - Admin can update post: #{admin_policy.update?}"
107
+ puts " - Admin can destroy post: #{admin_policy.destroy?}"
108
+ puts " - Viewer can view post: #{viewer_policy.show?}"
109
+ puts " - Viewer can update post: #{viewer_policy.update?}"
110
+ puts " - Viewer can destroy post: #{viewer_policy.destroy?}"
111
+ puts "\n"
112
+
113
+ # Step 6: Multi-tenancy isolation
114
+ puts "Step 6: Demonstrating multi-tenancy isolation..."
115
+ other_org = Organization.create!(name: "Other Corp")
116
+
117
+ SmartDomain::Integration::TenantContext.with_tenant(other_org.id) do
118
+ other_user = User.create!(
119
+ email: "bob@other.com",
120
+ name: "Bob Johnson",
121
+ role: "admin",
122
+ organization_id: other_org.id
123
+ )
124
+
125
+ puts "✓ Created second organization: #{other_org.name}"
126
+ puts " - Users in Acme Corp: #{org.users.count}"
127
+ puts " - Users in Other Corp: #{other_org.users.count}"
128
+ puts " - Posts in Acme Corp: #{org.posts.count}"
129
+ puts " - Posts in Other Corp: #{other_org.posts.count}"
130
+ puts " - Data is properly isolated by organization_id"
131
+ end
132
+ puts "\n"
133
+ end
134
+
135
+ # Summary
136
+ puts "=" * 80
137
+ puts "Demo Summary"
138
+ puts "=" * 80
139
+ puts "\nFeatures demonstrated:"
140
+ puts "1. ✓ Multi-tenancy with TenantContext"
141
+ puts "2. ✓ Domain services for business logic"
142
+ puts "3. ✓ Event-driven architecture with domain events"
143
+ puts "4. ✓ Generic handlers (70% boilerplate reduction)"
144
+ puts "5. ✓ Custom event handlers"
145
+ puts "6. ✓ Event mixins (Actor, ChangeTracking)"
146
+ puts "7. ✓ Domain policies for authorization"
147
+ puts "8. ✓ ActiveRecord integration"
148
+ puts "9. ✓ Organization-based data isolation"
149
+ puts "\nCheck your Rails logs to see all the events and handler executions!"
150
+ puts "=" * 80 + "\n\n"
File without changes
File without changes
@@ -0,0 +1,135 @@
1
+ <!doctype html>
2
+
3
+ <html lang="en">
4
+
5
+ <head>
6
+
7
+ <title>The server cannot process the request due to a client error (400 Bad Request)</title>
8
+
9
+ <meta charset="utf-8">
10
+ <meta name="viewport" content="initial-scale=1, width=device-width">
11
+ <meta name="robots" content="noindex, nofollow">
12
+
13
+ <style>
14
+
15
+ *, *::before, *::after {
16
+ box-sizing: border-box;
17
+ }
18
+
19
+ * {
20
+ margin: 0;
21
+ }
22
+
23
+ html {
24
+ font-size: 16px;
25
+ }
26
+
27
+ body {
28
+ background: #FFF;
29
+ color: #261B23;
30
+ display: grid;
31
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Aptos, Roboto, "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
32
+ font-size: clamp(1rem, 2.5vw, 2rem);
33
+ -webkit-font-smoothing: antialiased;
34
+ font-style: normal;
35
+ font-weight: 400;
36
+ letter-spacing: -0.0025em;
37
+ line-height: 1.4;
38
+ min-height: 100dvh;
39
+ place-items: center;
40
+ text-rendering: optimizeLegibility;
41
+ -webkit-text-size-adjust: 100%;
42
+ }
43
+
44
+ #error-description {
45
+ fill: #d30001;
46
+ }
47
+
48
+ #error-id {
49
+ fill: #f0eff0;
50
+ }
51
+
52
+ @media (prefers-color-scheme: dark) {
53
+ body {
54
+ background: #101010;
55
+ color: #e0e0e0;
56
+ }
57
+
58
+ #error-description {
59
+ fill: #FF6161;
60
+ }
61
+
62
+ #error-id {
63
+ fill: #2c2c2c;
64
+ }
65
+ }
66
+
67
+ a {
68
+ color: inherit;
69
+ font-weight: 700;
70
+ text-decoration: underline;
71
+ text-underline-offset: 0.0925em;
72
+ }
73
+
74
+ b, strong {
75
+ font-weight: 700;
76
+ }
77
+
78
+ i, em {
79
+ font-style: italic;
80
+ }
81
+
82
+ main {
83
+ display: grid;
84
+ gap: 1em;
85
+ padding: 2em;
86
+ place-items: center;
87
+ text-align: center;
88
+ }
89
+
90
+ main header {
91
+ width: min(100%, 12em);
92
+ }
93
+
94
+ main header svg {
95
+ height: auto;
96
+ max-width: 100%;
97
+ width: 100%;
98
+ }
99
+
100
+ main article {
101
+ width: min(100%, 30em);
102
+ }
103
+
104
+ main article p {
105
+ font-size: 75%;
106
+ }
107
+
108
+ main article br {
109
+ display: none;
110
+
111
+ @media(min-width: 48em) {
112
+ display: inline;
113
+ }
114
+ }
115
+
116
+ </style>
117
+
118
+ </head>
119
+
120
+ <body>
121
+
122
+ <!-- This file lives in public/400.html -->
123
+
124
+ <main>
125
+ <header>
126
+ <svg height="172" viewBox="0 0 480 172" width="480" xmlns="http://www.w3.org/2000/svg"><path d="m124.48 3.00509-45.6889 100.02991h26.2239v-28.1168h38.119v28.1168h21.628v35.145h-21.628v30.82h-37.308v-30.82h-72.1833v-31.901l50.2851-103.27391zm115.583 168.69891c-40.822 0-64.884-35.146-64.884-85.7015 0-50.5554 24.062-85.700907 64.884-85.700907 40.823 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.061 85.7015-64.884 85.7015zm0-133.2831c-17.572 0-22.709 21.8984-22.709 47.5816 0 25.6835 5.137 47.5815 22.709 47.5815 17.303 0 22.71-21.898 22.71-47.5815 0-25.6832-5.407-47.5816-22.71-47.5816zm140.456 133.2831c-40.823 0-64.884-35.146-64.884-85.7015 0-50.5554 24.061-85.700907 64.884-85.700907 40.822 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.062 85.7015-64.884 85.7015zm0-133.2831c-17.573 0-22.71 21.8984-22.71 47.5816 0 25.6835 5.137 47.5815 22.71 47.5815 17.302 0 22.709-21.898 22.709-47.5815 0-25.6832-5.407-47.5816-22.709-47.5816z" id="error-id"/><path d="m123.606 85.4445c3.212 1.0523 5.538 4.2089 5.538 8.0301 0 6.1472-4.209 9.5254-11.298 9.5254h-15.617v-34.0033h14.565c7.089 0 11.353 3.1566 11.353 9.2484 0 3.6551-2.049 6.3134-4.541 7.1994zm-12.904-2.9905h5.095c2.603 0 3.988-.9968 3.988-3.1013 0-2.1044-1.385-3.0459-3.988-3.0459h-5.095zm0 6.6456v6.5902h5.981c2.492 0 3.877-1.3291 3.877-3.2674 0-2.049-1.385-3.3228-3.877-3.3228zm43.786 13.9004h-8.362v-1.274c-.831.831-3.323 1.717-5.981 1.717-4.929 0-9.083-2.769-9.083-8.0301 0-4.818 4.154-7.9193 9.581-7.9193 2.049 0 4.486.6646 5.483 1.3845v-1.606c0-1.606-.942-2.9905-3.046-2.9905-1.606 0-2.548.7199-2.935 1.8275h-8.197c.72-4.8181 4.985-8.6393 11.409-8.6393 7.088 0 11.131 3.7659 11.131 10.2453zm-8.362-6.9779v-1.4399c-.554-1.0522-2.049-1.7167-3.655-1.7167-1.717 0-3.434.7199-3.434 2.3813 0 1.7168 1.717 2.4367 3.434 2.4367 1.606 0 3.101-.6645 3.655-1.6614zm27.996 6.9779v-1.994c-1.163 1.329-3.599 2.548-6.147 2.548-7.199 0-11.131-5.8151-11.131-13.0145s3.932-13.0143 11.131-13.0143c2.548 0 4.984 1.2184 6.147 2.5475v-13.0697h8.695v35.997zm0-9.1931v-6.5902c-.664-1.3291-2.159-2.326-3.821-2.326-2.99 0-4.763 2.4368-4.763 5.6488s1.773 5.5934 4.763 5.5934c1.717 0 3.157-.9415 3.821-2.326zm35.471-2.049h-3.101v11.2421h-8.806v-34.0033h15.285c7.31 0 12.35 4.1535 12.35 11.5744 0 5.1503-2.603 8.6947-6.757 10.2453l7.975 12.1836h-9.858zm-3.101-15.2849v8.1962h5.538c3.156 0 4.596-1.606 4.596-4.0981s-1.44-4.0981-4.596-4.0981zm36.957 17.8323h8.03c-.886 5.7597-5.206 9.2487-11.685 9.2487-7.643 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.316-13.0143 12.515-13.0143 7.643 0 11.962 5.095 11.962 12.5159v2.1598h-16.115c.277 2.9905 1.827 4.5965 4.32 4.5965 1.772 0 3.156-.7753 3.655-2.4921zm-3.822-10.0237c-2.049 0-3.433 1.2737-3.987 3.5997h7.532c-.111-2.0491-1.385-3.5997-3.545-3.5997zm30.98 27.5234v-10.799c-1.163 1.329-3.6 2.548-6.147 2.548-7.2 0-11.132-5.9259-11.132-13.0145 0-7.144 3.932-13.0143 11.132-13.0143 2.547 0 4.984 1.2184 6.147 2.5475v-1.9937h8.695v33.726zm0-17.9981v-6.5902c-.665-1.3291-2.105-2.326-3.821-2.326-2.991 0-4.763 2.4368-4.763 5.6488s1.772 5.5934 4.763 5.5934c1.661 0 3.156-.9415 3.821-2.326zm36.789-15.7279v24.921h-8.695v-2.16c-1.329 1.551-3.821 2.714-6.646 2.714-5.482 0-8.75-3.5999-8.75-9.1379v-16.3371h8.64v14.288c0 2.1045.996 3.5997 3.212 3.5997 1.606 0 3.101-1.0522 3.544-2.769v-15.1187zm19.084 16.2263h8.03c-.886 5.7597-5.206 9.2487-11.685 9.2487-7.643 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.316-13.0143 12.515-13.0143 7.643 0 11.963 5.095 11.963 12.5159v2.1598h-16.116c.277 2.9905 1.828 4.5965 4.32 4.5965 1.772 0 3.156-.7753 3.655-2.4921zm-3.822-10.0237c-2.049 0-3.433 1.2737-3.987 3.5997h7.532c-.111-2.0491-1.385-3.5997-3.545-3.5997zm13.428 11.0206h8.474c.387 1.3845 1.606 2.1598 3.156 2.1598 1.44 0 2.548-.5538 2.548-1.7168 0-.9414-.72-1.2737-1.939-1.5506l-4.873-.9969c-4.154-.886-6.867-2.8797-6.867-7.2547 0-5.3165 4.762-8.4178 10.633-8.4178 6.812 0 10.522 3.1567 11.297 8.0855h-8.03c-.277-1.0522-1.052-1.9937-3.046-1.9937-1.273 0-2.326.5538-2.326 1.6614 0 .7753.554 1.163 1.717 1.3845l4.929 1.163c4.541 1.0522 6.978 3.4335 6.978 7.4763 0 5.3168-4.818 8.2518-10.91 8.2518-6.369 0-10.965-2.88-11.741-8.2518zm27.538-.8861v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.205v6.7564h-5.205v8.307c0 1.9383.941 2.769 2.658 2.769.941 0 1.993-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.871 0-9.193-2.769-9.193-9.0819z" id="error-description"/></svg>
127
+ </header>
128
+ <article>
129
+ <p><strong>The server cannot process the request due to a client error.</strong> Please check the request and try again. If you're the application owner check the logs for more information.</p>
130
+ </article>
131
+ </main>
132
+
133
+ </body>
134
+
135
+ </html>
@@ -0,0 +1,135 @@
1
+ <!doctype html>
2
+
3
+ <html lang="en">
4
+
5
+ <head>
6
+
7
+ <title>The page you were looking for doesn't exist (404 Not found)</title>
8
+
9
+ <meta charset="utf-8">
10
+ <meta name="viewport" content="initial-scale=1, width=device-width">
11
+ <meta name="robots" content="noindex, nofollow">
12
+
13
+ <style>
14
+
15
+ *, *::before, *::after {
16
+ box-sizing: border-box;
17
+ }
18
+
19
+ * {
20
+ margin: 0;
21
+ }
22
+
23
+ html {
24
+ font-size: 16px;
25
+ }
26
+
27
+ body {
28
+ background: #FFF;
29
+ color: #261B23;
30
+ display: grid;
31
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Aptos, Roboto, "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
32
+ font-size: clamp(1rem, 2.5vw, 2rem);
33
+ -webkit-font-smoothing: antialiased;
34
+ font-style: normal;
35
+ font-weight: 400;
36
+ letter-spacing: -0.0025em;
37
+ line-height: 1.4;
38
+ min-height: 100dvh;
39
+ place-items: center;
40
+ text-rendering: optimizeLegibility;
41
+ -webkit-text-size-adjust: 100%;
42
+ }
43
+
44
+ #error-description {
45
+ fill: #d30001;
46
+ }
47
+
48
+ #error-id {
49
+ fill: #f0eff0;
50
+ }
51
+
52
+ @media (prefers-color-scheme: dark) {
53
+ body {
54
+ background: #101010;
55
+ color: #e0e0e0;
56
+ }
57
+
58
+ #error-description {
59
+ fill: #FF6161;
60
+ }
61
+
62
+ #error-id {
63
+ fill: #2c2c2c;
64
+ }
65
+ }
66
+
67
+ a {
68
+ color: inherit;
69
+ font-weight: 700;
70
+ text-decoration: underline;
71
+ text-underline-offset: 0.0925em;
72
+ }
73
+
74
+ b, strong {
75
+ font-weight: 700;
76
+ }
77
+
78
+ i, em {
79
+ font-style: italic;
80
+ }
81
+
82
+ main {
83
+ display: grid;
84
+ gap: 1em;
85
+ padding: 2em;
86
+ place-items: center;
87
+ text-align: center;
88
+ }
89
+
90
+ main header {
91
+ width: min(100%, 12em);
92
+ }
93
+
94
+ main header svg {
95
+ height: auto;
96
+ max-width: 100%;
97
+ width: 100%;
98
+ }
99
+
100
+ main article {
101
+ width: min(100%, 30em);
102
+ }
103
+
104
+ main article p {
105
+ font-size: 75%;
106
+ }
107
+
108
+ main article br {
109
+ display: none;
110
+
111
+ @media(min-width: 48em) {
112
+ display: inline;
113
+ }
114
+ }
115
+
116
+ </style>
117
+
118
+ </head>
119
+
120
+ <body>
121
+
122
+ <!-- This file lives in public/404.html -->
123
+
124
+ <main>
125
+ <header>
126
+ <svg height="172" viewBox="0 0 480 172" width="480" xmlns="http://www.w3.org/2000/svg"><path d="m124.48 3.00509-45.6889 100.02991h26.2239v-28.1168h38.119v28.1168h21.628v35.145h-21.628v30.82h-37.308v-30.82h-72.1833v-31.901l50.2851-103.27391zm115.583 168.69891c-40.822 0-64.884-35.146-64.884-85.7015 0-50.5554 24.062-85.700907 64.884-85.700907 40.823 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.061 85.7015-64.884 85.7015zm0-133.2831c-17.572 0-22.709 21.8984-22.709 47.5816 0 25.6835 5.137 47.5815 22.709 47.5815 17.303 0 22.71-21.898 22.71-47.5815 0-25.6832-5.407-47.5816-22.71-47.5816zm165.328-35.41581-45.689 100.02991h26.224v-28.1168h38.119v28.1168h21.628v35.145h-21.628v30.82h-37.308v-30.82h-72.184v-31.901l50.285-103.27391z" id="error-id"/><path d="m157.758 68.9967v34.0033h-7.199l-14.233-19.8814v19.8814h-8.584v-34.0033h8.307l13.125 18.7184v-18.7184zm28.454 21.5428c0 7.6978-5.15 13.0145-12.737 13.0145-7.532 0-12.738-5.3167-12.738-13.0145s5.206-13.0143 12.738-13.0143c7.587 0 12.737 5.3165 12.737 13.0143zm-8.528 0c0-3.4336-1.496-5.8703-4.209-5.8703-2.659 0-4.154 2.4367-4.154 5.8703s1.495 5.8149 4.154 5.8149c2.713 0 4.209-2.3813 4.209-5.8149zm13.184 3.8766v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.205v6.7564h-5.205v8.307c0 1.9383.941 2.769 2.658 2.769.941 0 1.994-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.87 0-9.193-2.769-9.193-9.0819zm37.027 8.5839h-8.806v-34.0033h23.924v7.6978h-15.118v6.7564h13.9v7.5316h-13.9zm41.876-12.4605c0 7.6978-5.15 13.0145-12.737 13.0145-7.532 0-12.738-5.3167-12.738-13.0145s5.206-13.0143 12.738-13.0143c7.587 0 12.737 5.3165 12.737 13.0143zm-8.529 0c0-3.4336-1.495-5.8703-4.208-5.8703-2.659 0-4.154 2.4367-4.154 5.8703s1.495 5.8149 4.154 5.8149c2.713 0 4.208-2.3813 4.208-5.8149zm35.337-12.4605v24.921h-8.695v-2.16c-1.329 1.551-3.821 2.714-6.646 2.714-5.482 0-8.75-3.5999-8.75-9.1379v-16.3371h8.64v14.288c0 2.1045.997 3.5997 3.212 3.5997 1.606 0 3.101-1.0522 3.544-2.769v-15.1187zm4.076 24.921v-24.921h8.694v2.1598c1.385-1.5506 3.822-2.7136 6.701-2.7136 5.538 0 8.806 3.5997 8.806 9.1377v16.3371h-8.639v-14.2327c0-2.049-1.053-3.5443-3.268-3.5443-1.717 0-3.156.9969-3.6 2.7136v15.0634zm44.113 0v-1.994c-1.163 1.329-3.6 2.548-6.147 2.548-7.2 0-11.132-5.8151-11.132-13.0145s3.932-13.0143 11.132-13.0143c2.547 0 4.984 1.2184 6.147 2.5475v-13.0697h8.695v35.997zm0-9.1931v-6.5902c-.665-1.3291-2.16-2.326-3.821-2.326-2.991 0-4.763 2.4368-4.763 5.6488s1.772 5.5934 4.763 5.5934c1.717 0 3.156-.9415 3.821-2.326z" id="error-description"/></svg>
127
+ </header>
128
+ <article>
129
+ <p><strong>The page you were looking for doesn't exist.</strong> You may have mistyped the address or the page may have moved. If you're the application owner check the logs for more information.</p>
130
+ </article>
131
+ </main>
132
+
133
+ </body>
134
+
135
+ </html>