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,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Core
5
+ module ProcessManagers
6
+ # Client for obtaining access tokens using client credentials.
7
+ #
8
+ # @example
9
+ # client = Yes::Core::ProcessManagers::AccessTokenClient.new
10
+ # access_token = client.call(client_id: 'id', client_secret: 'secret')
11
+ class AccessTokenClient
12
+ # Custom error class for AccessTokenClient failures.
13
+ class Error < Yes::Core::ProcessManagers::Base::Error; end
14
+
15
+ # Holds structured response data from the token request.
16
+ Response = Data.define(:response, :request_body, :request_url, :body, :parsed_body, :status_code)
17
+
18
+ # @return [String] the authentication service URL
19
+ AUTH_URL = ENV.fetch('AUTH_URL', 'http://auth-cluster-ip-service:3000/v1')
20
+
21
+ # @return [String] the OAuth2 grant type
22
+ GRANT_TYPE = 'client_credentials'
23
+
24
+ # @return [Faraday::Connection] the HTTP connection
25
+ attr_reader :connection
26
+ private :connection
27
+
28
+ # Initializes the AccessTokenClient with a Faraday connection.
29
+ def initialize
30
+ @connection = Faraday.new(AUTH_URL) do |f|
31
+ f.request :json
32
+ end
33
+ end
34
+
35
+ # Requests an access token using client credentials.
36
+ #
37
+ # @param client_id [String] the client ID
38
+ # @param client_secret [String] the client secret
39
+ # @return [String] the access token
40
+ # @raise [Error] if the access token cannot be obtained
41
+ def call(client_id:, client_secret:)
42
+ payload = { client_id:, client_secret:, grant_type: GRANT_TYPE }
43
+
44
+ response = perform_request(payload)
45
+ access_token_error!(response) unless response.response.success?
46
+
47
+ return response.parsed_body['access_token'] if response.parsed_body&.dig('access_token')
48
+
49
+ access_token_error!(response)
50
+ end
51
+
52
+ private
53
+
54
+ # Performs the HTTP request to obtain the access token.
55
+ #
56
+ # @param payload [Hash] the request payload
57
+ # @return [Response] the response object
58
+ def perform_request(payload)
59
+ response = connection.post do |req|
60
+ req.url 'oauth/token'
61
+ req.body = payload
62
+ end
63
+ Response.new(
64
+ response:,
65
+ request_url: response.env.url.to_s,
66
+ request_body: payload,
67
+ body: response.body,
68
+ parsed_body: parse_response(response.body),
69
+ status_code: response.status
70
+ )
71
+ end
72
+
73
+ # Parses the JSON response body.
74
+ #
75
+ # @param body [String] the response body
76
+ # @return [Hash, nil] the parsed JSON or nil if parsing fails
77
+ def parse_response(body)
78
+ JSON.parse(body)
79
+ rescue JSON::ParserError
80
+ nil
81
+ end
82
+
83
+ # Raises an error with details about the failed access token request.
84
+ #
85
+ # @param response [Response] the response object
86
+ # @raise [Error] with details about the failed request
87
+ def access_token_error!(response)
88
+ msg = 'Access Token Client: failed to get access token'
89
+
90
+ extra = {
91
+ request: {
92
+ url: response.request_url,
93
+ body: response.request_body.merge(client_secret: '[FILTERED]')
94
+ },
95
+ response: {
96
+ body: response.parsed_body || response.body,
97
+ status: response.status_code
98
+ }
99
+ }
100
+
101
+ Rails.logger.error("#{msg} extra: #{extra}")
102
+ raise Error.new(msg, extra:)
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Core
5
+ module ProcessManagers
6
+ # Base class for process managers.
7
+ #
8
+ # @abstract Subclass and override {#call} to implement a process manager.
9
+ #
10
+ # @example
11
+ # class MyProcessManager < Yes::Core::ProcessManagers::Base
12
+ # def call(event)
13
+ # # handle event
14
+ # end
15
+ # end
16
+ class Base
17
+ # Error class for process manager failures, with optional extra context.
18
+ class Error < StandardError
19
+ # @return [Hash] additional error context
20
+ attr_accessor :extra
21
+
22
+ # @param msg [String] the error message
23
+ # @param extra [Hash] additional error information
24
+ def initialize(msg, extra: {})
25
+ @extra = extra
26
+ super(msg)
27
+ end
28
+ end
29
+
30
+ # Handles an event. Must be implemented by subclasses.
31
+ #
32
+ # @param _event [Object] the event to handle
33
+ # @raise [NotImplementedError] if not overridden
34
+ def call(_event)
35
+ raise NotImplementedError
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Core
5
+ module ProcessManagers
6
+ # Publishes commands to a command API client with automatic access token retrieval.
7
+ #
8
+ # @abstract Subclass and override {#call} to implement command publishing logic.
9
+ #
10
+ # @example
11
+ # class MyCommandRunner < Yes::Core::ProcessManagers::CommandRunner
12
+ # def call(event)
13
+ # publish(
14
+ # client_id: ENV['CLIENT_ID'],
15
+ # client_secret: ENV['CLIENT_SECRET'],
16
+ # commands_data: [{ context: 'Foo', aggregate: 'Bar', command: 'create', params: {} }]
17
+ # )
18
+ # end
19
+ # end
20
+ class CommandRunner < Base
21
+ # @return [AccessTokenClient] client for retrieving access tokens
22
+ attr_reader :access_token_client
23
+
24
+ # @return [ServiceClient] client for sending commands to the API
25
+ attr_reader :command_api_client
26
+
27
+ # @return [Logger] logger instance for error logging
28
+ attr_reader :logger
29
+
30
+ private :access_token_client, :command_api_client, :logger
31
+
32
+ # Initializes a new CommandRunner instance.
33
+ #
34
+ # @param command_api_client [ServiceClient, nil] the API client to use for sending commands
35
+ def initialize(command_api_client: nil)
36
+ super()
37
+
38
+ @command_api_client = command_api_client
39
+ @access_token_client = AccessTokenClient.new
40
+ @logger = Rails.logger
41
+ end
42
+
43
+ private
44
+
45
+ # Publishes commands to the API.
46
+ #
47
+ # @param client_id [String] the client ID for authentication
48
+ # @param client_secret [String] the client secret for authentication
49
+ # @param commands_data [Array<Hash>] the commands to be published
50
+ # @param custom_command_api_client [ServiceClient, nil] optional custom API client
51
+ # @return [Faraday::Response] the API response
52
+ # @raise [ArgumentError] if no API client is available
53
+ # @raise [Yes::Core::ProcessManagers::Base::Error] if the API request fails
54
+ def publish(client_id:, client_secret:, commands_data:, custom_command_api_client: nil)
55
+ access_token = access_token_client.call(client_id:, client_secret:)
56
+ api_client = custom_command_api_client || command_api_client
57
+
58
+ if api_client.nil?
59
+ raise ArgumentError, 'No API client available. Ensure custom_command_api_client is provided ' \
60
+ 'or command_api_client is initialized.'
61
+ end
62
+
63
+ response = api_client.call(access_token:, commands_data:, channel:)
64
+ process_manager_error!(response) unless response.success?
65
+
66
+ response
67
+ end
68
+
69
+ # Generates the channel name based on the class name.
70
+ #
71
+ # @return [String] the channel name
72
+ # @example
73
+ # "/process_managers/do_something_manager"
74
+ def channel
75
+ "/#{self.class.name.split('::').first(2).flatten.join('/').underscore}"
76
+ end
77
+
78
+ # Processes and logs errors from the API response.
79
+ #
80
+ # @param response [Faraday::Response] the API response
81
+ # @param error_msg [String, nil] additional error message
82
+ # @raise [Yes::Core::ProcessManagers::Base::Error] with detailed error information
83
+ def process_manager_error!(response, error_msg: nil)
84
+ msg = 'Process Manager: failed to send commands'
85
+
86
+ extra = {
87
+ error_msg:,
88
+ request: {
89
+ url: response.env.url.to_s,
90
+ body: JSON.parse(response.env.request_body),
91
+ token: response.env.request_headers['Authorization']
92
+ },
93
+ response: {
94
+ body: begin
95
+ JSON.parse(response.body)
96
+ rescue StandardError
97
+ response.body
98
+ end,
99
+ status: response.status
100
+ }
101
+ }
102
+
103
+ logger.error("#{msg} extra: #{extra}")
104
+ raise Yes::Core::ProcessManagers::Base::Error.new(msg, extra:)
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Core
5
+ module ProcessManagers
6
+ # Handles communication with external command API services.
7
+ #
8
+ # @example
9
+ # client = Yes::Core::ProcessManagers::ServiceClient.new('my_service')
10
+ # client.call(access_token: token, commands_data: [...], channel: '/pm/channel')
11
+ class ServiceClient
12
+ # @return [String] the URL of the service to communicate with
13
+ attr_reader :service_url
14
+ private :service_url
15
+
16
+ # Initializes a new ServiceClient.
17
+ #
18
+ # @param service [String] the name of the service to connect to
19
+ def initialize(service)
20
+ @service_url = ENV.fetch(
21
+ "#{service.upcase}_SERVICE_URL",
22
+ "http://#{service.underscore.tr('_', '-')}-cluster-ip-service:3000"
23
+ )
24
+ end
25
+
26
+ # Sends commands to the service.
27
+ #
28
+ # @param access_token [String] JWT token for authentication
29
+ # @param commands_data [Array<Hash>] array of command data to be sent
30
+ # @param channel [String] the channel to send command notifications to
31
+ # @return [Faraday::Response] the response from the service
32
+ # @raise [ArgumentError] if access_token or channel is nil
33
+ def call(access_token: nil, commands_data: [], channel: nil)
34
+ raise ArgumentError, 'channel and access_token is required' if access_token.nil? || channel.nil?
35
+
36
+ connection(access_token).post do |req|
37
+ req.url '/v1/commands'
38
+ req.body = { channel:, commands: commands_data }
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ # Creates a Faraday connection to the service.
45
+ #
46
+ # @param access_token [String] JWT token for authentication
47
+ # @return [Faraday::Connection] the configured Faraday connection
48
+ def connection(access_token)
49
+ Faraday.new(service_url) do |f|
50
+ f.request :json
51
+ f.request :authorization, 'Bearer', access_token
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Core
5
+ module ProcessManagers
6
+ # Represents the state of a subject loaded in a process manager by replaying events.
7
+ #
8
+ # @abstract Subclass and override {#stream}, {#required_attributes}, and implement apply_* methods.
9
+ #
10
+ # @example
11
+ # class UserState < Yes::Core::ProcessManagers::State
12
+ # RELEVANT_EVENTS = %w[UserCreated UserUpdated].freeze
13
+ #
14
+ # attr_reader :name, :email
15
+ #
16
+ # private
17
+ #
18
+ # def stream
19
+ # PgEventstore::Stream.new(context: 'Users', stream_name: 'User', id: id)
20
+ # end
21
+ #
22
+ # def required_attributes
23
+ # %i[name email]
24
+ # end
25
+ #
26
+ # def apply_user_created(event)
27
+ # @name = event.data['name']
28
+ # @email = event.data['email']
29
+ # end
30
+ # end
31
+ class State
32
+ # Loads the state for a given ID.
33
+ #
34
+ # @param id [String, Integer] the id of the subject to be loaded
35
+ # @return [State] a new instance with events applied
36
+ def self.load(id)
37
+ new(id).tap(&:load)
38
+ end
39
+
40
+ # @param id [String, Integer] the id of the subject to be loaded
41
+ def initialize(id)
42
+ @id = id
43
+ end
44
+
45
+ # Loads the state from relevant events.
46
+ #
47
+ # @return [void]
48
+ def load
49
+ events = relevant_events(stream)
50
+ return unless events
51
+
52
+ process_events(events)
53
+ end
54
+
55
+ # Checks if the state is valid (all required attributes are present).
56
+ #
57
+ # @return [Boolean] true if all required attributes are present
58
+ def valid?
59
+ required_attributes.all? { |attr| send(attr).present? }
60
+ end
61
+
62
+ private
63
+
64
+ # @return [String, Integer] the subject ID
65
+ attr_reader :id
66
+
67
+ # Defines the event stream this state is loaded from.
68
+ #
69
+ # @abstract Must be implemented by subclasses.
70
+ # @raise [NotImplementedError] if not implemented in subclass
71
+ # @return [PgEventstore::Stream] the event stream
72
+ def stream
73
+ raise NotImplementedError, "#{self.class} must implement #stream"
74
+ end
75
+
76
+ # Processes the relevant events and applies them to the state.
77
+ #
78
+ # @param events [Hash] a hash of event types and their corresponding events
79
+ # @raise [NotImplementedError] if an apply_* method is not implemented for an event type
80
+ # @return [void]
81
+ def process_events(events)
82
+ events.each do |event_type, event|
83
+ event_name = event_type.split('::').last
84
+ method_name = "apply_#{event_name.underscore}"
85
+
86
+ raise NotImplementedError, "#{self.class} must implement ##{method_name}" unless respond_to?(method_name, true)
87
+
88
+ send(method_name, event)
89
+ end
90
+ end
91
+
92
+ # Defines the required attributes for this state.
93
+ #
94
+ # @abstract Must be implemented by subclasses.
95
+ # @raise [NotImplementedError] if not implemented in subclass
96
+ # @return [Array<Symbol>] list of required attribute names
97
+ def required_attributes
98
+ raise NotImplementedError, "#{self.class} must implement #required_attributes"
99
+ end
100
+
101
+ # Retrieves relevant events for the given stream.
102
+ #
103
+ # @param stream [PgEventstore::Stream] the event stream to read from
104
+ # @return [Hash, nil] a hash of relevant events, or nil if no events are found
105
+ def relevant_events(stream)
106
+ options = { direction: 'Backwards', filter: { event_types: self.class::RELEVANT_EVENTS } }
107
+ PgEventstore.client.read_paginated(stream, options:).each_with_object({}) do |events, result|
108
+ events.each do |event|
109
+ result[event.type] ||= event
110
+
111
+ return result if (self.class::RELEVANT_EVENTS - result.keys).empty?
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Core
5
+ class Railtie < Rails::Railtie
6
+ config.active_job.custom_serializers << Yes::Core::ActiveJobSerializers::DryStructSerializer
7
+ config.active_job.custom_serializers << Yes::Core::ActiveJobSerializers::CommandGroupSerializer
8
+
9
+ # Runs before any initializers are run
10
+ config.before_configuration do
11
+ PgEventstore.configure do |config|
12
+ config.subscription_pull_interval = 0.5
13
+ config.event_class_resolver = Yes::Core::EventClassResolver.new
14
+ # Order of middlewares is important, :with_indifferent_access must come first
15
+ config.middlewares = {
16
+ with_indifferent_access: Yes::Core::Middlewares::WithIndifferentAccess.new,
17
+ timestamp: Yes::Core::Middlewares::Timestamp.new
18
+ }
19
+ end
20
+ end
21
+
22
+ config.after_initialize do |app|
23
+ # Find all aggregates and register their public read models
24
+ Rails.root.glob('app/contexts/**/**/aggregate.rb').each do |file|
25
+ require file
26
+ context, aggregate = file.to_s.split('contexts/').last.split('/')
27
+ klass = "#{context.camelize}::#{aggregate.camelize}::Aggregate".constantize
28
+
29
+ if klass.read_model_public?
30
+ app.config.yes_read_api.read_models ||= []
31
+ app.config.yes_read_api.read_models << klass.read_model_name.pluralize
32
+ end
33
+
34
+ # Also register the template read model if the aggregate is draftable and the changes read model is public
35
+ if klass.draftable? && klass.changes_read_model_public?
36
+ app.config.yes_read_api.read_models ||= []
37
+ app.config.yes_read_api.read_models << klass.changes_read_model_name.pluralize
38
+ end
39
+ end
40
+ end
41
+
42
+ initializer 'yes-core.config' do |_app|
43
+ unless Rails.env.test?
44
+ Yes::Core.configure do |config|
45
+ config.logger ||= Rails.logger
46
+ end
47
+ end
48
+
49
+ PgEventstore.logger ||= Rails.logger if ENV['PG_ES_LOGGING'] == 'true'
50
+ end
51
+
52
+ # Load aggregate shortcuts when Rails console starts
53
+ console do
54
+ Yes::Core::Utils::AggregateShortcuts.load!
55
+ end
56
+ end
57
+ end
58
+ end