yes-core 1.0.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/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +69 -0
- data/lib/yes/core/active_job_serializers/command_group_serializer.rb +29 -0
- data/lib/yes/core/active_job_serializers/dry_struct_serializer.rb +57 -0
- data/lib/yes/core/aggregate/draftable.rb +205 -0
- data/lib/yes/core/aggregate/dsl/attribute_data.rb +37 -0
- data/lib/yes/core/aggregate/dsl/attribute_definer.rb +54 -0
- data/lib/yes/core/aggregate/dsl/attribute_definers/aggregate.rb +36 -0
- data/lib/yes/core/aggregate/dsl/attribute_definers/standard.rb +36 -0
- data/lib/yes/core/aggregate/dsl/class_name_convention.rb +80 -0
- data/lib/yes/core/aggregate/dsl/class_resolvers/authorizer.rb +132 -0
- data/lib/yes/core/aggregate/dsl/class_resolvers/base.rb +80 -0
- data/lib/yes/core/aggregate/dsl/class_resolvers/command/authorizer.rb +30 -0
- data/lib/yes/core/aggregate/dsl/class_resolvers/command/authorizer_factory.rb +34 -0
- data/lib/yes/core/aggregate/dsl/class_resolvers/command/base.rb +38 -0
- data/lib/yes/core/aggregate/dsl/class_resolvers/command/cerbos_authorizer.rb +114 -0
- data/lib/yes/core/aggregate/dsl/class_resolvers/command/command.rb +70 -0
- data/lib/yes/core/aggregate/dsl/class_resolvers/command/event.rb +88 -0
- data/lib/yes/core/aggregate/dsl/class_resolvers/command/guard_evaluator.rb +84 -0
- data/lib/yes/core/aggregate/dsl/class_resolvers/command/simple_authorizer.rb +50 -0
- data/lib/yes/core/aggregate/dsl/class_resolvers/command/state_updater.rb +46 -0
- data/lib/yes/core/aggregate/dsl/class_resolvers/read_model.rb +75 -0
- data/lib/yes/core/aggregate/dsl/class_resolvers/read_model_filter.rb +88 -0
- data/lib/yes/core/aggregate/dsl/class_resolvers/read_model_serializer.rb +76 -0
- data/lib/yes/core/aggregate/dsl/command_data.rb +54 -0
- data/lib/yes/core/aggregate/dsl/command_definer.rb +263 -0
- data/lib/yes/core/aggregate/dsl/command_shortcut_expander.rb +233 -0
- data/lib/yes/core/aggregate/dsl/constant_resolver.rb +67 -0
- data/lib/yes/core/aggregate/dsl/method_definers/attribute/accessor.rb +28 -0
- data/lib/yes/core/aggregate/dsl/method_definers/attribute/aggregate_accessor.rb +36 -0
- data/lib/yes/core/aggregate/dsl/method_definers/attribute/base.rb +42 -0
- data/lib/yes/core/aggregate/dsl/method_definers/command/base.rb +42 -0
- data/lib/yes/core/aggregate/dsl/method_definers/command/can_command.rb +41 -0
- data/lib/yes/core/aggregate/dsl/method_definers/command/command.rb +50 -0
- data/lib/yes/core/aggregate/has_authorizer.rb +86 -0
- data/lib/yes/core/aggregate/has_read_model.rb +169 -0
- data/lib/yes/core/aggregate/read_model_rebuilder.rb +40 -0
- data/lib/yes/core/aggregate/shared_read_model_rebuilder.rb +158 -0
- data/lib/yes/core/aggregate.rb +404 -0
- data/lib/yes/core/authentication_error.rb +8 -0
- data/lib/yes/core/authorization/cerbos_client_provider.rb +27 -0
- data/lib/yes/core/authorization/command_authorizer.rb +40 -0
- data/lib/yes/core/authorization/command_cerbos_authorizer.rb +182 -0
- data/lib/yes/core/authorization/read_model_authorizer.rb +22 -0
- data/lib/yes/core/authorization/read_models_authorizer.rb +49 -0
- data/lib/yes/core/authorization/read_request_authorizer.rb +32 -0
- data/lib/yes/core/authorization/read_request_cerbos_authorizer.rb +112 -0
- data/lib/yes/core/command.rb +35 -0
- data/lib/yes/core/command_handling/aggregate_tracker.rb +33 -0
- data/lib/yes/core/command_handling/command_executor.rb +171 -0
- data/lib/yes/core/command_handling/command_handler.rb +124 -0
- data/lib/yes/core/command_handling/event_publisher.rb +189 -0
- data/lib/yes/core/command_handling/guard_evaluator.rb +165 -0
- data/lib/yes/core/command_handling/guard_runner.rb +76 -0
- data/lib/yes/core/command_handling/payload_proxy.rb +159 -0
- data/lib/yes/core/command_handling/read_model_recovery_service.rb +264 -0
- data/lib/yes/core/command_handling/read_model_revision_guard.rb +198 -0
- data/lib/yes/core/command_handling/read_model_updater.rb +103 -0
- data/lib/yes/core/command_handling/state_updater.rb +113 -0
- data/lib/yes/core/commands/bus.rb +46 -0
- data/lib/yes/core/commands/group.rb +135 -0
- data/lib/yes/core/commands/group_response.rb +13 -0
- data/lib/yes/core/commands/helper.rb +126 -0
- data/lib/yes/core/commands/notifier.rb +65 -0
- data/lib/yes/core/commands/processor.rb +137 -0
- data/lib/yes/core/commands/response.rb +63 -0
- data/lib/yes/core/commands/stateless/group_handler.rb +186 -0
- data/lib/yes/core/commands/stateless/group_response.rb +15 -0
- data/lib/yes/core/commands/stateless/handler.rb +292 -0
- data/lib/yes/core/commands/stateless/handler_helpers.rb +321 -0
- data/lib/yes/core/commands/stateless/response.rb +14 -0
- data/lib/yes/core/commands/stateless/subject.rb +41 -0
- data/lib/yes/core/commands/validator.rb +28 -0
- data/lib/yes/core/configuration.rb +432 -0
- data/lib/yes/core/data_decryptor.rb +59 -0
- data/lib/yes/core/data_encryptor.rb +60 -0
- data/lib/yes/core/encryption_metadata.rb +33 -0
- data/lib/yes/core/error.rb +14 -0
- data/lib/yes/core/error_messages.rb +37 -0
- data/lib/yes/core/event.rb +222 -0
- data/lib/yes/core/event_class_resolver.rb +40 -0
- data/lib/yes/core/generators/read_models/add_pending_update_tracking_generator.rb +43 -0
- data/lib/yes/core/generators/read_models/templates/add_pending_update_tracking.rb.erb +122 -0
- data/lib/yes/core/generators/read_models/templates/migration.rb.erb +9 -0
- data/lib/yes/core/generators/read_models/update_generator.rb +147 -0
- data/lib/yes/core/jobs/read_model_recovery_job.rb +219 -0
- data/lib/yes/core/middlewares/encryptor.rb +48 -0
- data/lib/yes/core/middlewares/timestamp.rb +29 -0
- data/lib/yes/core/middlewares/with_indifferent_access.rb +22 -0
- data/lib/yes/core/models/application_record.rb +9 -0
- data/lib/yes/core/open_telemetry/otl_span.rb +110 -0
- data/lib/yes/core/open_telemetry/trackable.rb +101 -0
- data/lib/yes/core/payload_store/base.rb +33 -0
- data/lib/yes/core/payload_store/errors.rb +13 -0
- data/lib/yes/core/payload_store/lookup.rb +44 -0
- data/lib/yes/core/process_managers/access_token_client.rb +107 -0
- data/lib/yes/core/process_managers/base.rb +40 -0
- data/lib/yes/core/process_managers/command_runner.rb +109 -0
- data/lib/yes/core/process_managers/service_client.rb +57 -0
- data/lib/yes/core/process_managers/state.rb +118 -0
- data/lib/yes/core/railtie.rb +58 -0
- data/lib/yes/core/read_model/builder.rb +267 -0
- data/lib/yes/core/read_model/event_handler.rb +64 -0
- data/lib/yes/core/read_model/filter.rb +118 -0
- data/lib/yes/core/read_model/filter_query_builder.rb +104 -0
- data/lib/yes/core/serializer.rb +21 -0
- data/lib/yes/core/subscriptions.rb +94 -0
- data/lib/yes/core/test_support/event_helpers.rb +27 -0
- data/lib/yes/core/test_support/jwt_helpers.rb +30 -0
- data/lib/yes/core/test_support/subscriptions_helper.rb +88 -0
- data/lib/yes/core/test_support/test_helper.rb +27 -0
- data/lib/yes/core/test_support.rb +5 -0
- data/lib/yes/core/transaction_details.rb +90 -0
- data/lib/yes/core/type_lookup.rb +88 -0
- data/lib/yes/core/types.rb +110 -0
- data/lib/yes/core/utils/aggregate_shortcuts.rb +164 -0
- data/lib/yes/core/utils/caller_utils.rb +37 -0
- data/lib/yes/core/utils/command_utils.rb +226 -0
- data/lib/yes/core/utils/error_notifier.rb +101 -0
- data/lib/yes/core/utils/event_name_resolver.rb +67 -0
- data/lib/yes/core/utils/exponential_retrier.rb +180 -0
- data/lib/yes/core/utils/hash_utils.rb +63 -0
- data/lib/yes/core/version.rb +7 -0
- data/lib/yes/core.rb +85 -0
- data/lib/yes.rb +0 -0
- metadata +324 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
# Returns the singleton instance of the configuration
|
|
6
|
+
# @return [Yes::Core::Configuration] The configuration instance
|
|
7
|
+
# @example
|
|
8
|
+
# config = Yes::Core.configuration
|
|
9
|
+
# config.register_command_class(:user, :create, CreateUserCommand)
|
|
10
|
+
def self.configuration
|
|
11
|
+
@configuration ||= Configuration.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Configures Yes::Core
|
|
15
|
+
# @yield [Yes::Core::Configuration] The configuration instance
|
|
16
|
+
# @example
|
|
17
|
+
# Yes::Core.configure do |config|
|
|
18
|
+
# config.aggregate_shortcuts = true
|
|
19
|
+
# end
|
|
20
|
+
def self.configure
|
|
21
|
+
yield configuration
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class Configuration
|
|
25
|
+
# @return [Boolean] Enable aggregate shortcuts in Rails console (default: false)
|
|
26
|
+
attr_accessor :aggregate_shortcuts
|
|
27
|
+
|
|
28
|
+
# @return [#call] A callable that receives auth_data and returns boolean indicating super admin status
|
|
29
|
+
attr_accessor :super_admin_check
|
|
30
|
+
|
|
31
|
+
# @return [#call] A callable that receives auth_data and returns a principal data hash for Cerbos (commands)
|
|
32
|
+
attr_accessor :cerbos_principal_data_builder
|
|
33
|
+
|
|
34
|
+
# @return [#call] A callable that receives auth_data and returns a principal data hash for Cerbos (read requests)
|
|
35
|
+
# Falls back to cerbos_principal_data_builder if not set.
|
|
36
|
+
attr_writer :cerbos_read_principal_data_builder
|
|
37
|
+
|
|
38
|
+
def cerbos_read_principal_data_builder
|
|
39
|
+
@cerbos_read_principal_data_builder || @cerbos_principal_data_builder
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @return [String] URL of the Cerbos server
|
|
43
|
+
attr_accessor :cerbos_url
|
|
44
|
+
|
|
45
|
+
# @return [Boolean] Whether to use TLS for Cerbos connections (default: true)
|
|
46
|
+
attr_accessor :cerbos_tls
|
|
47
|
+
|
|
48
|
+
# @return [Boolean] Whether to include metadata in Cerbos command authorizer responses
|
|
49
|
+
attr_accessor :cerbos_commands_authorizer_include_metadata
|
|
50
|
+
|
|
51
|
+
# @return [Boolean] Whether to include metadata in Cerbos read authorizer responses
|
|
52
|
+
attr_accessor :cerbos_read_authorizer_include_metadata
|
|
53
|
+
|
|
54
|
+
# @return [Array<String>] Default actions for Cerbos read authorizer
|
|
55
|
+
attr_accessor :cerbos_read_authorizer_actions
|
|
56
|
+
|
|
57
|
+
# @return [String] Prefix for Cerbos read authorizer resource ids
|
|
58
|
+
attr_accessor :cerbos_read_authorizer_resource_id_prefix
|
|
59
|
+
|
|
60
|
+
# @return [Object] Logger instance
|
|
61
|
+
attr_accessor :logger
|
|
62
|
+
|
|
63
|
+
# @return [#call, nil] A callable error reporter responding to #call(error, context:).
|
|
64
|
+
# When nil, errors are only logged.
|
|
65
|
+
# Example: ->(error, context:) { Sentry.capture_exception(error, extra: context) }
|
|
66
|
+
attr_accessor :error_reporter
|
|
67
|
+
|
|
68
|
+
# @return [Object, nil] Payload store client for resolving large payload references
|
|
69
|
+
attr_accessor :payload_store_client
|
|
70
|
+
|
|
71
|
+
# @return [Boolean] Whether to process commands inline (synchronously) or via ActiveJob
|
|
72
|
+
attr_accessor :process_commands_inline
|
|
73
|
+
|
|
74
|
+
# @return [Array<Class>] Command notifier classes to instantiate for batch notifications
|
|
75
|
+
attr_accessor :command_notifier_classes
|
|
76
|
+
|
|
77
|
+
# @return [Object, nil] OpenTelemetry tracer instance. When nil, all tracing is no-op.
|
|
78
|
+
attr_accessor :otl_tracer
|
|
79
|
+
|
|
80
|
+
# @return [String] Anonymous principal ID for Cerbos read authorizer
|
|
81
|
+
attr_accessor :cerbos_read_authorizer_principal_anonymous_id
|
|
82
|
+
|
|
83
|
+
# @return [Boolean] Whether to raise on missing handler methods in aggregate state
|
|
84
|
+
attr_accessor :raise_on_missing_handler_method
|
|
85
|
+
|
|
86
|
+
# @return [String, nil] URL for subscription heartbeat pings (default: nil, disables heartbeat)
|
|
87
|
+
attr_accessor :subscriptions_heartbeat_url
|
|
88
|
+
|
|
89
|
+
# @return [Integer] Interval in seconds between heartbeat pings (default: 30)
|
|
90
|
+
attr_accessor :subscriptions_heartbeat_interval
|
|
91
|
+
|
|
92
|
+
# @return [String] Service name for telemetry and identification
|
|
93
|
+
attr_accessor :service_name
|
|
94
|
+
|
|
95
|
+
# @return [String] Service version for telemetry
|
|
96
|
+
attr_accessor :service_version
|
|
97
|
+
|
|
98
|
+
# @return [#call, nil] Authentication adapter for API controllers.
|
|
99
|
+
# Must respond to #authenticate(request) (returns auth data hash, raises on failure),
|
|
100
|
+
# #verify_token(token) and #error_classes (returns array of error classes).
|
|
101
|
+
attr_accessor :auth_adapter
|
|
102
|
+
|
|
103
|
+
# Initializes a new configuration instance with nested hashes for class storage.
|
|
104
|
+
def initialize
|
|
105
|
+
@registered_classes = Hash.new do |h, k|
|
|
106
|
+
h[k] = Hash.new { |h2, k2| h2[k2] = {} }
|
|
107
|
+
end
|
|
108
|
+
@aggregate_shortcuts = false
|
|
109
|
+
@super_admin_check = ->(_auth_data) { false }
|
|
110
|
+
@cerbos_principal_data_builder = lambda { |auth_data|
|
|
111
|
+
{ id: auth_data[:identity_id], roles: [], attributes: {} }
|
|
112
|
+
}
|
|
113
|
+
@cerbos_url = ENV.fetch('CERBOS_URL', 'cerbos-cluster-ip-service:3593')
|
|
114
|
+
@cerbos_tls = true
|
|
115
|
+
@cerbos_commands_authorizer_include_metadata = false
|
|
116
|
+
@cerbos_read_authorizer_include_metadata = false
|
|
117
|
+
@cerbos_read_authorizer_actions = %w[read]
|
|
118
|
+
@cerbos_read_authorizer_resource_id_prefix = 'read-'
|
|
119
|
+
@cerbos_read_authorizer_principal_anonymous_id = 'anonymous'
|
|
120
|
+
@logger = nil
|
|
121
|
+
@error_reporter = nil
|
|
122
|
+
@payload_store_client = nil
|
|
123
|
+
@process_commands_inline = true
|
|
124
|
+
@command_notifier_classes = []
|
|
125
|
+
@otl_tracer = nil
|
|
126
|
+
@raise_on_missing_handler_method = defined?(Rails) ? Rails.env.local? : false
|
|
127
|
+
@subscriptions_heartbeat_url = nil
|
|
128
|
+
@subscriptions_heartbeat_interval = 30
|
|
129
|
+
@service_name = ENV.fetch('SERVICE_NAME', nil)
|
|
130
|
+
@service_version = ENV.fetch('APP_VERSION', '')
|
|
131
|
+
@auth_adapter = nil
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Register a class for a specific aggregate and type
|
|
135
|
+
# @param context_name [Symbol, String] The context for the aggregate
|
|
136
|
+
# @param aggregate_name [Symbol, String] The name of the aggregate
|
|
137
|
+
# @param action_name [Symbol, String] The name of the command/event
|
|
138
|
+
# @param type [Symbol] The type (:command, :event, or :guard_evaluator)
|
|
139
|
+
# @param klass [Class] The class to register
|
|
140
|
+
# @example Register a command class
|
|
141
|
+
# register_aggregate_class(:authentication, :user, :create, :command, CreateUserCommand)
|
|
142
|
+
def register_aggregate_class(context_name, aggregate_name, action_name, type, klass)
|
|
143
|
+
key = [context_name, aggregate_name]
|
|
144
|
+
@registered_classes[key][type][action_name] = klass
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Register a read model class for a specific aggregate
|
|
148
|
+
# @param context_name [Symbol, String] The context for the aggregate
|
|
149
|
+
# @param aggregate_name [Symbol, String] The name of the aggregate
|
|
150
|
+
# @param klass [Class] The class to register
|
|
151
|
+
# @example
|
|
152
|
+
# register_read_model_class(:authentication, :user, UserReadModel)
|
|
153
|
+
def register_read_model_class(context_name, aggregate_name, klass, draft: false)
|
|
154
|
+
key = [context_name, aggregate_name]
|
|
155
|
+
read_model_key = draft ? :draft_read_model : :read_model
|
|
156
|
+
@registered_classes[key][read_model_key] = klass
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Register a read model filter class for a specific aggregate
|
|
160
|
+
# @param context_name [Symbol, String] The context for the aggregate
|
|
161
|
+
# @param aggregate_name [Symbol, String] The name of the aggregate
|
|
162
|
+
# @param klass [Class] The class to register
|
|
163
|
+
# @example
|
|
164
|
+
# register_read_model_filter_class(:authentication, :user, UserReadModelFilter)
|
|
165
|
+
def register_read_model_filter_class(context_name, aggregate_name, klass)
|
|
166
|
+
key = [context_name, aggregate_name]
|
|
167
|
+
@registered_classes[key][:read_model_filter] = klass
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Register a command class for a specific aggregate
|
|
171
|
+
# @param context_name [Symbol, String] The context for the aggregate
|
|
172
|
+
# @param aggregate_name [Symbol, String] The name of the aggregate
|
|
173
|
+
# @param command_name [Symbol, String] The name of the command
|
|
174
|
+
# @param klass [Class] The class to register
|
|
175
|
+
# @example
|
|
176
|
+
# register_command_class(:authentication, :user, :create, CreateUserCommand)
|
|
177
|
+
def register_command_class(context_name, aggregate_name, command_name, klass)
|
|
178
|
+
register_aggregate_class(context_name, aggregate_name, command_name, :command, klass)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Register an event class for a specific aggregate
|
|
182
|
+
# @param context_name [Symbol, String] The context for the aggregate
|
|
183
|
+
# @param aggregate_name [Symbol, String] The name of the aggregate
|
|
184
|
+
# @param event_name [Symbol, String] The name of the event
|
|
185
|
+
# @param klass [Class] The class to register
|
|
186
|
+
# @example
|
|
187
|
+
# register_event_class(:authentication, :user, :created, UserCreatedEvent)
|
|
188
|
+
def register_event_class(context_name, aggregate_name, event_name, klass)
|
|
189
|
+
register_aggregate_class(context_name, aggregate_name, event_name, :event, klass)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Register a guard evaluator class for a specific aggregate
|
|
193
|
+
# @param context_name [Symbol, String] The context for the aggregate
|
|
194
|
+
# @param aggregate_name [Symbol, String] The name of the aggregate
|
|
195
|
+
# @param command_name [Symbol, String] The name of the command
|
|
196
|
+
# @param klass [Class] The class to register
|
|
197
|
+
# @example
|
|
198
|
+
# register_guard_evaluator_class(:authentication, :user, :create, CreateUserGuardEvaluator)
|
|
199
|
+
def register_guard_evaluator_class(context_name, aggregate_name, command_name, klass)
|
|
200
|
+
register_aggregate_class(context_name, aggregate_name, command_name, :guard_evaluator, klass)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Register an aggregate authorizer class for a specific aggregate
|
|
204
|
+
# @param context_name [Symbol, String] The context for the aggregate
|
|
205
|
+
# @param aggregate_name [Symbol, String] The name of the aggregate
|
|
206
|
+
# @param klass [Class] The authorizer class to register
|
|
207
|
+
# @example
|
|
208
|
+
# register_aggregate_authorizer_class(:authentication, :user, UserAuthorizer)
|
|
209
|
+
def register_aggregate_authorizer_class(context_name, aggregate_name, klass)
|
|
210
|
+
key = [context_name, aggregate_name]
|
|
211
|
+
@registered_classes[key][:aggregate_authorizer] = klass
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Register a command authorizer class for a specific aggregate
|
|
215
|
+
# @param context_name [Symbol, String] The context for the aggregate
|
|
216
|
+
# @param aggregate_name [Symbol, String] The name of the aggregate
|
|
217
|
+
# @param command_name [Symbol, String] The name of the command
|
|
218
|
+
# @param klass [Class] The authorizer class to register
|
|
219
|
+
# @example
|
|
220
|
+
# register_command_authorizer_class(:sales, :user, :create, CreateUserAuthorizer)
|
|
221
|
+
def register_command_authorizer_class(context_name, aggregate_name, command_name, klass)
|
|
222
|
+
register_aggregate_class(context_name, aggregate_name, command_name, :authorizer, klass)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Register the event(s) associated with a specific command
|
|
226
|
+
# @param context_name [Symbol, String] The context for the aggregate
|
|
227
|
+
# @param aggregate_name [Symbol, String] The name of the aggregate
|
|
228
|
+
# @param command_name [Symbol, String] The name of the command
|
|
229
|
+
# @param event_names [Array<Symbol, String>] An array of event names
|
|
230
|
+
# @example
|
|
231
|
+
# register_command_events(:authentication, :user, :create, [:user_created, :welcome_email_sent])
|
|
232
|
+
def register_command_events(context_name, aggregate_name, command_name, event_names)
|
|
233
|
+
key = [context_name, aggregate_name]
|
|
234
|
+
mappings = @registered_classes[key][:command_event_mappings] ||= {}
|
|
235
|
+
mappings[command_name] = event_names
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Retrieve all command-to-event mappings for a specific aggregate
|
|
239
|
+
# @param context_name [Symbol, String] The context for the aggregate
|
|
240
|
+
# @param aggregate_name [Symbol, String] The name of the aggregate
|
|
241
|
+
# @return [Hash] A hash where keys are command names and values are arrays of event names
|
|
242
|
+
# @example
|
|
243
|
+
# mappings = command_event_mappings(:authentication, :user)
|
|
244
|
+
def command_event_mappings(context_name, aggregate_name)
|
|
245
|
+
key = [context_name, aggregate_name]
|
|
246
|
+
@registered_classes[key][:command_event_mappings] || {}
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Retrieve the event names associated with a specific command
|
|
250
|
+
# @param context_name [Symbol, String] The context for the aggregate
|
|
251
|
+
# @param aggregate_name [Symbol, String] The name of the aggregate
|
|
252
|
+
# @param command_name [Symbol, String] The name of the command
|
|
253
|
+
# @return [Array<Symbol, String>] An array of event names associated with the command, or an empty array if none
|
|
254
|
+
# @example
|
|
255
|
+
# event_names = command_event_mapping(:authentication, :user, :create)
|
|
256
|
+
def command_event_mapping(context_name, aggregate_name, command_name)
|
|
257
|
+
command_event_mappings(context_name, aggregate_name)[command_name] || []
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Retrieve the actual event classes associated with a specific command
|
|
261
|
+
# @param context_name [Symbol, String] The context for the aggregate
|
|
262
|
+
# @param aggregate_name [Symbol, String] The name of the aggregate
|
|
263
|
+
# @param command_name [Symbol, String] The name of the command
|
|
264
|
+
# @return [Array<Class>] An array of event classes associated with the command
|
|
265
|
+
# @example
|
|
266
|
+
# event_classes = event_classes_for_command(:authentication, :user, :create)
|
|
267
|
+
def event_classes_for_command(context_name, aggregate_name, command_name)
|
|
268
|
+
command_event_mapping(context_name, aggregate_name, command_name).map do |event_name|
|
|
269
|
+
aggregate_class(context_name, aggregate_name, event_name, :event)
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Retrieve a registered class for a given aggregate, action, and type
|
|
274
|
+
# @param context_name [Symbol, String] The context for the aggregate
|
|
275
|
+
# @param aggregate_name [Symbol, String] The name of the aggregate
|
|
276
|
+
# @param action_name [Symbol, String, nil] The name of the action (command/event), nil for read_model types
|
|
277
|
+
# @param type [Symbol] The type of the class (:command, :event, :guard_evaluator, :read_model, :draft_read_model)
|
|
278
|
+
# @return [Class, nil] The registered class or nil if not found
|
|
279
|
+
# @example Get a command class
|
|
280
|
+
# command_class = aggregate_class(:authentication, :user, :create, :command)
|
|
281
|
+
# @example Get a read model class
|
|
282
|
+
# read_model_class = aggregate_class(:authentication, :user, nil, :read_model)
|
|
283
|
+
def aggregate_class(context_name, aggregate_name, action_name, type)
|
|
284
|
+
if action_name.nil?
|
|
285
|
+
@registered_classes.dig([context_name, aggregate_name], type)
|
|
286
|
+
else
|
|
287
|
+
@registered_classes.dig([context_name, aggregate_name], type, action_name)
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# List all registered classes for a specific aggregate in a context
|
|
292
|
+
# @param context_name [Symbol, String] The context for the aggregate
|
|
293
|
+
# @param aggregate_name [Symbol, String] The name of the aggregate
|
|
294
|
+
# @return [Hash] A hash of registered classes grouped by type
|
|
295
|
+
# @example
|
|
296
|
+
# classes = list_aggregate_classes("Authentication", "User)
|
|
297
|
+
def list_aggregate_classes(context_name, aggregate_name)
|
|
298
|
+
@registered_classes[[context_name, aggregate_name]]
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Retrieve a guard evaluator class for a specific command
|
|
302
|
+
# @param context_name [Symbol, String] The context for the aggregate
|
|
303
|
+
# @param aggregate_name [Symbol, String] The name of the aggregate
|
|
304
|
+
# @param command_name [Symbol, String] The name of the command
|
|
305
|
+
# @return [Class, nil] The registered guard evaluator class or nil if not found
|
|
306
|
+
# @example
|
|
307
|
+
# evaluator = guard_evaluator_class(:authentication, :user, :create)
|
|
308
|
+
def guard_evaluator_class(context_name, aggregate_name, command_name)
|
|
309
|
+
aggregate_class(context_name, aggregate_name, command_name.to_s.underscore.to_sym, :guard_evaluator)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# List all registered classes across all aggregates and contexts
|
|
313
|
+
# @return [Hash] A complete hash of all registered classes
|
|
314
|
+
# @example
|
|
315
|
+
# all_classes = list_all_registered_classes
|
|
316
|
+
# # Returns:
|
|
317
|
+
# # {
|
|
318
|
+
# # [:authentication, :user] => {
|
|
319
|
+
# # command: { create: CreateUserCommand },
|
|
320
|
+
# # event: { created: UserCreatedEvent },
|
|
321
|
+
# # guard_evaluator: { create: CreateUserGuardEvaluator }
|
|
322
|
+
# # }
|
|
323
|
+
# # }
|
|
324
|
+
def list_all_registered_classes
|
|
325
|
+
@registered_classes
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Get all read model class names from registered aggregates
|
|
329
|
+
# @return [Array<String>] Array of read model class names
|
|
330
|
+
# @example
|
|
331
|
+
# read_model_classes = all_read_model_class_names
|
|
332
|
+
# # Returns: ["UserReadModel", "UserChangesReadModel", "ProfileReadModel", ...]
|
|
333
|
+
def all_read_model_class_names
|
|
334
|
+
list_all_registered_classes.keys.flat_map do |context_aggregate|
|
|
335
|
+
context_name, aggregate_name = context_aggregate
|
|
336
|
+
aggregate_class_name = "#{context_name.to_s.camelize}::#{aggregate_name.to_s.camelize}::Aggregate"
|
|
337
|
+
|
|
338
|
+
begin
|
|
339
|
+
aggregate_class = aggregate_class_name.constantize
|
|
340
|
+
models = []
|
|
341
|
+
|
|
342
|
+
# Add main read model if it exists
|
|
343
|
+
models << aggregate_class.read_model_name.camelize.to_s if aggregate_class.respond_to?(:read_model_name) && aggregate_class.read_model_name
|
|
344
|
+
|
|
345
|
+
# Add changes read model if aggregate is draftable
|
|
346
|
+
models << aggregate_class.changes_read_model_name.camelize.to_s if aggregate_class.respond_to?(:changes_read_model_name) && aggregate_class.changes_read_model_name
|
|
347
|
+
|
|
348
|
+
models
|
|
349
|
+
rescue NameError
|
|
350
|
+
# Skip if aggregate class doesn't exist
|
|
351
|
+
[]
|
|
352
|
+
end
|
|
353
|
+
end.compact.uniq
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Get all read model classes (constantized)
|
|
357
|
+
# @return [Array<Class>] Array of read model classes
|
|
358
|
+
# @example
|
|
359
|
+
# read_model_classes = all_read_model_classes
|
|
360
|
+
# # Returns: [UserReadModel, UserChangesReadModel, ProfileReadModel, ...]
|
|
361
|
+
def all_read_model_classes
|
|
362
|
+
all_read_model_class_names.filter_map do |class_name|
|
|
363
|
+
class_name.constantize
|
|
364
|
+
rescue NameError
|
|
365
|
+
nil
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Get all read model classes with their associated aggregate classes
|
|
370
|
+
# @return [Array<Hash>] Array of hashes with read_model_class, aggregate_class, and is_draft flag
|
|
371
|
+
# @example
|
|
372
|
+
# mappings = all_read_models_with_aggregate_classes
|
|
373
|
+
# # Returns: [
|
|
374
|
+
# # { read_model_class: UserReadModel, aggregate_class: User::Aggregate, is_draft: false },
|
|
375
|
+
# # { read_model_class: UserChangesReadModel, aggregate_class: User::Aggregate, is_draft: true }
|
|
376
|
+
# # ]
|
|
377
|
+
def all_read_models_with_aggregate_classes
|
|
378
|
+
list_all_registered_classes.keys.flat_map do |context_aggregate|
|
|
379
|
+
context_name, aggregate_name = context_aggregate
|
|
380
|
+
aggregate_class_name = "#{context_name.to_s.camelize}::#{aggregate_name.to_s.camelize}::Aggregate"
|
|
381
|
+
|
|
382
|
+
begin
|
|
383
|
+
aggregate_class = aggregate_class_name.constantize
|
|
384
|
+
models = []
|
|
385
|
+
|
|
386
|
+
# Main read model (not draft)
|
|
387
|
+
if aggregate_class.respond_to?(:read_model_name) && aggregate_class.read_model_name
|
|
388
|
+
begin
|
|
389
|
+
read_model_class = aggregate_class.read_model_name.camelize.constantize
|
|
390
|
+
models << {
|
|
391
|
+
read_model_class: read_model_class,
|
|
392
|
+
aggregate_class: aggregate_class,
|
|
393
|
+
is_draft: false
|
|
394
|
+
}
|
|
395
|
+
rescue NameError
|
|
396
|
+
# Skip if read model class doesn't exist
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Changes read model (draft)
|
|
401
|
+
if aggregate_class.respond_to?(:changes_read_model_name) && aggregate_class.changes_read_model_name
|
|
402
|
+
begin
|
|
403
|
+
changes_model_class = aggregate_class.changes_read_model_name.camelize.constantize
|
|
404
|
+
models << {
|
|
405
|
+
read_model_class: changes_model_class,
|
|
406
|
+
aggregate_class: aggregate_class,
|
|
407
|
+
is_draft: true
|
|
408
|
+
}
|
|
409
|
+
rescue NameError
|
|
410
|
+
# Skip if changes read model class doesn't exist
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
models
|
|
415
|
+
rescue NameError
|
|
416
|
+
# Skip if aggregate class doesn't exist
|
|
417
|
+
[]
|
|
418
|
+
end
|
|
419
|
+
end.compact
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# Get all read model table names
|
|
423
|
+
# @return [Array<String>] Array of read model table names
|
|
424
|
+
# @example
|
|
425
|
+
# table_names = all_read_model_table_names
|
|
426
|
+
# # Returns: ["user_read_models", "user_changes_read_models", "profile_read_models", ...]
|
|
427
|
+
def all_read_model_table_names
|
|
428
|
+
all_read_model_classes.map(&:table_name).uniq
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
# Decrypts event data attributes using a key from the key repository.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# decryptor = DataDecryptor.new(data: event.data, schema: event.metadata['encryption'], repository: repo)
|
|
9
|
+
# decrypted_data = decryptor.call
|
|
10
|
+
class DataDecryptor
|
|
11
|
+
# Decrypts the data attributes specified in the encryption metadata.
|
|
12
|
+
#
|
|
13
|
+
# @return [Hash] the decrypted data
|
|
14
|
+
def call
|
|
15
|
+
return encrypted_data if encryption_metadata.empty?
|
|
16
|
+
|
|
17
|
+
result = find_key(encryption_metadata['key'])
|
|
18
|
+
return encrypted_data unless result.success?
|
|
19
|
+
|
|
20
|
+
decrypt_attributes(
|
|
21
|
+
key: result.value!,
|
|
22
|
+
data: encrypted_data,
|
|
23
|
+
attributes: encryption_metadata['attributes']
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
attr_reader :key_repository, :encryption_metadata, :encrypted_data
|
|
30
|
+
|
|
31
|
+
# @param data [Hash] the encrypted event data
|
|
32
|
+
# @param schema [Hash, nil] the encryption metadata from the event
|
|
33
|
+
# @param repository [#find, #decrypt] the key repository
|
|
34
|
+
def initialize(data:, schema:, repository:)
|
|
35
|
+
@encrypted_data = Yes::Core::Utils::HashUtils.deep_dup(data).transform_keys!(&:to_s)
|
|
36
|
+
@key_repository = repository
|
|
37
|
+
@encryption_metadata = schema&.transform_keys(&:to_s) || {}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def decrypt_attributes(key:, data:, attributes: {}) # rubocop:disable Lint/UnusedMethodArgument
|
|
41
|
+
return data unless key
|
|
42
|
+
|
|
43
|
+
res = key_repository.decrypt(key:, message: data['es_encrypted'])
|
|
44
|
+
return data if res.failure?
|
|
45
|
+
|
|
46
|
+
decrypted_text = res.value!
|
|
47
|
+
decrypted = JSON.parse(decrypted_text.attributes[:message]).transform_keys(&:to_s)
|
|
48
|
+
decrypted.each { |k, value| data[k] = value if data.key?(k) }
|
|
49
|
+
data.delete('es_encrypted')
|
|
50
|
+
data
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @return [Dry::Monads::Result]
|
|
54
|
+
def find_key(identifier)
|
|
55
|
+
key_repository.find(identifier)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
# Encrypts event data attributes using a key from the key repository.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# encryptor = DataEncryptor.new(data: event.data, schema: event.class.encryption_schema, repository: repo)
|
|
9
|
+
# encryptor.call
|
|
10
|
+
# encryptor.encrypted_data
|
|
11
|
+
# encryptor.encryption_metadata
|
|
12
|
+
class DataEncryptor
|
|
13
|
+
# @return [Hash] the encrypted data
|
|
14
|
+
attr_reader :encrypted_data
|
|
15
|
+
|
|
16
|
+
# @return [Hash] the encryption metadata (key, iv, attributes)
|
|
17
|
+
attr_reader :encryption_metadata
|
|
18
|
+
|
|
19
|
+
# Encrypts the data attributes specified in the schema.
|
|
20
|
+
#
|
|
21
|
+
# @return [Hash] the encrypted data
|
|
22
|
+
def call
|
|
23
|
+
return encrypted_data if encryption_metadata.empty?
|
|
24
|
+
|
|
25
|
+
key_id = encryption_metadata[:key]
|
|
26
|
+
res = key_repository.find(key_id)
|
|
27
|
+
res = key_repository.create(key_id) if res.failure?
|
|
28
|
+
key = res.value!
|
|
29
|
+
|
|
30
|
+
encryption_metadata[:iv] = key.attributes[:iv]
|
|
31
|
+
encrypt_attributes(
|
|
32
|
+
key:,
|
|
33
|
+
data: encrypted_data,
|
|
34
|
+
attributes: encryption_metadata[:attributes].map(&:to_s)
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
attr_reader :key_repository
|
|
41
|
+
|
|
42
|
+
# @param data [Hash] the event data
|
|
43
|
+
# @param schema [Hash] the encryption schema
|
|
44
|
+
# @param repository [#find, #create, #encrypt, #decrypt] the key repository
|
|
45
|
+
def initialize(data:, schema:, repository:)
|
|
46
|
+
@encrypted_data = Yes::Core::Utils::HashUtils.deep_dup(data).transform_keys!(&:to_s)
|
|
47
|
+
@key_repository = repository
|
|
48
|
+
@encryption_metadata = EncryptionMetadata.new(data:, schema:).call
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def encrypt_attributes(key:, data:, attributes:)
|
|
52
|
+
text = JSON.generate(data.select { |hash_key, _value| attributes.include?(hash_key.to_s) })
|
|
53
|
+
encrypted = key_repository.encrypt(key:, message: text).value!
|
|
54
|
+
attributes.each { |att| data[att.to_s] = 'es_encrypted' if data.key?(att.to_s) }
|
|
55
|
+
data['es_encrypted'] = encrypted.attributes[:message]
|
|
56
|
+
data
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
# Extracts encryption metadata from a command/event's encryption schema.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# metadata = EncryptionMetadata.new(data: event.data, schema: event.class.encryption_schema)
|
|
9
|
+
# metadata.call # => { key: 'user-123', attributes: [:email, :phone] }
|
|
10
|
+
class EncryptionMetadata
|
|
11
|
+
# @return [Hash] the encryption metadata (key, attributes) or empty hash
|
|
12
|
+
def call
|
|
13
|
+
return {} unless schema
|
|
14
|
+
|
|
15
|
+
{
|
|
16
|
+
key: schema[:key].call(data),
|
|
17
|
+
attributes: schema[:attributes].map(&:to_sym)
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
attr_reader :data, :schema
|
|
24
|
+
|
|
25
|
+
# @param data [Hash] the event data
|
|
26
|
+
# @param schema [Hash, nil] the encryption schema with :key (callable) and :attributes (array)
|
|
27
|
+
def initialize(data:, schema:)
|
|
28
|
+
@data = data.transform_keys(&:to_sym)
|
|
29
|
+
@schema = schema
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
# Helper class for looking up error messages from I18n
|
|
6
|
+
class ErrorMessages
|
|
7
|
+
class << self
|
|
8
|
+
# Looks up the error message for a guard from I18n translations
|
|
9
|
+
#
|
|
10
|
+
# @param context_name [String] The context name
|
|
11
|
+
# @param aggregate_name [String] The aggregate name
|
|
12
|
+
# @param command_name [String] The command name
|
|
13
|
+
# @param guard_name [Symbol] The name of the guard
|
|
14
|
+
# @return [String] The error message
|
|
15
|
+
def guard_error(context_name, aggregate_name, command_name, guard_name)
|
|
16
|
+
context_key, aggregate_key, command_key, guard_key = normalize_keys(
|
|
17
|
+
context_name, aggregate_name, command_name, guard_name
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Try to find the error message in the following order:
|
|
21
|
+
# 1. Specific translation for this aggregate attribute guard
|
|
22
|
+
# 2. Default fallback message
|
|
23
|
+
I18n.t(
|
|
24
|
+
"aggregates.#{context_key}.#{aggregate_key}.commands.#{command_key}.guards.#{guard_key}.error",
|
|
25
|
+
default: "Guard '#{guard_key}' failed"
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def normalize_keys(*keys)
|
|
32
|
+
keys.map { _1.to_s.underscore }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|