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.
Files changed (128) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +69 -0
  5. data/lib/yes/core/active_job_serializers/command_group_serializer.rb +29 -0
  6. data/lib/yes/core/active_job_serializers/dry_struct_serializer.rb +57 -0
  7. data/lib/yes/core/aggregate/draftable.rb +205 -0
  8. data/lib/yes/core/aggregate/dsl/attribute_data.rb +37 -0
  9. data/lib/yes/core/aggregate/dsl/attribute_definer.rb +54 -0
  10. data/lib/yes/core/aggregate/dsl/attribute_definers/aggregate.rb +36 -0
  11. data/lib/yes/core/aggregate/dsl/attribute_definers/standard.rb +36 -0
  12. data/lib/yes/core/aggregate/dsl/class_name_convention.rb +80 -0
  13. data/lib/yes/core/aggregate/dsl/class_resolvers/authorizer.rb +132 -0
  14. data/lib/yes/core/aggregate/dsl/class_resolvers/base.rb +80 -0
  15. data/lib/yes/core/aggregate/dsl/class_resolvers/command/authorizer.rb +30 -0
  16. data/lib/yes/core/aggregate/dsl/class_resolvers/command/authorizer_factory.rb +34 -0
  17. data/lib/yes/core/aggregate/dsl/class_resolvers/command/base.rb +38 -0
  18. data/lib/yes/core/aggregate/dsl/class_resolvers/command/cerbos_authorizer.rb +114 -0
  19. data/lib/yes/core/aggregate/dsl/class_resolvers/command/command.rb +70 -0
  20. data/lib/yes/core/aggregate/dsl/class_resolvers/command/event.rb +88 -0
  21. data/lib/yes/core/aggregate/dsl/class_resolvers/command/guard_evaluator.rb +84 -0
  22. data/lib/yes/core/aggregate/dsl/class_resolvers/command/simple_authorizer.rb +50 -0
  23. data/lib/yes/core/aggregate/dsl/class_resolvers/command/state_updater.rb +46 -0
  24. data/lib/yes/core/aggregate/dsl/class_resolvers/read_model.rb +75 -0
  25. data/lib/yes/core/aggregate/dsl/class_resolvers/read_model_filter.rb +88 -0
  26. data/lib/yes/core/aggregate/dsl/class_resolvers/read_model_serializer.rb +76 -0
  27. data/lib/yes/core/aggregate/dsl/command_data.rb +54 -0
  28. data/lib/yes/core/aggregate/dsl/command_definer.rb +263 -0
  29. data/lib/yes/core/aggregate/dsl/command_shortcut_expander.rb +233 -0
  30. data/lib/yes/core/aggregate/dsl/constant_resolver.rb +67 -0
  31. data/lib/yes/core/aggregate/dsl/method_definers/attribute/accessor.rb +28 -0
  32. data/lib/yes/core/aggregate/dsl/method_definers/attribute/aggregate_accessor.rb +36 -0
  33. data/lib/yes/core/aggregate/dsl/method_definers/attribute/base.rb +42 -0
  34. data/lib/yes/core/aggregate/dsl/method_definers/command/base.rb +42 -0
  35. data/lib/yes/core/aggregate/dsl/method_definers/command/can_command.rb +41 -0
  36. data/lib/yes/core/aggregate/dsl/method_definers/command/command.rb +50 -0
  37. data/lib/yes/core/aggregate/has_authorizer.rb +86 -0
  38. data/lib/yes/core/aggregate/has_read_model.rb +169 -0
  39. data/lib/yes/core/aggregate/read_model_rebuilder.rb +40 -0
  40. data/lib/yes/core/aggregate/shared_read_model_rebuilder.rb +158 -0
  41. data/lib/yes/core/aggregate.rb +404 -0
  42. data/lib/yes/core/authentication_error.rb +8 -0
  43. data/lib/yes/core/authorization/cerbos_client_provider.rb +27 -0
  44. data/lib/yes/core/authorization/command_authorizer.rb +40 -0
  45. data/lib/yes/core/authorization/command_cerbos_authorizer.rb +182 -0
  46. data/lib/yes/core/authorization/read_model_authorizer.rb +22 -0
  47. data/lib/yes/core/authorization/read_models_authorizer.rb +49 -0
  48. data/lib/yes/core/authorization/read_request_authorizer.rb +32 -0
  49. data/lib/yes/core/authorization/read_request_cerbos_authorizer.rb +112 -0
  50. data/lib/yes/core/command.rb +35 -0
  51. data/lib/yes/core/command_handling/aggregate_tracker.rb +33 -0
  52. data/lib/yes/core/command_handling/command_executor.rb +171 -0
  53. data/lib/yes/core/command_handling/command_handler.rb +124 -0
  54. data/lib/yes/core/command_handling/event_publisher.rb +189 -0
  55. data/lib/yes/core/command_handling/guard_evaluator.rb +165 -0
  56. data/lib/yes/core/command_handling/guard_runner.rb +76 -0
  57. data/lib/yes/core/command_handling/payload_proxy.rb +159 -0
  58. data/lib/yes/core/command_handling/read_model_recovery_service.rb +264 -0
  59. data/lib/yes/core/command_handling/read_model_revision_guard.rb +198 -0
  60. data/lib/yes/core/command_handling/read_model_updater.rb +103 -0
  61. data/lib/yes/core/command_handling/state_updater.rb +113 -0
  62. data/lib/yes/core/commands/bus.rb +46 -0
  63. data/lib/yes/core/commands/group.rb +135 -0
  64. data/lib/yes/core/commands/group_response.rb +13 -0
  65. data/lib/yes/core/commands/helper.rb +126 -0
  66. data/lib/yes/core/commands/notifier.rb +65 -0
  67. data/lib/yes/core/commands/processor.rb +137 -0
  68. data/lib/yes/core/commands/response.rb +63 -0
  69. data/lib/yes/core/commands/stateless/group_handler.rb +186 -0
  70. data/lib/yes/core/commands/stateless/group_response.rb +15 -0
  71. data/lib/yes/core/commands/stateless/handler.rb +292 -0
  72. data/lib/yes/core/commands/stateless/handler_helpers.rb +321 -0
  73. data/lib/yes/core/commands/stateless/response.rb +14 -0
  74. data/lib/yes/core/commands/stateless/subject.rb +41 -0
  75. data/lib/yes/core/commands/validator.rb +28 -0
  76. data/lib/yes/core/configuration.rb +432 -0
  77. data/lib/yes/core/data_decryptor.rb +59 -0
  78. data/lib/yes/core/data_encryptor.rb +60 -0
  79. data/lib/yes/core/encryption_metadata.rb +33 -0
  80. data/lib/yes/core/error.rb +14 -0
  81. data/lib/yes/core/error_messages.rb +37 -0
  82. data/lib/yes/core/event.rb +222 -0
  83. data/lib/yes/core/event_class_resolver.rb +40 -0
  84. data/lib/yes/core/generators/read_models/add_pending_update_tracking_generator.rb +43 -0
  85. data/lib/yes/core/generators/read_models/templates/add_pending_update_tracking.rb.erb +122 -0
  86. data/lib/yes/core/generators/read_models/templates/migration.rb.erb +9 -0
  87. data/lib/yes/core/generators/read_models/update_generator.rb +147 -0
  88. data/lib/yes/core/jobs/read_model_recovery_job.rb +219 -0
  89. data/lib/yes/core/middlewares/encryptor.rb +48 -0
  90. data/lib/yes/core/middlewares/timestamp.rb +29 -0
  91. data/lib/yes/core/middlewares/with_indifferent_access.rb +22 -0
  92. data/lib/yes/core/models/application_record.rb +9 -0
  93. data/lib/yes/core/open_telemetry/otl_span.rb +110 -0
  94. data/lib/yes/core/open_telemetry/trackable.rb +101 -0
  95. data/lib/yes/core/payload_store/base.rb +33 -0
  96. data/lib/yes/core/payload_store/errors.rb +13 -0
  97. data/lib/yes/core/payload_store/lookup.rb +44 -0
  98. data/lib/yes/core/process_managers/access_token_client.rb +107 -0
  99. data/lib/yes/core/process_managers/base.rb +40 -0
  100. data/lib/yes/core/process_managers/command_runner.rb +109 -0
  101. data/lib/yes/core/process_managers/service_client.rb +57 -0
  102. data/lib/yes/core/process_managers/state.rb +118 -0
  103. data/lib/yes/core/railtie.rb +58 -0
  104. data/lib/yes/core/read_model/builder.rb +267 -0
  105. data/lib/yes/core/read_model/event_handler.rb +64 -0
  106. data/lib/yes/core/read_model/filter.rb +118 -0
  107. data/lib/yes/core/read_model/filter_query_builder.rb +104 -0
  108. data/lib/yes/core/serializer.rb +21 -0
  109. data/lib/yes/core/subscriptions.rb +94 -0
  110. data/lib/yes/core/test_support/event_helpers.rb +27 -0
  111. data/lib/yes/core/test_support/jwt_helpers.rb +30 -0
  112. data/lib/yes/core/test_support/subscriptions_helper.rb +88 -0
  113. data/lib/yes/core/test_support/test_helper.rb +27 -0
  114. data/lib/yes/core/test_support.rb +5 -0
  115. data/lib/yes/core/transaction_details.rb +90 -0
  116. data/lib/yes/core/type_lookup.rb +88 -0
  117. data/lib/yes/core/types.rb +110 -0
  118. data/lib/yes/core/utils/aggregate_shortcuts.rb +164 -0
  119. data/lib/yes/core/utils/caller_utils.rb +37 -0
  120. data/lib/yes/core/utils/command_utils.rb +226 -0
  121. data/lib/yes/core/utils/error_notifier.rb +101 -0
  122. data/lib/yes/core/utils/event_name_resolver.rb +67 -0
  123. data/lib/yes/core/utils/exponential_retrier.rb +180 -0
  124. data/lib/yes/core/utils/hash_utils.rb +63 -0
  125. data/lib/yes/core/version.rb +7 -0
  126. data/lib/yes/core.rb +85 -0
  127. data/lib/yes.rb +0 -0
  128. 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