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,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
|