seams 0.1.0 → 0.2.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 (222) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +212 -12
  3. data/README.md +235 -82
  4. data/lib/generators/seams/accounts/accounts_generator.rb +31 -3
  5. data/lib/generators/seams/accounts/templates/app/controllers/memberships_controller.rb.tt +100 -0
  6. data/lib/generators/seams/accounts/templates/app/views/memberships/index.html.erb.tt +52 -0
  7. data/lib/generators/seams/accounts/templates/config/routes.rb.tt +10 -4
  8. data/lib/generators/seams/accounts/templates/lib/concerns/authorization.rb.tt +35 -2
  9. data/lib/generators/seams/accounts/templates/lib/engine.rb.tt +13 -0
  10. data/lib/generators/seams/accounts/templates/spec/runtime/authorization_spec.rb.tt +102 -0
  11. data/lib/generators/seams/accounts/templates/spec/runtime/memberships_flow_spec.rb.tt +145 -0
  12. data/lib/generators/seams/admin/admin_generator.rb +51 -4
  13. data/lib/generators/seams/admin/templates/README.md.tt +33 -3
  14. data/lib/generators/seams/admin/templates/app/controllers/admin/application_controller.rb.tt +156 -8
  15. data/lib/generators/seams/admin/templates/app/dashboards/admin/account_dashboard.rb.tt +9 -1
  16. data/lib/generators/seams/admin/templates/app/dashboards/admin/accounts_membership_dashboard.rb.tt +9 -1
  17. data/lib/generators/seams/admin/templates/app/dashboards/admin/identity_dashboard.rb.tt +6 -0
  18. data/lib/generators/seams/admin/templates/app/dashboards/admin/invitation_dashboard.rb.tt +7 -1
  19. data/lib/generators/seams/admin/templates/app/dashboards/admin/invoice_dashboard.rb.tt +6 -0
  20. data/lib/generators/seams/admin/templates/app/dashboards/admin/lifetime_pass_dashboard.rb.tt +6 -0
  21. data/lib/generators/seams/admin/templates/app/dashboards/admin/notification_dashboard.rb.tt +6 -0
  22. data/lib/generators/seams/admin/templates/app/dashboards/admin/notification_preference_dashboard.rb.tt +6 -0
  23. data/lib/generators/seams/admin/templates/app/dashboards/admin/plan_dashboard.rb.tt +6 -0
  24. data/lib/generators/seams/admin/templates/app/dashboards/admin/subscription_dashboard.rb.tt +6 -0
  25. data/lib/generators/seams/admin/templates/app/dashboards/admin/team_dashboard.rb.tt +12 -2
  26. data/lib/generators/seams/admin/templates/app/dashboards/admin/teams_membership_dashboard.rb.tt +7 -1
  27. data/lib/generators/seams/admin/templates/app/fields/admin/fields/belongs_to.rb.tt +12 -0
  28. data/lib/generators/seams/admin/templates/app/fields/admin/fields/dashboard_option.rb.tt +22 -0
  29. data/lib/generators/seams/admin/templates/app/fields/admin/fields/has_many.rb.tt +12 -0
  30. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/account_policy.rb.tt +4 -0
  31. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/accounts_membership_policy.rb.tt +6 -0
  32. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/application_policy.rb.tt +51 -10
  33. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/identity_policy.rb.tt +4 -0
  34. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/invitation_policy.rb.tt +6 -0
  35. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/invoice_policy.rb.tt +6 -0
  36. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/lifetime_pass_policy.rb.tt +6 -0
  37. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/notification_policy.rb.tt +6 -0
  38. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/notification_preference_policy.rb.tt +6 -0
  39. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/plan_policy.rb.tt +7 -0
  40. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/subscription_policy.rb.tt +6 -0
  41. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/team_policy.rb.tt +6 -0
  42. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/teams_membership_policy.rb.tt +6 -0
  43. data/lib/generators/seams/admin/templates/app/views/seams/admin/application/_index_header.html.erb.tt +57 -0
  44. data/lib/generators/seams/admin/templates/app/views/seams/admin/application/_navigation.html.erb.tt +14 -0
  45. data/lib/generators/seams/admin/templates/config/routes.rb.tt +24 -14
  46. data/lib/generators/seams/admin/templates/lib/concerns/authenticator.rb.tt +3 -0
  47. data/lib/generators/seams/admin/templates/lib/configuration.rb.tt +3 -1
  48. data/lib/generators/seams/admin/templates/spec/runtime/admin_boot_spec.rb.tt +4 -2
  49. data/lib/generators/seams/auth/add_oauth_provider/add_oauth_provider_generator.rb +1 -2
  50. data/lib/generators/seams/auth/auth_generator.rb +1 -2
  51. data/lib/generators/seams/auth/templates/app/controllers/oauth/callbacks_controller.rb.tt +3 -0
  52. data/lib/generators/seams/auth/templates/app/controllers/password_resets_controller.rb.tt +4 -0
  53. data/lib/generators/seams/auth/templates/app/controllers/registrations_controller.rb.tt +5 -0
  54. data/lib/generators/seams/auth/templates/app/controllers/sessions_controller.rb.tt +5 -0
  55. data/lib/generators/seams/auth/templates/db/migrate/create_auth_oauth_providers.rb.tt +5 -1
  56. data/lib/generators/seams/auth/templates/lib/concerns/authentication.rb.tt +12 -1
  57. data/lib/generators/seams/auth/templates/lib/engine.rb.tt +27 -0
  58. data/lib/generators/seams/billing/billing_generator.rb +4 -3
  59. data/lib/generators/seams/billing/templates/README.md.tt +26 -0
  60. data/lib/generators/seams/billing/templates/app/controllers/invoices_controller.rb.tt +2 -0
  61. data/lib/generators/seams/billing/templates/app/controllers/subscriptions_controller.rb.tt +2 -0
  62. data/lib/generators/seams/billing/templates/app/services/invoices/sync_service.rb.tt +8 -7
  63. data/lib/generators/seams/billing/templates/app/services/stripe_service.rb.tt +9 -7
  64. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_handler_base.rb.tt +18 -3
  65. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_handler_base.rb.tt +7 -21
  66. data/lib/generators/seams/billing/templates/lib/engine.rb.tt +15 -0
  67. data/lib/generators/seams/billing/templates/lib/gateways/stripe.rb.tt +10 -10
  68. data/lib/generators/seams/billing/templates/lib/stripe/client.rb.tt +40 -14
  69. data/lib/generators/seams/billing/templates/lib/stripe/payload.rb.tt +90 -0
  70. data/lib/generators/seams/billing/templates/lib/stripe/webhook_signature.rb.tt +11 -1
  71. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/charge_refunded.json.tt +1 -0
  72. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/checkout_session_completed.json.tt +1 -0
  73. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_created.json.tt +15 -4
  74. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_deleted.json.tt +7 -1
  75. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_trial_will_end.json.tt +18 -1
  76. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_updated.json.tt +26 -5
  77. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_created.json.tt +16 -2
  78. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_finalized.json.tt +16 -2
  79. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_paid.json.tt +17 -2
  80. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_payment_failed.json.tt +16 -2
  81. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_voided.json.tt +16 -2
  82. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/payment_intent_payment_failed.json.tt +1 -0
  83. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/payment_intent_succeeded.json.tt +1 -0
  84. data/lib/generators/seams/billing/templates/spec/gateways/stripe_spec.rb.tt +47 -1
  85. data/lib/generators/seams/billing/templates/spec/runtime/webhook_handlers_spec.rb.tt +39 -1
  86. data/lib/generators/seams/design/design_generator.rb +647 -0
  87. data/lib/generators/seams/design/templates/README.md.tt +129 -0
  88. data/lib/generators/seams/design/templates/app/assets/tailwind/_tokens.css +322 -0
  89. data/lib/generators/seams/design/templates/app/assets/tailwind/themes/_quire.css +42 -0
  90. data/lib/generators/seams/design/templates/app/controllers/design/dashboard_controller.rb.tt +30 -0
  91. data/lib/generators/seams/design/templates/app/controllers/design/guide_controller.rb.tt +28 -0
  92. data/lib/generators/seams/design/templates/app/form_builders/design/form_builder.rb.tt +90 -0
  93. data/lib/generators/seams/design/templates/app/helpers/design/ui_helper.rb.tt +37 -0
  94. data/lib/generators/seams/design/templates/app/views/design/dashboard/index.html.erb.tt +38 -0
  95. data/lib/generators/seams/design/templates/app/views/design/guide/index.html.erb.tt +19 -0
  96. data/lib/generators/seams/design/templates/app/views/layouts/application.html.erb.tt +61 -0
  97. data/lib/generators/seams/design/templates/app/views/layouts/design/guide.html.erb.tt +21 -0
  98. data/lib/generators/seams/design/templates/app/views/ui/_banner.html.erb.tt +10 -0
  99. data/lib/generators/seams/design/templates/app/views/ui/_breadcrumb.html.erb.tt +8 -0
  100. data/lib/generators/seams/design/templates/app/views/ui/_build_row.html.erb.tt +14 -0
  101. data/lib/generators/seams/design/templates/app/views/ui/_button.html.erb.tt +4 -0
  102. data/lib/generators/seams/design/templates/app/views/ui/_card.html.erb.tt +2 -0
  103. data/lib/generators/seams/design/templates/app/views/ui/_chapter_row.html.erb.tt +11 -0
  104. data/lib/generators/seams/design/templates/app/views/ui/_checkbox.html.erb.tt +12 -0
  105. data/lib/generators/seams/design/templates/app/views/ui/_counter.html.erb.tt +2 -0
  106. data/lib/generators/seams/design/templates/app/views/ui/_data_table.html.erb.tt +25 -0
  107. data/lib/generators/seams/design/templates/app/views/ui/_dialog.html.erb.tt +14 -0
  108. data/lib/generators/seams/design/templates/app/views/ui/_diff.html.erb.tt +12 -0
  109. data/lib/generators/seams/design/templates/app/views/ui/_drawer.html.erb.tt +7 -0
  110. data/lib/generators/seams/design/templates/app/views/ui/_empty.html.erb.tt +5 -0
  111. data/lib/generators/seams/design/templates/app/views/ui/_field.html.erb.tt +14 -0
  112. data/lib/generators/seams/design/templates/app/views/ui/_icon.html.erb.tt +2 -0
  113. data/lib/generators/seams/design/templates/app/views/ui/_icon_sprite.html.erb.tt +22 -0
  114. data/lib/generators/seams/design/templates/app/views/ui/_input_group.html.erb.tt +11 -0
  115. data/lib/generators/seams/design/templates/app/views/ui/_kbd.html.erb.tt +4 -0
  116. data/lib/generators/seams/design/templates/app/views/ui/_menu.html.erb.tt +18 -0
  117. data/lib/generators/seams/design/templates/app/views/ui/_meter.html.erb.tt +15 -0
  118. data/lib/generators/seams/design/templates/app/views/ui/_note.html.erb.tt +2 -0
  119. data/lib/generators/seams/design/templates/app/views/ui/_outline.html.erb.tt +9 -0
  120. data/lib/generators/seams/design/templates/app/views/ui/_pagination.html.erb.tt +12 -0
  121. data/lib/generators/seams/design/templates/app/views/ui/_panel.html.erb.tt +2 -0
  122. data/lib/generators/seams/design/templates/app/views/ui/_popover.html.erb.tt +2 -0
  123. data/lib/generators/seams/design/templates/app/views/ui/_radio.html.erb.tt +5 -0
  124. data/lib/generators/seams/design/templates/app/views/ui/_savestate.html.erb.tt +5 -0
  125. data/lib/generators/seams/design/templates/app/views/ui/_segmented.html.erb.tt +7 -0
  126. data/lib/generators/seams/design/templates/app/views/ui/_stepper.html.erb.tt +12 -0
  127. data/lib/generators/seams/design/templates/app/views/ui/_switch.html.erb.tt +6 -0
  128. data/lib/generators/seams/design/templates/app/views/ui/_tag.html.erb.tt +2 -0
  129. data/lib/generators/seams/design/templates/app/views/ui/_toast.html.erb.tt +12 -0
  130. data/lib/generators/seams/design/templates/app/views/ui/_toolbar.html.erb.tt +15 -0
  131. data/lib/generators/seams/design/templates/app/views/ui/previews/_banner.html.erb.tt +8 -0
  132. data/lib/generators/seams/design/templates/app/views/ui/previews/_breadcrumb.html.erb.tt +5 -0
  133. data/lib/generators/seams/design/templates/app/views/ui/previews/_build_row.html.erb.tt +6 -0
  134. data/lib/generators/seams/design/templates/app/views/ui/previews/_button.html.erb.tt +9 -0
  135. data/lib/generators/seams/design/templates/app/views/ui/previews/_card.html.erb.tt +12 -0
  136. data/lib/generators/seams/design/templates/app/views/ui/previews/_chapter_row.html.erb.tt +6 -0
  137. data/lib/generators/seams/design/templates/app/views/ui/previews/_checkbox.html.erb.tt +6 -0
  138. data/lib/generators/seams/design/templates/app/views/ui/previews/_counter.html.erb.tt +6 -0
  139. data/lib/generators/seams/design/templates/app/views/ui/previews/_data_table.html.erb.tt +14 -0
  140. data/lib/generators/seams/design/templates/app/views/ui/previews/_dialog.html.erb.tt +8 -0
  141. data/lib/generators/seams/design/templates/app/views/ui/previews/_diff.html.erb.tt +10 -0
  142. data/lib/generators/seams/design/templates/app/views/ui/previews/_drawer.html.erb.tt +5 -0
  143. data/lib/generators/seams/design/templates/app/views/ui/previews/_empty.html.erb.tt +5 -0
  144. data/lib/generators/seams/design/templates/app/views/ui/previews/_field.html.erb.tt +5 -0
  145. data/lib/generators/seams/design/templates/app/views/ui/previews/_input_group.html.erb.tt +5 -0
  146. data/lib/generators/seams/design/templates/app/views/ui/previews/_kbd.html.erb.tt +5 -0
  147. data/lib/generators/seams/design/templates/app/views/ui/previews/_menu.html.erb.tt +9 -0
  148. data/lib/generators/seams/design/templates/app/views/ui/previews/_meter.html.erb.tt +6 -0
  149. data/lib/generators/seams/design/templates/app/views/ui/previews/_note.html.erb.tt +2 -0
  150. data/lib/generators/seams/design/templates/app/views/ui/previews/_outline.html.erb.tt +8 -0
  151. data/lib/generators/seams/design/templates/app/views/ui/previews/_pagination.html.erb.tt +5 -0
  152. data/lib/generators/seams/design/templates/app/views/ui/previews/_panel.html.erb.tt +5 -0
  153. data/lib/generators/seams/design/templates/app/views/ui/previews/_popover.html.erb.tt +2 -0
  154. data/lib/generators/seams/design/templates/app/views/ui/previews/_radio.html.erb.tt +6 -0
  155. data/lib/generators/seams/design/templates/app/views/ui/previews/_savestate.html.erb.tt +4 -0
  156. data/lib/generators/seams/design/templates/app/views/ui/previews/_segmented.html.erb.tt +15 -0
  157. data/lib/generators/seams/design/templates/app/views/ui/previews/_stepper.html.erb.tt +6 -0
  158. data/lib/generators/seams/design/templates/app/views/ui/previews/_switch.html.erb.tt +5 -0
  159. data/lib/generators/seams/design/templates/app/views/ui/previews/_tag.html.erb.tt +8 -0
  160. data/lib/generators/seams/design/templates/app/views/ui/previews/_toast.html.erb.tt +6 -0
  161. data/lib/generators/seams/design/templates/app/views/ui/previews/_toolbar.html.erb.tt +11 -0
  162. data/lib/generators/seams/design/templates/lib/design/components.rb.tt +22 -0
  163. data/lib/generators/seams/design/templates/lib/design.rb.tt +18 -0
  164. data/lib/generators/seams/design/templates/lib/engine.rb.tt +38 -0
  165. data/lib/generators/seams/design/templates/lib/generators/design/component/component_generator.rb.tt +52 -0
  166. data/lib/generators/seams/design/templates/lib/generators/design/component/templates/component.html.erb.tt +5 -0
  167. data/lib/generators/seams/design/templates/lib/generators/design/component/templates/preview.html.erb.tt +3 -0
  168. data/lib/generators/seams/design/templates/spec/runtime/design_boot_spec.rb.tt +79 -0
  169. data/lib/generators/seams/design/templates/spec/runtime/form_builder_spec.rb.tt +97 -0
  170. data/lib/generators/seams/design/templates/spec/runtime/guide_spec.rb.tt +40 -0
  171. data/lib/generators/seams/design/templates/spec/runtime/ui_components_spec.rb.tt +70 -0
  172. data/lib/generators/seams/engine/engine_generator.rb +6 -0
  173. data/lib/generators/seams/engine/templates/Gemfile.tt +1 -1
  174. data/lib/generators/seams/engine/templates/app/application_controller.rb.tt +6 -0
  175. data/lib/generators/seams/engine/templates/rubocop.yml.tt +9 -0
  176. data/lib/generators/seams/install/install_generator.rb +72 -6
  177. data/lib/generators/seams/install/templates/Dockerfile.tt +2 -2
  178. data/lib/generators/seams/install/templates/bin_seams.tt +7 -4
  179. data/lib/generators/seams/install/templates/ci.yml.tt +18 -8
  180. data/lib/generators/seams/install/templates/deploy.yml.tt +4 -2
  181. data/lib/generators/seams/install/templates/doc/ARCHITECTURE.md.tt +1 -1
  182. data/lib/generators/seams/install/templates/docker-entrypoint.tt +4 -2
  183. data/lib/generators/seams/install/templates/herb.yml.tt +10 -0
  184. data/lib/generators/seams/install/templates/lefthook.yml.tt +25 -0
  185. data/lib/generators/seams/install/templates/ruby-version.tt +1 -1
  186. data/lib/generators/seams/install/templates/seams.rake.tt +13 -1
  187. data/lib/generators/seams/install/templates/strong_migrations.rb.tt +26 -0
  188. data/lib/generators/seams/notifications/notifications_generator.rb +3 -2
  189. data/lib/generators/seams/notifications/templates/app/controllers/preferences_controller.rb.tt +5 -13
  190. data/lib/generators/seams/notifications/templates/lib/engine.rb.tt +12 -0
  191. data/lib/generators/seams/notifications/templates/lib/notifications.rb.tt +1 -0
  192. data/lib/generators/seams/notifications/templates/lib/preferences.rb.tt +36 -0
  193. data/lib/generators/seams/permissions/permissions_generator.rb +97 -0
  194. data/lib/generators/seams/permissions/templates/config/initializers/seams_permissions.rb.tt +35 -0
  195. data/lib/generators/seams/teams/templates/app/controllers/invitations_controller.rb.tt +27 -11
  196. data/lib/generators/seams/teams/templates/app/controllers/teams_controller.rb.tt +5 -13
  197. data/lib/generators/seams/teams/templates/lib/concerns/authorization.rb.tt +2 -4
  198. data/lib/generators/seams/teams/templates/lib/engine.rb.tt +13 -0
  199. data/lib/seams/cli/list.rb +15 -0
  200. data/lib/seams/cli/quality.rb +46 -5
  201. data/lib/seams/cli/resolve.rb +9 -5
  202. data/lib/seams/cli/test_changed.rb +5 -1
  203. data/lib/seams/cli.rb +20 -0
  204. data/lib/seams/configuration.rb +24 -1
  205. data/lib/seams/cops/no_cross_engine_dependency.rb +39 -7
  206. data/lib/seams/cops/no_cross_engine_model_access.rb +79 -1
  207. data/lib/seams/event_registry.rb +19 -0
  208. data/lib/seams/events/adapter.rb +9 -0
  209. data/lib/seams/events/adapters/active_support.rb +4 -0
  210. data/lib/seams/events/publisher.rb +29 -0
  211. data/lib/seams/events.rb +1 -0
  212. data/lib/seams/generators/dummy_app_writer.rb +21 -0
  213. data/lib/seams/generators/follow_up_generator.rb +1 -2
  214. data/lib/seams/generators/host_injector.rb +9 -2
  215. data/lib/seams/generators/splicer.rb +2 -2
  216. data/lib/seams/observability/adapter.rb +22 -0
  217. data/lib/seams/observability.rb +6 -0
  218. data/lib/seams/permission_registry.rb +66 -0
  219. data/lib/seams/permissions.rb +120 -0
  220. data/lib/seams/version.rb +1 -1
  221. data/lib/seams.rb +7 -0
  222. metadata +107 -3
@@ -23,6 +23,10 @@ module Seams
23
23
 
24
24
  NAME_PATTERN = /\A[a-z][a-z0-9_]*\z/
25
25
 
26
+ class_option :skip_host_wiring, type: :boolean, default: false,
27
+ desc: "Do not mount the engine or add a host initializer " \
28
+ "(for canonical generators that wire the host themselves)"
29
+
26
30
  def validate_name
27
31
  unless NAME_PATTERN.match?(name)
28
32
  raise Seams::GeneratorError,
@@ -107,6 +111,8 @@ module Seams
107
111
  end
108
112
 
109
113
  def wire_into_host
114
+ return if options[:skip_host_wiring]
115
+
110
116
  # Mount the engine into the host's routes (idempotent — skips
111
117
  # if the line already exists, so canonical generators that
112
118
  # call this and ALSO mount themselves are safe).
@@ -12,7 +12,7 @@ source "https://rubygems.org"
12
12
  gemspec
13
13
 
14
14
  group :development, :test do
15
- gem "rspec-rails", "~> 7.1"
15
+ gem "rspec-rails", ">= 7.1", "< 9"
16
16
  gem "rubocop", require: false
17
17
  gem "rubocop-rails", require: false
18
18
  gem "rubocop-rspec", require: false
@@ -1,6 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module <%= module_name %>
4
+ # Enforces authentication on every action by default. Mount points
5
+ # that should be reachable while signed out (e.g. password reset,
6
+ # invitation accept-via-token) must opt out explicitly:
7
+ #
8
+ # skip_before_action :authenticate_identity!, only: %i[accept_form accept]
4
9
  class ApplicationController < ::ApplicationController
10
+ before_action :authenticate_identity!
5
11
  end
6
12
  end
@@ -32,16 +32,25 @@ Metrics/MethodLength:
32
32
  # host application here so this engine cannot reach into them
33
33
  # accidentally. Add concerns the other engines expose (e.g.
34
34
  # `Billing::Billable`) to ExposedConcerns to allow `include` calls.
35
+ #
36
+ # Both boundary cops exclude spec/**/*: boundaries are a
37
+ # production-code contract, and integration specs legitimately need to
38
+ # create real sibling records (e.g. `Auth::Identity.create!` in test
39
+ # setup) to exercise cross-engine behaviour.
35
40
  Seams/NoCrossEngineModelAccess:
36
41
  Enabled: true
37
42
  OwnEngine: <%= module_name %>
38
43
  OtherEngines: []
39
44
  ExposedConcerns: []
45
+ Exclude:
46
+ - "spec/**/*"
40
47
 
41
48
  Seams/NoCrossEngineDependency:
42
49
  Enabled: true
43
50
  OwnEngine: <%= name %>
44
51
  OtherEngines: []
52
+ Exclude:
53
+ - "spec/**/*"
45
54
 
46
55
  Seams/KnownQueueNames:
47
56
  Enabled: true
@@ -18,6 +18,23 @@ module Seams
18
18
 
19
19
  source_root File.expand_path("templates", __dir__)
20
20
 
21
+ # The quality toolchain is installed by DEFAULT — Seams is opinionated and
22
+ # sets a host up for success from the first CLI run. Opt out per tool with
23
+ # --no-strong-migrations / --no-lefthook.
24
+ #
25
+ # herb is the exception and is OPT-IN: it ships an arm64-darwin-only native
26
+ # gem with no Linux build, so on a Rails 8 multi-platform lock (which lists
27
+ # the Linux deploy platforms) `bundle` fails and the host won't boot. A
28
+ # fresh install booting matters more than a linter being on, so enable it
29
+ # explicitly with --herb once your lock can resolve it.
30
+ class_option :herb, type: :boolean, default: false,
31
+ desc: "Install herb (HTML+ERB linter) + .herb.yml + CI/hook lint step. " \
32
+ "Off by default: herb is arm64-darwin-only and breaks multi-platform locks."
33
+ class_option :strong_migrations, type: :boolean, default: true,
34
+ desc: "Install strong_migrations + initializer + CI migration-safety step"
35
+ class_option :lefthook, type: :boolean, default: true,
36
+ desc: "Install lefthook + lefthook.yml (pre-commit lint, pre-push specs)"
37
+
21
38
  def create_initializer
22
39
  template "seams.rb.tt", "config/initializers/seams.rb"
23
40
  end
@@ -129,6 +146,41 @@ module Seams
129
146
  File.chmod(0o755, full_path) if File.exist?(full_path)
130
147
  end
131
148
 
149
+ # --- Quality toolchain (default on; opt out per tool) -----------------
150
+
151
+ def create_herb_config
152
+ return unless options[:herb]
153
+
154
+ template_if_missing "herb.yml.tt", ".herb.yml"
155
+ end
156
+
157
+ def create_strong_migrations_initializer
158
+ return unless options[:strong_migrations]
159
+
160
+ template_if_missing "strong_migrations.rb.tt", "config/initializers/strong_migrations.rb"
161
+ end
162
+
163
+ def create_lefthook_config
164
+ return unless options[:lefthook]
165
+
166
+ template_if_missing "lefthook.yml.tt", "lefthook.yml"
167
+ end
168
+
169
+ def install_quality_gems
170
+ # The CI workflow (and lefthook) shell out to these, so make sure the
171
+ # gems are in the host Gemfile. All idempotent — skipped if present.
172
+ host_inject_gem("rubocop", group: :development)
173
+ host_inject_gem("brakeman", group: :development)
174
+ host_inject_gem("bundler-audit", group: :development)
175
+ host_inject_gem("herb", group: :development) if options[:herb]
176
+ host_inject_gem("lefthook", group: :development) if options[:lefthook]
177
+ # strong_migrations is a dev/CI guard — it vets migrations where they're
178
+ # written and in CI, and is kept OUT of the production path. The
179
+ # initializer is guarded with `if defined?(StrongMigrations)` so a
180
+ # production boot (where the gem isn't loaded) is unaffected.
181
+ host_inject_gem("strong_migrations", group: %i[development test]) if options[:strong_migrations]
182
+ end
183
+
132
184
  # Phase 1.5 — per-host helper scripts and architecture doc.
133
185
  def create_helper_scripts
134
186
  template_if_missing "script/collate_coverage.rb.tt", "script/collate_coverage.rb"
@@ -149,8 +201,9 @@ module Seams
149
201
  host_inject_gem("seams", "~> #{Seams::VERSION}")
150
202
  # Every Seams host needs rspec-rails so the per-engine
151
203
  # spec/dummy specs can actually run. Idempotent — skipped if
152
- # the host already has these gems.
153
- host_inject_gem("rspec-rails", "~> 7.1", group: :test)
204
+ # the host already has these gems. The range lets bundler pick
205
+ # rspec-rails 8.x on Rails 7.2+ and fall back to 7.1.x on Rails 7.1.
206
+ host_inject_gem("rspec-rails", ">= 7.1", "< 9", group: :test)
154
207
  end
155
208
 
156
209
  private
@@ -166,12 +219,23 @@ module Seams
166
219
 
167
220
  public
168
221
 
169
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
222
+ # rubocop:disable-next Metrics/AbcSize, Metrics/MethodLength
170
223
  def post_install_message
171
224
  say ""
172
225
  say " Seams is installed. Generate your first engine with:", :green
173
226
  say " bin/seams core (or bin/rails generate seams:core)"
174
227
  say ""
228
+ say " Quality toolchain (installed by default — opt out with --no-<tool>):", :yellow
229
+ say " rubocop + brakeman + bundle-audit (in CI)" \
230
+ "#{", herb (HTML+ERB lint)" if options[:herb]}" \
231
+ "#{", strong_migrations" if options[:strong_migrations]}"
232
+ say " HTML+ERB linting (herb) is opt-in — add --herb (arm64-darwin only).", :yellow unless options[:herb]
233
+ say " bundle install then bin/rails db:prepare"
234
+ if options[:lefthook]
235
+ say " Activate the git hooks (pre-commit lint, pre-push specs):", :yellow
236
+ say " bundle exec lefthook install"
237
+ end
238
+ say ""
175
239
  say " Canonical generators (run in this order):", :yellow
176
240
  say " bin/seams core - Core engine (concerns, audit log)"
177
241
  say " bin/seams auth - Auth engine (Identity, sessions, OAuth)"
@@ -184,6 +248,9 @@ module Seams
184
248
  say " bin/seams admin - Admin engine (Administrate-backed dashboards"
185
249
  say " for the canonical models, Pundit-gated,"
186
250
  say " audit-log auto-write). Requires auth + accounts."
251
+ say " bin/seams design - Design engine (ui_* component partials,"
252
+ say " Tailwind v4 tokens, Design::FormBuilder)."
253
+ say " Non-isolated: ui_* helpers resolve host-wide."
187
254
  say ""
188
255
  say " Follow-up generators (extend an already-installed engine):", :yellow
189
256
  say " bin/rails generate seams:auth:add_oauth_provider <name>"
@@ -201,11 +268,10 @@ module Seams
201
268
  say ""
202
269
  say " Recommended order: core -> auth -> accounts -> notifications -> billing -> teams.", :yellow
203
270
  say " Optional: append `admin` last for an Administrate-backed admin surface.", :yellow
204
- say " See doc/CURRENT_ATTRIBUTES.md (after install) for the per-request namespace cascade.", :yellow
205
- say " See doc/WRITING_FOLLOW_UP_GENERATORS.md to write your own follow-up generator.", :yellow
271
+ say " See doc/reference/CURRENT_ATTRIBUTES.md (after install) for the per-request namespace cascade.", :yellow
272
+ say " See doc/how-to/WRITING_FOLLOW_UP_GENERATORS.md to write your own follow-up generator.", :yellow
206
273
  say ""
207
274
  end
208
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
209
275
  end
210
276
  end
211
277
  end
@@ -1,12 +1,12 @@
1
1
  # Multi-stage Dockerfile tuned for a Seams-powered Rails app.
2
2
  #
3
3
  # RUBY_VERSION is read from the host's `.ruby-version` at build time
4
- # (default 4.0.3 — bump when you bump .ruby-version). Override with
4
+ # (default 4.0.7 — bump when you bump .ruby-version). Override with
5
5
  # `--build-arg RUBY_VERSION=...` if you need to.
6
6
  #
7
7
  # This file is compatible with Rails 8's `bin/docker-build`.
8
8
 
9
- ARG RUBY_VERSION=4.0.3
9
+ ARG RUBY_VERSION=4.0.7
10
10
  FROM ruby:${RUBY_VERSION}-slim AS base
11
11
 
12
12
  ENV BUNDLE_PATH="/usr/local/bundle" \
@@ -5,7 +5,8 @@
5
5
  #
6
6
  # bin/seams install # bin/rails generate seams:install
7
7
  # bin/seams engine billing # bin/rails generate seams:engine billing
8
- # bin/seams core | auth | accounts | notifications | billing | teams | admin
8
+ # bin/seams core | auth | accounts | notifications | billing | teams | admin | design
9
+ # bin/seams permissions # bin/rails generate seams:permissions
9
10
  # bin/seams remove billing # bin/rails generate seams:remove billing
10
11
  # bin/seams list # bin/rails seams:list
11
12
  # bin/seams test billing # bin/rails seams:test[billing]
@@ -16,7 +17,7 @@
16
17
 
17
18
  require "shellwords"
18
19
 
19
- CANONICAL = %w[install core auth accounts notifications billing teams admin].freeze
20
+ CANONICAL = %w[install core auth accounts notifications billing teams admin design permissions].freeze
20
21
  GENERIC = %w[engine remove].freeze
21
22
  TASKS = %w[list].freeze
22
23
  ENGINE_TASKS = %w[test quality].freeze
@@ -69,15 +70,17 @@ when nil, "help", "-h", "--help"
69
70
  Canonical generators:
70
71
  install Install the framework
71
72
  engine <name> Generate a generic engine
72
- core | auth | accounts | notifications | billing | teams | admin
73
+ core | auth | accounts | notifications | billing | teams | admin | design
73
74
  Generate a canonical engine
75
+ permissions Generate the host-editable role -> ability
76
+ grant map (config/initializers/seams_permissions.rb)
74
77
  remove <name> Remove an engine
75
78
 
76
79
  Follow-up generators (extend an already-installed engine):
77
80
  bin/rails generate seams:auth:add_oauth_provider <name>
78
81
  Add a new OAuth provider adapter
79
82
  (e.g. linkedin, apple, microsoft)
80
- See doc/WRITING_FOLLOW_UP_GENERATORS.md for how to write your own.
83
+ See doc/how-to/WRITING_FOLLOW_UP_GENERATORS.md for how to write your own.
81
84
 
82
85
  Diagnostics + escape hatch:
83
86
  list List engines + their events
@@ -16,21 +16,25 @@ env:
16
16
 
17
17
  jobs:
18
18
  lint:
19
- name: RuboCop
19
+ name: Lint
20
20
  runs-on: ubuntu-latest
21
21
  steps:
22
- - uses: actions/checkout@v4
22
+ - uses: actions/checkout@v7
23
23
  - uses: ruby/setup-ruby@v1
24
24
  with:
25
25
  ruby-version: ".ruby-version"
26
26
  bundler-cache: true
27
27
  - run: bundle exec rubocop --parallel
28
+ <% if options[:herb] -%>
29
+ - name: Lint HTML+ERB (herb)
30
+ run: bundle exec herb lint "**/*.html.erb"
31
+ <% end -%>
28
32
 
29
33
  security:
30
34
  name: Security (brakeman + bundle-audit)
31
35
  runs-on: ubuntu-latest
32
36
  steps:
33
- - uses: actions/checkout@v4
37
+ - uses: actions/checkout@v7
34
38
  - uses: ruby/setup-ruby@v1
35
39
  with:
36
40
  ruby-version: ".ruby-version"
@@ -48,7 +52,7 @@ jobs:
48
52
  outputs:
49
53
  engines: ${{ steps.set.outputs.engines }}
50
54
  steps:
51
- - uses: actions/checkout@v4
55
+ - uses: actions/checkout@v7
52
56
  - id: set
53
57
  run: |
54
58
  if [ -d engines ]; then
@@ -71,7 +75,7 @@ jobs:
71
75
  engine: ${{ fromJson(needs.discover.outputs.engines) }}
72
76
  services:
73
77
  postgres:
74
- image: postgres:16
78
+ image: postgres:18
75
79
  env:
76
80
  POSTGRES_PASSWORD: postgres
77
81
  ports: ["5432:5432"]
@@ -87,7 +91,7 @@ jobs:
87
91
  PGUSER: "postgres"
88
92
  PGPASSWORD: "postgres"
89
93
  steps:
90
- - uses: actions/checkout@v4
94
+ - uses: actions/checkout@v7
91
95
  - uses: ruby/setup-ruby@v1
92
96
  with:
93
97
  ruby-version: ".ruby-version"
@@ -99,7 +103,7 @@ jobs:
99
103
  runs-on: ubuntu-latest
100
104
  services:
101
105
  postgres:
102
- image: postgres:16
106
+ image: postgres:18
103
107
  env:
104
108
  POSTGRES_PASSWORD: postgres
105
109
  ports: ["5432:5432"]
@@ -115,9 +119,15 @@ jobs:
115
119
  PGUSER: "postgres"
116
120
  PGPASSWORD: "postgres"
117
121
  steps:
118
- - uses: actions/checkout@v4
122
+ - uses: actions/checkout@v7
119
123
  - uses: ruby/setup-ruby@v1
120
124
  with:
121
125
  ruby-version: ".ruby-version"
122
126
  bundler-cache: true
127
+ <% if options[:strong_migrations] -%>
128
+ - name: Migrations are safe (strong_migrations runs every migration from an empty database)
129
+ env:
130
+ RAILS_ENV: test
131
+ run: bundle exec rails db:create db:migrate
132
+ <% end -%>
123
133
  - run: bundle exec rspec spec
@@ -45,7 +45,7 @@ env:
45
45
  # Otherwise delete this block and provide DATABASE_URL via env.secret.
46
46
  # accessories:
47
47
  # postgres:
48
- # image: postgres:16
48
+ # image: postgres:18
49
49
  # host: TODO.web.example.com
50
50
  # port: "127.0.0.1:5432:5432"
51
51
  # env:
@@ -55,7 +55,9 @@ env:
55
55
  # secret:
56
56
  # - POSTGRES_PASSWORD
57
57
  # volumes:
58
- # - /var/lib/postgresql/data:/var/lib/postgresql/data
58
+ # # postgres:18+ keeps data under /var/lib/postgresql/<major>/docker,
59
+ # # so mount the parent directory (not .../data as in 17 and older).
60
+ # - /var/lib/postgresql:/var/lib/postgresql
59
61
 
60
62
  asset_path: /rails/public/assets
61
63
 
@@ -1,7 +1,7 @@
1
1
  # Architecture (host)
2
2
 
3
3
  > Generated by `seams:install`. Edit freely — this is YOUR doc, not
4
- > the gem's. Cross-reference the gem's own `doc/ARCHITECTURE.md` (in
4
+ > the gem's. Cross-reference the gem's own `doc/explanation/ARCHITECTURE.md` (in
5
5
  > the seams gem source) for framework-level details.
6
6
 
7
7
  ## The model
@@ -18,8 +18,10 @@ set -e
18
18
  # Web command is `bundle exec rails server` (or `./bin/rails server`).
19
19
  # Workers use `bundle exec rails solid_queue:start` — never run
20
20
  # db:prepare from a worker, two containers racing CREATE TABLE breaks.
21
- case "${1}${2}${3}" in
22
- *server*|*"./bin/rails"*"server"*|*"bin/rails"*"server"*)
21
+ # Match `server` as a whole word anywhere in the command: in
22
+ # `bundle exec rails server` it is the 4th argument.
23
+ case " $* " in
24
+ *" server "*|*" s "*)
23
25
  bundle exec rails db:prepare
24
26
  ;;
25
27
  esac
@@ -0,0 +1,10 @@
1
+ # Herb config for this Seams host. Herb lints HTML+ERB templates (host views
2
+ # AND generated engine views) for invalid nesting, accessibility, and unsafe
3
+ # raw output. Run with `bundle exec herb lint "**/*.html.erb"`; CI runs it too.
4
+ #
5
+ # Pin the version so a herb upgrade can't enable new rules mid-CI without a
6
+ # deliberate bump. Regenerate the full default with `bundle exec herb-lint --init`.
7
+ #
8
+ # Docs: https://herb-tools.dev/configuration
9
+
10
+ framework: actionview
@@ -0,0 +1,25 @@
1
+ # Git hooks for this Seams host. Install once with:
2
+ #
3
+ # bundle exec lefthook install
4
+ #
5
+ # These run the same quality gates as CI, but locally — before a push, so
6
+ # issues surface immediately instead of in CI. Bypass with `git ... --no-verify`.
7
+
8
+ pre-commit:
9
+ parallel: true
10
+ commands:
11
+ rubocop:
12
+ glob: "*.rb"
13
+ # --force-exclusion honours the engines/ exclude in .rubocop.yml even
14
+ # when files are passed explicitly (engines lint themselves).
15
+ run: bundle exec rubocop --force-exclusion {staged_files}
16
+ <% if options[:herb] -%>
17
+ herb:
18
+ glob: "*.html.erb"
19
+ run: bundle exec herb lint {staged_files}
20
+ <% end -%>
21
+
22
+ pre-push:
23
+ commands:
24
+ specs:
25
+ run: bundle exec rspec
@@ -1 +1 @@
1
- 4.0.3
1
+ 4.0.7
@@ -21,7 +21,12 @@ namespace :seams do
21
21
  task :quality, [:engine] => :environment do |_t, args|
22
22
  name = args[:engine] or abort("Usage: rake seams:quality[billing]")
23
23
  abort("Invalid engine name: #{name.inspect}") unless ENGINE_NAME_PATTERN.match?(name)
24
- sh "bundle", "exec", "rubocop", "engines/#{name}"
24
+ # Run from inside the engine directory with --ignore-parent-exclusion:
25
+ # the host .rubocop.yml excludes engines/**/* (engines lint
26
+ # themselves), and RuboCop merges that parent exclusion into any run
27
+ # unless told not to — `rubocop engines/#{name}` from the host root
28
+ # inspects zero files and reports a false pass.
29
+ sh "bundle", "exec", "rubocop", "--ignore-parent-exclusion", chdir: "engines/#{name}"
25
30
  end
26
31
 
27
32
  namespace :test do
@@ -45,6 +50,13 @@ namespace :seams do
45
50
  task audit: :environment do
46
51
  Rake::Task["environment"].invoke
47
52
  sh "bundle", "exec", "rubocop", "--parallel"
53
+ # Per-engine RuboCop: the host run above excludes engines/**/*, so
54
+ # each engine with its own .rubocop.yml is linted from inside its
55
+ # directory (--ignore-parent-exclusion stops the host's exclusion
56
+ # from turning these runs into 0-file no-ops).
57
+ Dir["engines/*"].select { |d| File.directory?(d) && File.exist?("#{d}/.rubocop.yml") }.sort.each do |dir|
58
+ sh "bundle", "exec", "rubocop", "--ignore-parent-exclusion", chdir: dir
59
+ end
48
60
  sh "bundle", "exec", "rspec", "spec"
49
61
  Dir["engines/*"].select { |d| File.directory?(d) }.sort.each do |dir|
50
62
  next if Dir.glob("#{dir}/spec/**/*_spec.rb").empty?
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # strong_migrations rejects unsafe database migrations (lock-heavy ALTERs,
4
+ # non-concurrent indexes, add_column with a volatile default on an existing
5
+ # table, …) before they reach production. It is a development/CI guard — the
6
+ # gem lives in the :development, :test groups and is NOT loaded in production,
7
+ # so this initializer is guarded accordingly. CI runs every migration from an
8
+ # empty database, which is where the checks actually fire.
9
+ return unless defined?(StrongMigrations)
10
+
11
+ # A fresh host has nothing in production yet, so audit ALL migrations rather
12
+ # than grandfathering them. Once you go live, set this to the latest applied
13
+ # version so a from-scratch migrate doesn't re-check historical migrations.
14
+ StrongMigrations.start_after = 0
15
+
16
+ # Match your production Postgres major version so the right checks run in
17
+ # development and CI. Uncomment and set:
18
+ # StrongMigrations.target_version = 16
19
+
20
+ # Safety timeouts. If you use PgBouncer in transaction mode, remove these and
21
+ # set timeouts on the database user instead.
22
+ StrongMigrations.lock_timeout = 10.seconds
23
+ StrongMigrations.statement_timeout = 1.hour
24
+
25
+ # Refresh planner statistics after an index is added.
26
+ StrongMigrations.auto_analyze = true
@@ -21,7 +21,7 @@ module Seams
21
21
  # reads from.
22
22
  #
23
23
  # Run with: bin/rails generate seams:notifications
24
- # rubocop:disable Metrics/ClassLength
24
+ # rubocop:disable-next Metrics/ClassLength
25
25
  class NotificationsGenerator < Rails::Generators::Base
26
26
  include Seams::Generators::HostInjector
27
27
  include Seams::Generators::EjectAware
@@ -67,6 +67,8 @@ module Seams
67
67
  engine_path("lib/notifications/configuration.rb")
68
68
  template_unless_ejected "lib/type_registry.rb.tt",
69
69
  engine_path("lib/notifications/type_registry.rb")
70
+ template_unless_ejected "lib/preferences.rb.tt",
71
+ engine_path("lib/notifications/preferences.rb")
70
72
  # lib/notifications.rb stays framework-managed (root require file).
71
73
  template "lib/notifications.rb.tt", engine_path("lib/notifications.rb"), force: true
72
74
  end
@@ -390,6 +392,5 @@ module Seams
390
392
  RB
391
393
  end
392
394
  end
393
- # rubocop:enable Metrics/ClassLength
394
395
  end
395
396
  end
@@ -7,15 +7,8 @@ module Notifications
7
7
  end
8
8
 
9
9
  def update
10
- # Whitelist the keys we accept. The form posts
11
- # `preferences[<channel>:<type>] = "1" | "0"`. Allowed channels
12
- # are the canonical CHANNELS list; type is either "any" (→ nil)
13
- # or a NotificationPreference::TYPE-style identifier ([a-z0-9_]).
14
10
  preference_params.each do |key, enabled|
15
11
  channel, type = key.split(":", 2)
16
- next unless Notifications::NotificationPreference::CHANNELS.include?(channel)
17
- next unless type.nil? || type == "any" || type.match?(/\A[a-z0-9_]+\z/)
18
-
19
12
  type = nil if type == "any"
20
13
  pref = Notifications::NotificationPreference.find_or_initialize_by(
21
14
  identity_id: current_identity_id,
@@ -29,13 +22,12 @@ module Notifications
29
22
 
30
23
  private
31
24
 
32
- # `params[:preferences].to_h` raises ActionController::UnfilteredParameters
33
- # in Rails default config. Use permit! after filtering to the
34
- # `:preferences` key — the per-key channel/type validation above
35
- # is the real safety net.
25
+ # Permit only the keys the channel/type registry allows. The closed
26
+ # list is derived at request time from NotificationPreference::CHANNELS
27
+ # and Notifications::TypeRegistry, so brakeman sees no mass-assignment
28
+ # risk and unknown keys are rejected by Rails rather than silently dropped.
36
29
  def preference_params
37
- raw = params.require(:preferences)
38
- raw.respond_to?(:permit!) ? raw.permit!.to_h : raw.to_h
30
+ params.require(:preferences).permit(*Notifications::Preferences.allowed_keys)
39
31
  end
40
32
 
41
33
  # Resolves the signed-in identity's id from `Auth::Current.identity`
@@ -16,6 +16,18 @@ module Notifications
16
16
  # seams:insertion-point notifications.engine.events
17
17
  end
18
18
 
19
+ initializer "notifications.register_abilities" do
20
+ # Ability codes this engine owns, registered exactly like events
21
+ # (resource.action.engine). `read` is the member-level view;
22
+ # `manage` is the admin-level mutate. Host grant maps reference
23
+ # these codes; see Seams::Permissions::DEFAULT_GRANTS.
24
+ Seams::PermissionRegistry.register("notification.read.notifications", owned_by: "Notifications")
25
+ Seams::PermissionRegistry.register("notification.manage.notifications", owned_by: "Notifications")
26
+ Seams::PermissionRegistry.register("preference.manage.notifications", owned_by: "Notifications")
27
+ # Follow-up generators that ship new notifications abilities register them here.
28
+ # seams:insertion-point notifications.engine.abilities
29
+ end
30
+
19
31
  initializer "notifications.append_migrations" do |app|
20
32
  unless app.root == root
21
33
  config.paths["db/migrate"].expanded.each do |expanded_path|
@@ -3,6 +3,7 @@
3
3
  require "notifications/version"
4
4
  require "notifications/configuration"
5
5
  require "notifications/type_registry"
6
+ require "notifications/preferences"
6
7
  require "notifications/engine"
7
8
  require "notifications/concerns/notifiable"
8
9
  require "notifications/adapters/abstract"
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notifications
4
+ # Channel/type key registry for the preferences surface.
5
+ #
6
+ # The preferences form posts flat keys of the form
7
+ # +<channel>:<type>+ (e.g. +in_app:billing.invoice_paid+) or
8
+ # +<channel>:any+ for the channel-wide catch-all row. This module
9
+ # derives the closed set of acceptable keys from:
10
+ #
11
+ # * +NotificationPreference::CHANNELS+ — the canonical delivery
12
+ # channels compiled into the engine.
13
+ # * +Notifications::TypeRegistry.names+ — the notification types the
14
+ # host has registered (seeded by +Notifications.seed_default_types!+
15
+ # and extended via the initializer).
16
+ #
17
+ # PreferencesController passes the result to +params.permit(...)+ so
18
+ # Rails rejects unknown keys without relying on +permit!+.
19
+ module Preferences
20
+ module_function
21
+
22
+ # Returns every key the preferences form is allowed to post.
23
+ # Format: +["in_app:any", "in_app:welcome", "email:any", ...]+
24
+ def allowed_keys
25
+ channels = Notifications::NotificationPreference::CHANNELS
26
+ types = Notifications::TypeRegistry.names
27
+
28
+ channels.flat_map do |channel|
29
+ [
30
+ "#{channel}:any",
31
+ *types.map { |type| "#{channel}:#{type}" }
32
+ ]
33
+ end
34
+ end
35
+ end
36
+ end