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,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
class Aggregate
|
|
6
|
+
module Dsl
|
|
7
|
+
module ClassResolvers
|
|
8
|
+
# Creates and registers authorizer classes for aggregates based on
|
|
9
|
+
# Yes::Core::Authorization::CommandCerbosAuthorizer.
|
|
10
|
+
#
|
|
11
|
+
# This class resolver generates authorizer classes associated with aggregates.
|
|
12
|
+
# Each authorizer class defines a RESOURCE constant containing the
|
|
13
|
+
# associated read model class and resource name used for authorization checks.
|
|
14
|
+
#
|
|
15
|
+
# @example Generated authorizer class structure
|
|
16
|
+
# # Generated for a 'User' aggregate
|
|
17
|
+
# class GeneratedAuthorizer < Yes::Core::Authorization::CommandCerbosAuthorizer
|
|
18
|
+
# RESOURCE = { read_model: Auth::Resources::User, name: 'user' }.freeze
|
|
19
|
+
# end
|
|
20
|
+
class Authorizer < Base
|
|
21
|
+
# Initializes a new authorizer class resolver
|
|
22
|
+
#
|
|
23
|
+
# @param options [Yes::Core::Aggregate::HasAuthorizer::AuthorizerOptions] Data object containing:
|
|
24
|
+
# - authorizer_base_class [Class] The authorizer class to use.
|
|
25
|
+
# - context [String] The name of the context (e.g., 'CompanyManagement')
|
|
26
|
+
# - aggregate [String] The name of the aggregate (e.g., 'Company')
|
|
27
|
+
# - read_model_class [Class, nil] Optional read model class. Defaults to
|
|
28
|
+
# `Auth::Resources::<AggregateName>`.
|
|
29
|
+
# - resource_name [String, nil] Optional resource name for Cerbos. Defaults to
|
|
30
|
+
# aggregate name underscored (e.g., 'company').
|
|
31
|
+
# - authorizer_block [Proc, nil] An optional block defining the custom logic for the `call` method
|
|
32
|
+
# if `authorizer_base_class` is not `Yes::Core::Authorization::CommandCerbosAuthorizer`.
|
|
33
|
+
def initialize(options)
|
|
34
|
+
@resource_name = options.resource_name
|
|
35
|
+
@read_model_class = options.read_model_class
|
|
36
|
+
@authorizer_class = options.authorizer_base_class
|
|
37
|
+
@custom_call_logic = options.authorizer_block
|
|
38
|
+
@draftable = options.draftable
|
|
39
|
+
@changes_read_model_class = options.changes_read_model_class
|
|
40
|
+
# Base class expects context_name and aggregate_name parameters
|
|
41
|
+
super(options.context, options.aggregate)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Creates and registers the authorizer class in the Yes::Core configuration
|
|
45
|
+
#
|
|
46
|
+
# @return [Class] The found or generated authorizer class that was registered
|
|
47
|
+
def call
|
|
48
|
+
Yes::Core.configuration.register_aggregate_authorizer_class(
|
|
49
|
+
context_name,
|
|
50
|
+
aggregate_name,
|
|
51
|
+
find_or_generate_class
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
# @!attribute [r] resource_name
|
|
58
|
+
# @return [String] The name of the resource used for authorization.
|
|
59
|
+
# @!attribute [r] read_model_class
|
|
60
|
+
# @return [Class] The read model class associated with the resource.
|
|
61
|
+
# @!attribute [r] custom_call_logic
|
|
62
|
+
# @return [Proc, nil] The custom block provided for the call method logic.
|
|
63
|
+
# @!attribute [r] changes_read_model_class
|
|
64
|
+
# @return [Class, nil] The changes read model class for draftable aggregates.
|
|
65
|
+
attr_reader :resource_name, :read_model_class, :authorizer_class, :custom_call_logic, :changes_read_model_class
|
|
66
|
+
|
|
67
|
+
# @return [Symbol] Returns :authorizer as the class type
|
|
68
|
+
def class_type
|
|
69
|
+
:authorizer
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Checks if the aggregate is draftable
|
|
73
|
+
#
|
|
74
|
+
# @return [Boolean] true if the aggregate is draftable, false otherwise
|
|
75
|
+
def draftable?
|
|
76
|
+
@draftable == true
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Generates a new authorizer class dynamically.
|
|
80
|
+
# The generated class inherits from the specified authorizer_class.
|
|
81
|
+
# If the base class is Yes::Core::Authorization::CommandCerbosAuthorizer, it sets the RESOURCE constant.
|
|
82
|
+
# Otherwise, if a block was provided during initialization, it defines a `call` method
|
|
83
|
+
# executing that block's logic.
|
|
84
|
+
#
|
|
85
|
+
# @raise [ArgumentError] If the base class is not CommandCerbosAuthorizer and no block was provided.
|
|
86
|
+
# @return [Class] A new authorizer class.
|
|
87
|
+
def generate_class
|
|
88
|
+
klass = Class.new(authorizer_class)
|
|
89
|
+
|
|
90
|
+
if authorizer_class == Yes::Core::Authorization::CommandCerbosAuthorizer
|
|
91
|
+
resource_attributes = { read_model: read_model_class, name: resource_name }
|
|
92
|
+
resource_attributes[:draft_read_model] = changes_read_model_class if draftable?
|
|
93
|
+
klass.const_set(:RESOURCE, resource_attributes.freeze)
|
|
94
|
+
else
|
|
95
|
+
define_custom_call_logic(klass)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
klass
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Defines custom call logic for an authorizer class when not using CommandCerbosAuthorizer
|
|
102
|
+
#
|
|
103
|
+
# @param klass [Class] The dynamically generated authorizer class
|
|
104
|
+
# @raise [ArgumentError] If no custom_call_logic block was provided during initialization
|
|
105
|
+
# @return [void]
|
|
106
|
+
def define_custom_call_logic(klass)
|
|
107
|
+
_custom_logic = custom_call_logic
|
|
108
|
+
unless _custom_logic
|
|
109
|
+
raise ArgumentError,
|
|
110
|
+
"A block must be provided to define the 'call' method logic when not using CommandCerbosAuthorizer."
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Define call as a class method (matching CommandCerbosAuthorizer pattern)
|
|
114
|
+
klass.define_singleton_method(:call) do |current_command, current_auth_data|
|
|
115
|
+
@_exec_command = current_command
|
|
116
|
+
@_exec_auth_data = current_auth_data
|
|
117
|
+
define_singleton_method(:command) { @_exec_command }
|
|
118
|
+
define_singleton_method(:auth_data) { @_exec_auth_data }
|
|
119
|
+
begin
|
|
120
|
+
instance_exec(&_custom_logic)
|
|
121
|
+
ensure
|
|
122
|
+
@_exec_command = nil
|
|
123
|
+
@_exec_auth_data = nil
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
class Aggregate
|
|
6
|
+
module Dsl
|
|
7
|
+
module ClassResolvers
|
|
8
|
+
# Base class for creating and registering aggregate-related classes
|
|
9
|
+
#
|
|
10
|
+
# This class provides the foundation for resolving and generating various
|
|
11
|
+
# aggregate-related classes such as commands, events, and handlers.
|
|
12
|
+
# It handles class registration and provides a template for class generation.
|
|
13
|
+
#
|
|
14
|
+
# @abstract Subclass and override {#class_type}, {#class_name}, and {#generate_class}
|
|
15
|
+
# to implement specific class resolver behavior
|
|
16
|
+
class Base
|
|
17
|
+
# Creates and registers the class in the Yes::Core configuration
|
|
18
|
+
#
|
|
19
|
+
# @return [Class] The found or generated class that was registered
|
|
20
|
+
def call
|
|
21
|
+
Yes::Core.configuration.register_aggregate_class(
|
|
22
|
+
context_name,
|
|
23
|
+
aggregate_name,
|
|
24
|
+
class_name,
|
|
25
|
+
class_type,
|
|
26
|
+
find_or_generate_class
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# @return [ConstantResolver] The resolver for finding and setting constants
|
|
33
|
+
# @return [ClassNameConvention] The convention for generating class names
|
|
34
|
+
# @return [String] The name of the context
|
|
35
|
+
# @return [String] The name of the aggregate
|
|
36
|
+
attr_reader :constant_resolver, :class_name_convention, :context_name, :aggregate_name
|
|
37
|
+
|
|
38
|
+
# @param context_name [String] The name of the context
|
|
39
|
+
# @param aggregate_name [String] The name of the aggregate
|
|
40
|
+
def initialize(context_name, aggregate_name)
|
|
41
|
+
@context_name = context_name
|
|
42
|
+
@aggregate_name = aggregate_name
|
|
43
|
+
|
|
44
|
+
@class_name_convention = ClassNameConvention.new(
|
|
45
|
+
context: context_name,
|
|
46
|
+
aggregate: aggregate_name
|
|
47
|
+
)
|
|
48
|
+
@constant_resolver = ConstantResolver.new(class_name_convention)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Finds an existing class or generates a new one based on conventions
|
|
52
|
+
#
|
|
53
|
+
# @return [Class] The found or generated class
|
|
54
|
+
def find_or_generate_class
|
|
55
|
+
constant_resolver.find_conventional_class(class_type, class_name) ||
|
|
56
|
+
constant_resolver.set_constant_for(class_type, class_name, generate_class)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @abstract
|
|
60
|
+
# @return [Symbol] The type of class being resolved (e.g., :command, :event, :handler)
|
|
61
|
+
def class_type
|
|
62
|
+
raise NotImplementedError, "#{self.class} must implement #class_type"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @return [String] The name of the class to be generated
|
|
66
|
+
def class_name
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @abstract
|
|
71
|
+
# @return [Class] The generated class
|
|
72
|
+
def generate_class
|
|
73
|
+
raise NotImplementedError, "#{self.class} must implement #generate_class"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
class Aggregate
|
|
6
|
+
module Dsl
|
|
7
|
+
module ClassResolvers
|
|
8
|
+
module Command
|
|
9
|
+
# Base class for command authorizers
|
|
10
|
+
class Authorizer < Base
|
|
11
|
+
# Common methods for all authorizers
|
|
12
|
+
def class_type
|
|
13
|
+
:authorizer
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def class_name
|
|
17
|
+
command_data.name
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# This should be overridden by subclasses
|
|
21
|
+
def generate_class
|
|
22
|
+
raise NotImplementedError, "#{self.class} must implement #generate_class"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
class Aggregate
|
|
6
|
+
module Dsl
|
|
7
|
+
module ClassResolvers
|
|
8
|
+
module Command
|
|
9
|
+
# Factory class for creating appropriate command authorizer resolver
|
|
10
|
+
# based on the aggregate-level authorizer type
|
|
11
|
+
class AuthorizerFactory
|
|
12
|
+
# Creates and returns the appropriate authorizer resolver
|
|
13
|
+
# based on the aggregate's authorizer class type
|
|
14
|
+
#
|
|
15
|
+
# @param command_data [CommandData] Command data with aggregate information
|
|
16
|
+
# @return [SimpleAuthorizer, CerbosAuthorizer, nil] Appropriate authorizer or nil
|
|
17
|
+
def self.create(command_data)
|
|
18
|
+
# Only create command authorizers if there's an aggregate-level authorizer
|
|
19
|
+
aggregate_authorizer_class = command_data.aggregate_class.authorizer_class
|
|
20
|
+
return nil unless aggregate_authorizer_class
|
|
21
|
+
|
|
22
|
+
if aggregate_authorizer_class <= Yes::Core::Authorization::CommandCerbosAuthorizer
|
|
23
|
+
CerbosAuthorizer.new(command_data)
|
|
24
|
+
else
|
|
25
|
+
SimpleAuthorizer.new(command_data)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
class Aggregate
|
|
6
|
+
module Dsl
|
|
7
|
+
module ClassResolvers
|
|
8
|
+
module Command
|
|
9
|
+
# Base class for command-related class resolvers
|
|
10
|
+
#
|
|
11
|
+
# This class extends the base resolver functionality to handle
|
|
12
|
+
# command-specific class generation. It provides a foundation for
|
|
13
|
+
# resolvers that need to work with commands, such as command classes
|
|
14
|
+
# and their associated events.
|
|
15
|
+
#
|
|
16
|
+
# @abstract Subclass and implement the required methods from {ClassResolvers::Base}
|
|
17
|
+
class Base < ClassResolvers::Base
|
|
18
|
+
# Initializes a new command-based class resolver
|
|
19
|
+
#
|
|
20
|
+
# @param command_data [Yes::Core::Aggregate::Dsl::CommandData] The command data instance
|
|
21
|
+
# containing metadata about the command being processed
|
|
22
|
+
def initialize(command_data)
|
|
23
|
+
@command_data = command_data
|
|
24
|
+
|
|
25
|
+
super(command_data.context_name, command_data.aggregate_name)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# @return [Yes::Core::Aggregate::Dsl::CommandData] The command data instance
|
|
31
|
+
attr_reader :command_data
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
class Aggregate
|
|
6
|
+
module Dsl
|
|
7
|
+
module ClassResolvers
|
|
8
|
+
module Command
|
|
9
|
+
# Command resolver for Cerbos-based authorizers
|
|
10
|
+
class CerbosAuthorizer < Authorizer
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# Generates a Cerbos authorizer class with RESOURCE constant
|
|
14
|
+
# and optional DSL methods for resource_attributes and cerbos_payload
|
|
15
|
+
#
|
|
16
|
+
# @return [Class] The generated authorizer class
|
|
17
|
+
def generate_class
|
|
18
|
+
klass = Class.new(command_data.aggregate_class.authorizer_class)
|
|
19
|
+
|
|
20
|
+
# Apply Cerbos-specific overrides if we have a block
|
|
21
|
+
apply_cerbos_overrides(klass) if command_data.authorizer_block
|
|
22
|
+
|
|
23
|
+
klass
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Applies Cerbos-specific DSL overrides to allow customization
|
|
27
|
+
# of resource_attributes and cerbos_payload methods
|
|
28
|
+
#
|
|
29
|
+
# @param klass [Class] the class being generated
|
|
30
|
+
# rubocop:disable Metrics/MethodLength
|
|
31
|
+
def apply_cerbos_overrides(klass)
|
|
32
|
+
# Store the block for later use
|
|
33
|
+
user_block = command_data.authorizer_block
|
|
34
|
+
|
|
35
|
+
# Define instance variables and accessor methods for the DSL
|
|
36
|
+
klass.class_eval do
|
|
37
|
+
# Resource attributes storage
|
|
38
|
+
class << self
|
|
39
|
+
attr_accessor :_resource_attributes_block, :_cerbos_payload_block
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Define accessor methods for the DSL
|
|
43
|
+
define_method(:command) { @_command }
|
|
44
|
+
define_method(:resource) { @_resource }
|
|
45
|
+
define_method(:auth_data) { @_auth_data }
|
|
46
|
+
|
|
47
|
+
# Define resource_attributes method that will be called by external code
|
|
48
|
+
define_singleton_method(:resource_attributes) do |resource = nil, command = nil, &block|
|
|
49
|
+
if block
|
|
50
|
+
# Store the block for later use
|
|
51
|
+
self._resource_attributes_block = block
|
|
52
|
+
elsif _resource_attributes_block
|
|
53
|
+
# Execute the previously stored block
|
|
54
|
+
instance = new
|
|
55
|
+
instance.instance_variable_set(:@_command, command)
|
|
56
|
+
instance.instance_variable_set(:@_resource, resource)
|
|
57
|
+
|
|
58
|
+
begin
|
|
59
|
+
instance.instance_eval(&_resource_attributes_block)
|
|
60
|
+
ensure
|
|
61
|
+
instance.remove_instance_variable(:@_command) if instance.instance_variable_defined?(:@_command)
|
|
62
|
+
instance.remove_instance_variable(:@_resource) if instance.instance_variable_defined?(:@_resource)
|
|
63
|
+
end
|
|
64
|
+
elsif defined?(super)
|
|
65
|
+
# No block defined, call the original method if available
|
|
66
|
+
super(resource, command)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Define cerbos_payload method that will be called by external code
|
|
71
|
+
define_singleton_method(:cerbos_payload) do |command = nil, resource = nil, auth = nil, &block|
|
|
72
|
+
if block
|
|
73
|
+
# Store the block for later use
|
|
74
|
+
self._cerbos_payload_block = block
|
|
75
|
+
elsif _cerbos_payload_block
|
|
76
|
+
# Execute the previously stored block
|
|
77
|
+
instance = new
|
|
78
|
+
instance.instance_variable_set(:@_command, command)
|
|
79
|
+
instance.instance_variable_set(:@_resource, resource)
|
|
80
|
+
instance.instance_variable_set(:@_auth_data, auth)
|
|
81
|
+
|
|
82
|
+
begin
|
|
83
|
+
instance.instance_eval(&_cerbos_payload_block)
|
|
84
|
+
ensure
|
|
85
|
+
instance.remove_instance_variable(:@_command) if instance.instance_variable_defined?(:@_command)
|
|
86
|
+
instance.remove_instance_variable(:@_resource) if instance.instance_variable_defined?(:@_resource)
|
|
87
|
+
instance.remove_instance_variable(:@_auth_data) if instance.instance_variable_defined?(:@_auth_data)
|
|
88
|
+
end
|
|
89
|
+
elsif defined?(super)
|
|
90
|
+
# No block defined, call the original method if available
|
|
91
|
+
super(command, resource, auth)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# We need the resource_attributes and cerbos_payload to be callable from class context
|
|
97
|
+
# in the user block, but also store blocks for later execution
|
|
98
|
+
klass.instance_eval do
|
|
99
|
+
# Initial setup
|
|
100
|
+
self._resource_attributes_block = nil
|
|
101
|
+
self._cerbos_payload_block = nil
|
|
102
|
+
|
|
103
|
+
# Execute the user block to define overrides
|
|
104
|
+
instance_eval(&user_block)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
# rubocop:enable Metrics/MethodLength
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
class Aggregate
|
|
6
|
+
module Dsl
|
|
7
|
+
module ClassResolvers
|
|
8
|
+
module Command
|
|
9
|
+
# Resolves or generates a command class for a command
|
|
10
|
+
class Command < Base
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# @return [Symbol] The type of class being resolved
|
|
14
|
+
def class_type
|
|
15
|
+
:command
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @return [String] The name of the class to be generated
|
|
19
|
+
def class_name
|
|
20
|
+
command_data.name
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Generates a command class
|
|
24
|
+
#
|
|
25
|
+
# @return [Class] The generated command class
|
|
26
|
+
def generate_class
|
|
27
|
+
context = context_name
|
|
28
|
+
aggregate = aggregate_name
|
|
29
|
+
payload_attributes = command_data.payload_attributes || {}
|
|
30
|
+
|
|
31
|
+
Class.new(Yes::Core::Command) do
|
|
32
|
+
# Define the aggregate_id attribute for the command
|
|
33
|
+
attribute :"#{aggregate.underscore}_id", Yes::Core::Types::UUID
|
|
34
|
+
|
|
35
|
+
# Define payload attributes if any
|
|
36
|
+
payload_attributes.each do |attr_name, attr_type|
|
|
37
|
+
# Handle simple type (not a hash)
|
|
38
|
+
unless attr_type.is_a?(Hash)
|
|
39
|
+
attribute attr_name, Yes::Core::TypeLookup.type_for(attr_type, context)
|
|
40
|
+
next
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
resolved_type = Yes::Core::TypeLookup.type_for(attr_type[:type], context)
|
|
44
|
+
|
|
45
|
+
case [attr_type[:optional] == true, attr_type[:nullable] == true]
|
|
46
|
+
when [false, false]
|
|
47
|
+
# required key, non-nullable value
|
|
48
|
+
attribute attr_name, resolved_type
|
|
49
|
+
when [false, true]
|
|
50
|
+
# required key, nullable value
|
|
51
|
+
attribute attr_name, resolved_type.optional
|
|
52
|
+
when [true, false]
|
|
53
|
+
# optional key, non-nullable value
|
|
54
|
+
attribute? attr_name, resolved_type
|
|
55
|
+
when [true, true]
|
|
56
|
+
# optional key, nullable value
|
|
57
|
+
attribute? attr_name, resolved_type.optional
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
alias_method :aggregate_id, :"#{aggregate.underscore}_id"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
class Aggregate
|
|
6
|
+
module Dsl
|
|
7
|
+
module ClassResolvers
|
|
8
|
+
module Command
|
|
9
|
+
# Resolves or generates an event class for a command
|
|
10
|
+
class Event < Base
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# @return [Symbol] The type of class being resolved
|
|
14
|
+
def class_type
|
|
15
|
+
:event
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @return [String] The name of the class to be generated
|
|
19
|
+
def class_name
|
|
20
|
+
command_data.event_name
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Generates an event class
|
|
24
|
+
#
|
|
25
|
+
# @return [Class] The generated event class
|
|
26
|
+
def generate_class
|
|
27
|
+
aggregate = aggregate_name
|
|
28
|
+
context = context_name
|
|
29
|
+
payload_attributes = command_data.payload_attributes || {}
|
|
30
|
+
encrypted_attributes = command_data.encrypted_attributes || []
|
|
31
|
+
|
|
32
|
+
Class.new(Yes::Core::Event) do
|
|
33
|
+
define_method :schema do
|
|
34
|
+
Dry::Schema.Params do
|
|
35
|
+
# Helper to build attribute definition with support for optional key and nullable value
|
|
36
|
+
build_attribute = proc do |attr_name, attr_type_config|
|
|
37
|
+
# Handle simple type (not a hash)
|
|
38
|
+
unless attr_type_config.is_a?(Hash)
|
|
39
|
+
required(attr_name).value(Yes::Core::TypeLookup.type_for(attr_type_config, context, :event))
|
|
40
|
+
next
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
resolved_type = Yes::Core::TypeLookup.type_for(attr_type_config[:type], context, :event)
|
|
44
|
+
|
|
45
|
+
case [attr_type_config[:optional] == true, attr_type_config[:nullable] == true]
|
|
46
|
+
when [false, false]
|
|
47
|
+
# required key, non-nullable value
|
|
48
|
+
required(attr_name).value(resolved_type)
|
|
49
|
+
when [false, true]
|
|
50
|
+
# required key, nullable value
|
|
51
|
+
required(attr_name).maybe(resolved_type)
|
|
52
|
+
when [true, false]
|
|
53
|
+
# optional key, non-nullable value
|
|
54
|
+
optional(attr_name).value(resolved_type)
|
|
55
|
+
when [true, true]
|
|
56
|
+
# optional key, nullable value
|
|
57
|
+
optional(attr_name).maybe(resolved_type)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Define the aggregate_id attribute for the event
|
|
62
|
+
required(:"#{aggregate.underscore}_id").value(Yes::Core::Types::UUID)
|
|
63
|
+
|
|
64
|
+
# Define payload attributes if any
|
|
65
|
+
payload_attributes.each do |attr_name, attr_type|
|
|
66
|
+
build_attribute.call(attr_name, attr_type)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Add encryption_schema class method if there are encrypted attributes
|
|
72
|
+
if encrypted_attributes.any?
|
|
73
|
+
define_singleton_method :encryption_schema do
|
|
74
|
+
{
|
|
75
|
+
key: ->(data) { data[:"#{aggregate.underscore}_id"] },
|
|
76
|
+
attributes: encrypted_attributes
|
|
77
|
+
}
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
class Aggregate
|
|
6
|
+
module Dsl
|
|
7
|
+
module ClassResolvers
|
|
8
|
+
module Command
|
|
9
|
+
# Creates and registers guard evaluator classes for aggregate commands
|
|
10
|
+
#
|
|
11
|
+
# This class resolver generates plain guard evaluator classes that process
|
|
12
|
+
# commands in aggregates.
|
|
13
|
+
#
|
|
14
|
+
class GuardEvaluator < Base
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
# Returns the class type symbol for the guard evaluator
|
|
18
|
+
#
|
|
19
|
+
# @return [Symbol] Returns :guard_evaluator as the class type
|
|
20
|
+
# @api private
|
|
21
|
+
def class_type
|
|
22
|
+
:guard_evaluator
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Returns the name for the guard evaluator class
|
|
26
|
+
#
|
|
27
|
+
# @return [String] The name of the guard evaluator class derived from the attribute
|
|
28
|
+
# @api private
|
|
29
|
+
def class_name
|
|
30
|
+
command_data.name
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Creates a new guard evaluator class
|
|
34
|
+
#
|
|
35
|
+
# By default, includes a no_change guard that checks if the command would modify the aggregate's state.
|
|
36
|
+
# This guard is omitted if the command has a custom state_update block since we cannot rely on the default
|
|
37
|
+
# attribute update logic.
|
|
38
|
+
#
|
|
39
|
+
# @return [Class] A new guard evaluator class inheriting from Yes::Core::CommandHandling::GuardEvaluator
|
|
40
|
+
# @api private
|
|
41
|
+
def generate_class
|
|
42
|
+
command_name = command_data.name
|
|
43
|
+
context_name = command_data.context_name
|
|
44
|
+
aggregate_name = command_data.aggregate_name
|
|
45
|
+
|
|
46
|
+
# Add the no_change guard to the command data's guard names
|
|
47
|
+
# This will be added by the GuardEvaluator unless there's an update_state block
|
|
48
|
+
# or if it was already added by a shortcut expansion
|
|
49
|
+
command_data.add_guard(:no_change) unless command_data.guard_names.include?(:no_change) || command_data.update_state_block
|
|
50
|
+
|
|
51
|
+
Class.new(Yes::Core::CommandHandling::GuardEvaluator) do
|
|
52
|
+
guard :no_change do
|
|
53
|
+
state_updater_class = Yes::Core.configuration.aggregate_class(
|
|
54
|
+
context_name, aggregate_name, command_name, :state_updater
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
next true if state_updater_class.update_state_block
|
|
58
|
+
|
|
59
|
+
payload = raw_payload.except(:"#{aggregate_name.underscore}_id")
|
|
60
|
+
|
|
61
|
+
next true if payload.empty?
|
|
62
|
+
|
|
63
|
+
has_changes = false
|
|
64
|
+
I18n.with_locale(payload.delete(:locale) || I18n.locale) do
|
|
65
|
+
payload.each do |attribute, new_value|
|
|
66
|
+
current_value = public_send(attribute)
|
|
67
|
+
if current_value != new_value
|
|
68
|
+
has_changes = true
|
|
69
|
+
break
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
has_changes
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|