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,186 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
module Commands
|
|
6
|
+
module Stateless
|
|
7
|
+
# Handles a group of commands
|
|
8
|
+
class GroupHandler
|
|
9
|
+
include HandlerHelpers
|
|
10
|
+
|
|
11
|
+
class InvalidCommandGroupError < Error
|
|
12
|
+
def initialize(cmd_module_name, handler_module_name)
|
|
13
|
+
super("command #{cmd_module_name} does not match handler #{handler_module_name}")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class CustomHandlerMethodMissingError < Error; end
|
|
18
|
+
class CommandsError < Error; end
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
# @return [Array<Symbol, Class>] List of handlers for the command group
|
|
22
|
+
attr_reader :handlers
|
|
23
|
+
|
|
24
|
+
# @return [Boolean] Always returns true for stateless handlers
|
|
25
|
+
def stateless?
|
|
26
|
+
true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Adds a handler to the command group
|
|
30
|
+
# @param command_or_handler_method_name [Symbol, String] the name of the command class (String) or custom handler method (Symbol)
|
|
31
|
+
# @param context [Symbol, String] the context of the handler's command, camel or snake case
|
|
32
|
+
# @param subject [Symbol, String] the subject of the handler's command, camel or snake case
|
|
33
|
+
# @return [void]
|
|
34
|
+
def handler(command_or_handler_method_name, context: to_s.split('::')[0],
|
|
35
|
+
subject: to_s.split('::')[1])
|
|
36
|
+
@handlers ||= []
|
|
37
|
+
@handlers << build_handler(command_or_handler_method_name, context, subject)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# Builds a handler class or returns a symbol for the command, representing a custom handler method
|
|
43
|
+
# @param command_or_handler_name [Symbol, String] the name of the command class (String) or custom handler method (Symbol)
|
|
44
|
+
# @param context [String] the context of the handler's command
|
|
45
|
+
# @param subject [String] the subject of the handler's command
|
|
46
|
+
# @return [Class, Symbol] the handler class or a symbol representing the command
|
|
47
|
+
def build_handler(command_or_handler_name, context, subject)
|
|
48
|
+
return command_or_handler_name if command_or_handler_name.is_a?(Symbol)
|
|
49
|
+
|
|
50
|
+
Object.const_get(
|
|
51
|
+
"#{context}::#{subject}::Commands::#{command_or_handler_name}::Handler"
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @param cmd [Group] the command group to handle
|
|
57
|
+
# @param events_cache [Hash] already cached events
|
|
58
|
+
# { stream => { event_name => event_data } }
|
|
59
|
+
# @raise [InvalidCommandGroupError] if the command is not valid
|
|
60
|
+
def initialize(cmd, events_cache: {})
|
|
61
|
+
raise InvalidCommandGroupError.new(cmd.class.name.deconstantize, self.class.name.deconstantize) unless valid_command?(cmd)
|
|
62
|
+
|
|
63
|
+
@cmd = cmd
|
|
64
|
+
@events_cache = events_cache
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Executes the command group
|
|
68
|
+
# @raise [CommandsError] if any handler errors occur during execution
|
|
69
|
+
# @return [void]
|
|
70
|
+
def call
|
|
71
|
+
errors = []
|
|
72
|
+
|
|
73
|
+
PgEventstore.client.multiple do
|
|
74
|
+
errors.push(*run_command_handlers)
|
|
75
|
+
errors.push(*run_custom_handlers)
|
|
76
|
+
raise CommandsError.new(extra: errors), 'Command group failed' if errors.any?
|
|
77
|
+
|
|
78
|
+
publish_events
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
attr_reader :cmd, :events_cache
|
|
85
|
+
|
|
86
|
+
# Runs the defined command handlers for each command in the group
|
|
87
|
+
# @return [Array] updated errors array
|
|
88
|
+
def run_command_handlers
|
|
89
|
+
cmd.commands.each_with_object([]) do |command, errors|
|
|
90
|
+
handler_class(command.class).new(command, publish_events: false).call
|
|
91
|
+
rescue Handler::InvalidTransition, Handler::NoChangeTransition => e
|
|
92
|
+
errors << { command: command.class.name, error: e.message, extra: e.extra }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Runs any custom handlers defined for the command group
|
|
97
|
+
# @return [Array] updated errors array
|
|
98
|
+
def run_custom_handlers
|
|
99
|
+
custom_handlers.each_with_object([]) do |custom_handler, errors|
|
|
100
|
+
send(custom_handler)
|
|
101
|
+
rescue NoMethodError
|
|
102
|
+
raise CustomHandlerMethodMissingError, "Method #{custom_handler} not found"
|
|
103
|
+
rescue Handler::InvalidTransition, Handler::NoChangeTransition => e
|
|
104
|
+
errors << { custom_handler:, error: e.message, extra: e.extra }
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Publishes events for all commands in the group
|
|
109
|
+
# @return [void]
|
|
110
|
+
def publish_events
|
|
111
|
+
cmd.commands.each do |command|
|
|
112
|
+
handler = handler_class(command.class).new(command, revision_check: false)
|
|
113
|
+
# only run base class call method which publishes events
|
|
114
|
+
Handler.instance_method(:call).bind_call(handler)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Checks if the given command is valid for this handler
|
|
119
|
+
# @param cmd [Group] the command group to validate
|
|
120
|
+
# @return [Boolean] true if the command is valid, false otherwise
|
|
121
|
+
def valid_command?(cmd)
|
|
122
|
+
cmd.is_a?(Group) && cmd.class.name.deconstantize == self.class.name.deconstantize
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Gets the handler class for a given command class
|
|
126
|
+
# @param command_class [Class] the command class
|
|
127
|
+
# @return [Class] the handler class for the command
|
|
128
|
+
def handler_class(command_class)
|
|
129
|
+
handler = handler_for(command_class)
|
|
130
|
+
default_handlers.find { _1 == handler } || Class.new(Handler) { self.event_name = handler.event_name }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Gets the handler for a given command class
|
|
134
|
+
# @param command_class [Class] the command class
|
|
135
|
+
# @return [Class] the handler class for the command
|
|
136
|
+
def handler_for(command_class)
|
|
137
|
+
Object.const_get("#{command_class.name.deconstantize}::Handler")
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# @return [Array<Class>] list of default handlers
|
|
141
|
+
def default_handlers
|
|
142
|
+
self.class.handlers.without(custom_handlers)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# @return [Array<Symbol>] list of custom handlers
|
|
146
|
+
def custom_handlers
|
|
147
|
+
self.class.handlers.select { _1.is_a?(Symbol) }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Raises an InvalidTransition error
|
|
151
|
+
# @param message [String] the error message
|
|
152
|
+
# @param extra [Hash] additional error information
|
|
153
|
+
# @raise [Handler::InvalidTransition] always raised
|
|
154
|
+
def invalid_transition(message, extra: {})
|
|
155
|
+
raise Handler::InvalidTransition.new(extra:), message.to_s
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Raises a NoChangeTransition error
|
|
159
|
+
# @param message [String] the error message
|
|
160
|
+
# @param extra [Hash] additional error information
|
|
161
|
+
# @raise [Handler::NoChangeTransition] always raised
|
|
162
|
+
def no_change_transition(message, extra: {})
|
|
163
|
+
raise Handler::NoChangeTransition.new(extra:), message.to_s
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Returns a subject object composed based on the command group handler class
|
|
167
|
+
# @param context [String] the context of the subject
|
|
168
|
+
# @param subject [String] the subject of the command group
|
|
169
|
+
# @param aggregate_id [String] the id of the aggregate
|
|
170
|
+
# @param stream_prefix [String] the stream prefix of the subject
|
|
171
|
+
# @return [Yes::Core::Commands::Stateless::Subject] the subject object
|
|
172
|
+
def subject_data(
|
|
173
|
+
context: self.class.to_s.split('::')[0],
|
|
174
|
+
subject: self.class.to_s.split('::')[1],
|
|
175
|
+
aggregate_id: nil,
|
|
176
|
+
stream_prefix: nil
|
|
177
|
+
)
|
|
178
|
+
Yes::Core::Commands::Stateless::Subject.new(
|
|
179
|
+
context:, subject:, stream_prefix:, aggregate_id:
|
|
180
|
+
)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
module Commands
|
|
6
|
+
module Stateless
|
|
7
|
+
# Response object for stateless command groups
|
|
8
|
+
class GroupResponse < Response
|
|
9
|
+
attribute :cmd, Types.Instance(Yes::Core::Commands::Group)
|
|
10
|
+
attribute? :error, Types.Instance(Yes::Core::Commands::Stateless::GroupHandler::CommandsError).optional
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
module Commands
|
|
6
|
+
module Stateless
|
|
7
|
+
# Handles stateless commands by publishing events to the event store
|
|
8
|
+
# without maintaining aggregate state in memory.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# class MyHandler < Yes::Core::Commands::Stateless::Handler
|
|
12
|
+
# self.event_name = 'Created'
|
|
13
|
+
# end
|
|
14
|
+
class Handler
|
|
15
|
+
include OpenTelemetry::Trackable
|
|
16
|
+
include HandlerHelpers
|
|
17
|
+
|
|
18
|
+
class TransitionError < Error; end
|
|
19
|
+
class InvalidTransition < TransitionError; end
|
|
20
|
+
class NoChangeTransition < TransitionError; end
|
|
21
|
+
|
|
22
|
+
MISSING_CMDS_MSG = 'Commands missing'
|
|
23
|
+
|
|
24
|
+
module RevisionsLoader
|
|
25
|
+
attr_reader(:revisions, :subject_stream_revision)
|
|
26
|
+
|
|
27
|
+
def call
|
|
28
|
+
transaction.otl_contexts.timestamps[:command_handling_started_at_ms] = (Time.now.utc.to_f * 1000).to_i if transaction&.otl_contexts
|
|
29
|
+
|
|
30
|
+
if revision_check
|
|
31
|
+
@subject_stream_revision = expected_revision(stream) || :no_stream
|
|
32
|
+
@revisions = load_stream_revisions
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Here the business logic is checked
|
|
36
|
+
super
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# @return [Hash] { <#PgEventstore::Stream> => expected_revision, ... }
|
|
42
|
+
def load_stream_revisions
|
|
43
|
+
revisions = {}
|
|
44
|
+
|
|
45
|
+
self.class.streams&.each do |stream_attrs|
|
|
46
|
+
parts = stream_attrs[:prefix].split('::')
|
|
47
|
+
stream = PgEventstore::Stream.new(
|
|
48
|
+
context: parts[0],
|
|
49
|
+
stream_name: parts[1..].join('::'),
|
|
50
|
+
stream_id: event_payload[stream_attrs[:subject_key]]
|
|
51
|
+
)
|
|
52
|
+
revisions[stream] = expected_revision(stream)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
revisions
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.inherited(base)
|
|
60
|
+
super
|
|
61
|
+
|
|
62
|
+
base.prepend(RevisionsLoader)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
class << self
|
|
66
|
+
attr_accessor :event_name, :streams
|
|
67
|
+
|
|
68
|
+
# @return [Boolean]
|
|
69
|
+
def stateless?
|
|
70
|
+
true
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
attr_reader(:events_cache, :cmd_helper, :cmd, :revision_check, :publish_events)
|
|
75
|
+
private :events_cache, :cmd_helper, :cmd
|
|
76
|
+
|
|
77
|
+
delegate :origin, :batch_id, :transaction, to: :cmd
|
|
78
|
+
delegate :aggregate_id, :context, :subject, :locale, :event_payload, to: :cmd_helper
|
|
79
|
+
alias attributes event_payload
|
|
80
|
+
|
|
81
|
+
# @param cmd [Yes::Core::Command]
|
|
82
|
+
# @param events_cache [Hash] already cached events { stream => { event_name => event_data } }
|
|
83
|
+
# @param revision_check [Boolean] whether to check stream revisions before publishing
|
|
84
|
+
# @param publish_events [Boolean] whether to actually publish events
|
|
85
|
+
# @return [Stateless::Handler]
|
|
86
|
+
def initialize(cmd, events_cache: {}, revision_check: true, publish_events: true)
|
|
87
|
+
@cmd = cmd
|
|
88
|
+
@cmd_helper = Commands::Helper.new(cmd)
|
|
89
|
+
@revision_check = revision_check
|
|
90
|
+
@publish_events = publish_events
|
|
91
|
+
@events_cache = events_cache
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @return [void]
|
|
95
|
+
def call
|
|
96
|
+
return unless publish_events
|
|
97
|
+
|
|
98
|
+
publish_event(self.class.event_name)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Publishes a single event to the event store
|
|
102
|
+
#
|
|
103
|
+
# @param event_name [String] the name of the event to publish
|
|
104
|
+
# @return [PgEventstore::Event] the published event
|
|
105
|
+
def publish_event(event_name)
|
|
106
|
+
transaction.otl_contexts.publisher = self.class.propagate_context(service_name: true) if transaction&.otl_contexts
|
|
107
|
+
|
|
108
|
+
type = event_type(event_name)
|
|
109
|
+
event_class = events_module.const_get(event_name)
|
|
110
|
+
event = event_class.new(
|
|
111
|
+
type:,
|
|
112
|
+
data: event_payload,
|
|
113
|
+
metadata: event_metadata
|
|
114
|
+
)
|
|
115
|
+
otl_record_event_data(event)
|
|
116
|
+
verify_revisions! if revision_check
|
|
117
|
+
|
|
118
|
+
PgEventstore.client.append_to_stream(
|
|
119
|
+
stream,
|
|
120
|
+
event,
|
|
121
|
+
options: { expected_revision: subject_stream_revision }
|
|
122
|
+
).tap { otl_record_response(_1) }
|
|
123
|
+
end
|
|
124
|
+
otl_trackable :publish_event, OpenTelemetry::OtlSpan::OtlData.new(span_name: 'Publish Event', span_kind: :producer)
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
# @return [Module]
|
|
129
|
+
def events_module
|
|
130
|
+
"#{context}::#{subject}::Events".constantize
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# @param event_name [String]
|
|
134
|
+
# @return [String]
|
|
135
|
+
def event_type(event_name)
|
|
136
|
+
"#{context}::#{stream_name(subject)}#{event_name}"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# @return [PgEventstore::Stream]
|
|
140
|
+
def stream
|
|
141
|
+
PgEventstore::Stream.new(context:, stream_name: stream_name(subject), stream_id: aggregate_id)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# @param subject [String]
|
|
145
|
+
# @return [String]
|
|
146
|
+
def stream_name(subject)
|
|
147
|
+
return subject unless cmd.metadata&.dig(:edit_template_command)
|
|
148
|
+
|
|
149
|
+
"#{subject}EditTemplate"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# @param stream [PgEventstore::Stream]
|
|
153
|
+
# @return [Integer, nil]
|
|
154
|
+
def expected_revision(stream = self.stream)
|
|
155
|
+
PgEventstore.client.read(
|
|
156
|
+
stream,
|
|
157
|
+
options: { direction: 'Backwards', max_count: 1 },
|
|
158
|
+
middlewares: []
|
|
159
|
+
).first&.stream_revision || 0
|
|
160
|
+
rescue PgEventstore::StreamNotFoundError
|
|
161
|
+
nil
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# @return [Hash]
|
|
165
|
+
def event_metadata
|
|
166
|
+
metadata = { origin:, batch_id: }
|
|
167
|
+
metadata.merge!(cmd.metadata || {})
|
|
168
|
+
metadata.merge!(transaction.for_eventstore_metadata) if transaction
|
|
169
|
+
metadata.deep_transform_keys(&:to_s)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# @param message [String]
|
|
173
|
+
# @param extra [Hash]
|
|
174
|
+
# @return [InvalidTransition]
|
|
175
|
+
def invalid_transition(message, extra: {})
|
|
176
|
+
raise InvalidTransition.new(extra:), message.to_s
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# @param message [String]
|
|
180
|
+
# @param extra [Hash]
|
|
181
|
+
# @return [NoChangeTransition]
|
|
182
|
+
def no_change_transition(message, extra: {})
|
|
183
|
+
raise NoChangeTransition.new(extra:), message.to_s
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# @return [void]
|
|
187
|
+
# @raise [PgEventstore::WrongExpectedRevisionError]
|
|
188
|
+
def verify_revisions!
|
|
189
|
+
revisions.each do |stream, revision|
|
|
190
|
+
expected = expected_revision(stream)
|
|
191
|
+
next if revision == expected
|
|
192
|
+
|
|
193
|
+
revision_error!(revision || -1, expected || -1, stream)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# @param revision [Integer]
|
|
198
|
+
# @param expected_revision [Integer]
|
|
199
|
+
# @param stream [PgEventstore::Stream]
|
|
200
|
+
def revision_error!(revision, expected_revision, stream)
|
|
201
|
+
PgEventstore::WrongExpectedRevisionError.new(revision:, expected_revision:, stream:).tap do |error|
|
|
202
|
+
self.class.current_span&.status = ::OpenTelemetry::Trace::Status.error('Wrong expected revision')
|
|
203
|
+
self.class.current_span&.add_attributes(
|
|
204
|
+
{
|
|
205
|
+
current_revision: revision,
|
|
206
|
+
expected_revision: expected_revision,
|
|
207
|
+
stream: stream.to_json
|
|
208
|
+
}.stringify_keys
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
raise error
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# @param context [String]
|
|
216
|
+
# @param subject [String]
|
|
217
|
+
# @param aggregate_id [String]
|
|
218
|
+
# @param stream_prefix [String, nil]
|
|
219
|
+
# @return [Yes::Core::Commands::Stateless::Subject]
|
|
220
|
+
def subject_data(
|
|
221
|
+
context: self.class.to_s.split('::')[0],
|
|
222
|
+
subject: self.class.to_s.split('::')[1],
|
|
223
|
+
aggregate_id: self.aggregate_id,
|
|
224
|
+
stream_prefix: nil
|
|
225
|
+
)
|
|
226
|
+
Yes::Core::Commands::Stateless::Subject.new(context:, subject:, stream_prefix:, aggregate_id:)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# @param command [String]
|
|
230
|
+
# @param context [String]
|
|
231
|
+
# @param subject [String]
|
|
232
|
+
# @return [Data]
|
|
233
|
+
def command_data(
|
|
234
|
+
command:,
|
|
235
|
+
context: self.class.to_s.split('::')[0],
|
|
236
|
+
subject: self.class.to_s.split('::')[1]
|
|
237
|
+
)
|
|
238
|
+
Data.define(:context, :subject, :command).new(context:, subject:, command:)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# @return [Yes::Core::Stateless::MissingCommandsAggregator]
|
|
242
|
+
def result_aggregator
|
|
243
|
+
@result_aggregator ||= Yes::Core::Stateless::MissingCommandsAggregator.new
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# @param event [PgEventstore::Event]
|
|
247
|
+
# @return [void]
|
|
248
|
+
def otl_record_event_data(event)
|
|
249
|
+
self.class.current_span&.add_attributes(
|
|
250
|
+
{
|
|
251
|
+
'event.type' => event.type,
|
|
252
|
+
'event.data' => event.data.to_json,
|
|
253
|
+
'event.metadata' => event.metadata.to_json
|
|
254
|
+
}
|
|
255
|
+
)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# @param result [PgEventstore::Event]
|
|
259
|
+
# @return [void]
|
|
260
|
+
def otl_record_response(result)
|
|
261
|
+
if ENV['STATSD_ADDR'].present? && defined?(StatsD)
|
|
262
|
+
StatsD.increment(
|
|
263
|
+
'events_processing_total',
|
|
264
|
+
tags: {
|
|
265
|
+
service: Rails.application.class.module_parent.name,
|
|
266
|
+
source: "#{Rails.application.class.module_parent.name}-#{result.type}",
|
|
267
|
+
target: "#{Rails.application.class.module_parent.name}-#{result.type}",
|
|
268
|
+
type: 'producer',
|
|
269
|
+
event: result.type
|
|
270
|
+
}
|
|
271
|
+
)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
self.class.current_span&.status = ::OpenTelemetry::Trace::Status.ok
|
|
275
|
+
self.class.current_span&.add_event(
|
|
276
|
+
'Event Published to PgEventstore',
|
|
277
|
+
timestamp: result.created_at,
|
|
278
|
+
attributes: {
|
|
279
|
+
'event.type' => result.type,
|
|
280
|
+
'event.link_id' => result.link_id || '',
|
|
281
|
+
'global_position' => result.global_position,
|
|
282
|
+
'stream' => result.stream.to_json,
|
|
283
|
+
'stream.revision' => result.stream_revision,
|
|
284
|
+
'timestamp_ms' => (result.created_at.to_f * 1000).to_i
|
|
285
|
+
}
|
|
286
|
+
)
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|