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,233 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
class Aggregate
|
|
6
|
+
module Dsl
|
|
7
|
+
#
|
|
8
|
+
# Class which converts attributes passed to Yes::Core::Aggregate::command in shorthand
|
|
9
|
+
# form to list of attributes and commands to define
|
|
10
|
+
#
|
|
11
|
+
class CommandShortcutExpander
|
|
12
|
+
ExpandedCommandShortcut = Data.define(:attributes, :commands)
|
|
13
|
+
CommandSpecification = Data.define(:name, :block)
|
|
14
|
+
AttributeSpecification = Data.define(:name, :type, :options)
|
|
15
|
+
class InvalidShortcut < Yes::Core::Error; end
|
|
16
|
+
|
|
17
|
+
SPECIAL_CASE_NAMES = {
|
|
18
|
+
change: :change,
|
|
19
|
+
enable: :enable,
|
|
20
|
+
activate: :enable,
|
|
21
|
+
publish: :publish
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
#
|
|
26
|
+
# Detects basic usage of command invocation, so the aggregate knows it's not a shortcut
|
|
27
|
+
#
|
|
28
|
+
# @param [Array<Object>] *args
|
|
29
|
+
# @param [Hash<Object, Object>] **kwargs
|
|
30
|
+
# @param [Proc] &block
|
|
31
|
+
#
|
|
32
|
+
# @return [Boolean]
|
|
33
|
+
#
|
|
34
|
+
def base_case?(*args, **kwargs, &block)
|
|
35
|
+
args.size == 1 && kwargs.empty? && block.present?
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
attr_reader :args, :kwargs, :block
|
|
40
|
+
|
|
41
|
+
#
|
|
42
|
+
# Takes all parameters passed to command invocation
|
|
43
|
+
#
|
|
44
|
+
# @param [Array<Object>] *args
|
|
45
|
+
# @param [Hash<Object, Object>] **kwargs
|
|
46
|
+
# @param [Proc] &block
|
|
47
|
+
#
|
|
48
|
+
def initialize(*args, **kwargs, &block)
|
|
49
|
+
@args = args
|
|
50
|
+
@kwargs = kwargs
|
|
51
|
+
@block = block
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
#
|
|
55
|
+
# Expands shortcut to Data object with list of attributes and commands
|
|
56
|
+
#
|
|
57
|
+
# @raise [InvalidShortcut] if shortcut is not recognized
|
|
58
|
+
# @return [ExpandedCommandShortcut] list of attributes and commands to
|
|
59
|
+
# generate
|
|
60
|
+
#
|
|
61
|
+
def call
|
|
62
|
+
case args
|
|
63
|
+
in [[Symbol, Symbol], Symbol]
|
|
64
|
+
handle_toggle_commands
|
|
65
|
+
in [Symbol => name, *]
|
|
66
|
+
raise InvalidShortcut unless SPECIAL_CASE_NAMES.include?(name)
|
|
67
|
+
|
|
68
|
+
send(:"handle_#{SPECIAL_CASE_NAMES[name]}_command")
|
|
69
|
+
else
|
|
70
|
+
raise InvalidShortcut
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def handle_toggle_commands
|
|
77
|
+
raise InvalidShortcut if block.present?
|
|
78
|
+
|
|
79
|
+
command_subject = args.second
|
|
80
|
+
attribute_name = kwargs[:attribute].presence || command_subject
|
|
81
|
+
|
|
82
|
+
set_flag_command, unset_flag_command = args.first
|
|
83
|
+
|
|
84
|
+
attributes =
|
|
85
|
+
if kwargs[:attribute] == false
|
|
86
|
+
[]
|
|
87
|
+
else
|
|
88
|
+
[
|
|
89
|
+
AttributeSpecification.new(
|
|
90
|
+
name: attribute_name,
|
|
91
|
+
type: :boolean,
|
|
92
|
+
options: {}
|
|
93
|
+
)
|
|
94
|
+
]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
commands = [
|
|
98
|
+
CommandSpecification.new(
|
|
99
|
+
name: :"#{set_flag_command}_#{command_subject}",
|
|
100
|
+
block: proc do
|
|
101
|
+
guard(:no_change) { !send(attribute_name) }
|
|
102
|
+
update_state do
|
|
103
|
+
send(attribute_name) { true }
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
),
|
|
107
|
+
CommandSpecification.new(
|
|
108
|
+
name: :"#{unset_flag_command}_#{command_subject}",
|
|
109
|
+
block: proc do
|
|
110
|
+
guard(:no_change) { send(attribute_name) }
|
|
111
|
+
update_state do
|
|
112
|
+
send(attribute_name) { false }
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
)
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
ExpandedCommandShortcut.new(attributes:, commands:)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def handle_change_command
|
|
122
|
+
raise InvalidShortcut if args.size > 3
|
|
123
|
+
|
|
124
|
+
command_subject = args[1]
|
|
125
|
+
attribute_name = kwargs[:attribute].presence || command_subject
|
|
126
|
+
attribute_type = args[2].presence || :string
|
|
127
|
+
localized = kwargs[:localized] || false
|
|
128
|
+
should_encrypt = kwargs[:encrypt] || false
|
|
129
|
+
additional_block = block # needs to be captured in a local variable to ensure proper closure in this scope
|
|
130
|
+
|
|
131
|
+
payload_options = { attribute_name => attribute_type }
|
|
132
|
+
payload_options[:locale] = :locale if localized
|
|
133
|
+
|
|
134
|
+
attributes =
|
|
135
|
+
if kwargs[:attribute] == false
|
|
136
|
+
[]
|
|
137
|
+
else
|
|
138
|
+
[
|
|
139
|
+
AttributeSpecification.new(
|
|
140
|
+
name: attribute_name,
|
|
141
|
+
type: attribute_type,
|
|
142
|
+
options: { localized: }
|
|
143
|
+
)
|
|
144
|
+
]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
commands = [
|
|
148
|
+
CommandSpecification.new(
|
|
149
|
+
name: :"change_#{command_subject}",
|
|
150
|
+
block: proc do
|
|
151
|
+
guard(:no_change) { value_changed?(send(attribute_name), payload.send(attribute_name)) }
|
|
152
|
+
payload(**payload_options)
|
|
153
|
+
encrypt(attribute_name) if should_encrypt
|
|
154
|
+
instance_eval(&additional_block) if additional_block.present?
|
|
155
|
+
end
|
|
156
|
+
)
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
ExpandedCommandShortcut.new(attributes:, commands:)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def handle_enable_command
|
|
163
|
+
raise InvalidShortcut if args.size > 2
|
|
164
|
+
|
|
165
|
+
command_verb = args.first
|
|
166
|
+
command_subject = args.second
|
|
167
|
+
attribute_name = kwargs[:attribute].presence || command_subject
|
|
168
|
+
additional_block = block
|
|
169
|
+
|
|
170
|
+
attributes =
|
|
171
|
+
if kwargs[:attribute] == false
|
|
172
|
+
[]
|
|
173
|
+
else
|
|
174
|
+
[
|
|
175
|
+
AttributeSpecification.new(
|
|
176
|
+
name: attribute_name,
|
|
177
|
+
type: :boolean,
|
|
178
|
+
options: {}
|
|
179
|
+
)
|
|
180
|
+
]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
commands = [
|
|
184
|
+
CommandSpecification.new(
|
|
185
|
+
name: :"#{command_verb}_#{command_subject}",
|
|
186
|
+
block: proc do
|
|
187
|
+
guard(:no_change) { !send(attribute_name) }
|
|
188
|
+
update_state do
|
|
189
|
+
send(attribute_name) { true }
|
|
190
|
+
end
|
|
191
|
+
instance_eval(&additional_block) if additional_block.present?
|
|
192
|
+
end
|
|
193
|
+
)
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
ExpandedCommandShortcut.new(attributes:, commands:)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def handle_publish_command
|
|
200
|
+
raise InvalidShortcut if args.size > 1 || block.present?
|
|
201
|
+
|
|
202
|
+
attribute_name = kwargs[:attribute].presence || :published
|
|
203
|
+
|
|
204
|
+
attributes =
|
|
205
|
+
if kwargs[:attribute] == false
|
|
206
|
+
[]
|
|
207
|
+
else
|
|
208
|
+
[
|
|
209
|
+
AttributeSpecification.new(
|
|
210
|
+
name: attribute_name,
|
|
211
|
+
type: :boolean,
|
|
212
|
+
options: {}
|
|
213
|
+
)
|
|
214
|
+
]
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
commands = [
|
|
218
|
+
CommandSpecification.new(
|
|
219
|
+
name: :publish,
|
|
220
|
+
block: proc do
|
|
221
|
+
guard(:no_change) { !send(attribute_name) }
|
|
222
|
+
update_state { send(attribute_name) { true } }
|
|
223
|
+
end
|
|
224
|
+
)
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
ExpandedCommandShortcut.new(attributes:, commands:)
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
class Aggregate
|
|
6
|
+
module Dsl
|
|
7
|
+
# Handles finding and setting constants based on conventional naming
|
|
8
|
+
#
|
|
9
|
+
# @api private
|
|
10
|
+
class ConstantResolver
|
|
11
|
+
# @param class_name_convention [ClassNameConvention] Convention for generating class names
|
|
12
|
+
def initialize(class_name_convention)
|
|
13
|
+
@class_name_convention = class_name_convention
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Attempts to find a class based on conventional naming
|
|
17
|
+
#
|
|
18
|
+
# @param type [Symbol] The type of class to find (:command, :event, or :handler)
|
|
19
|
+
# @param name [Symbol] The name of the class
|
|
20
|
+
# @return [Class, nil] The found class or nil if not found
|
|
21
|
+
def find_conventional_class(type, name)
|
|
22
|
+
class_name = class_name_convention.class_name_for(type, name)
|
|
23
|
+
class_name.constantize
|
|
24
|
+
rescue NameError
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Sets the generated class as a constant in the appropriate module path
|
|
29
|
+
#
|
|
30
|
+
# @param type [Symbol] The type of class (:command, :event, or :handler, ...)
|
|
31
|
+
# @param name [Symbol] The name for the class
|
|
32
|
+
# @param klass [Class] The class to set as constant
|
|
33
|
+
# @return [Class] The set class
|
|
34
|
+
def set_constant_for(type, name, klass)
|
|
35
|
+
class_name = class_name_convention.class_name_for(type, name)
|
|
36
|
+
modules = class_name.split('::')
|
|
37
|
+
class_name = modules.pop
|
|
38
|
+
|
|
39
|
+
parent_module = create_module_hierarchy(modules)
|
|
40
|
+
parent_module.const_set(class_name, klass)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
attr_reader :class_name_convention
|
|
46
|
+
|
|
47
|
+
def create_module_hierarchy(modules)
|
|
48
|
+
# Start with the root namespace if the path is absolute (starts with ::)
|
|
49
|
+
base = if modules.first && modules.first.empty?
|
|
50
|
+
Object.tap { modules.shift }
|
|
51
|
+
else
|
|
52
|
+
Object
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
modules.inject(base) do |mod, module_name|
|
|
56
|
+
if mod.const_defined?(module_name, false)
|
|
57
|
+
mod.const_get(module_name, false)
|
|
58
|
+
else
|
|
59
|
+
mod.const_set(module_name, Module.new)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
class Aggregate
|
|
6
|
+
module Dsl
|
|
7
|
+
module MethodDefiners
|
|
8
|
+
module Attribute
|
|
9
|
+
# Defines the accessor method for an attribute
|
|
10
|
+
class Accessor < Base
|
|
11
|
+
# Defines a reader method for the attribute that reads from the read model
|
|
12
|
+
# @return [void]
|
|
13
|
+
def call
|
|
14
|
+
name = @name
|
|
15
|
+
|
|
16
|
+
aggregate_class.define_method(name) do
|
|
17
|
+
return nil unless self.class.read_model_enabled?
|
|
18
|
+
|
|
19
|
+
read_model.public_send(name)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
class Aggregate
|
|
6
|
+
module Dsl
|
|
7
|
+
module MethodDefiners
|
|
8
|
+
module Attribute
|
|
9
|
+
# Defines the accessor methods for an aggregate attribute
|
|
10
|
+
class AggregateAccessor < Base
|
|
11
|
+
# Defines reader methods for the aggregate attribute that reads from the read model
|
|
12
|
+
# @return [void]
|
|
13
|
+
def call
|
|
14
|
+
name = @name
|
|
15
|
+
id_name = :"#{name}_id"
|
|
16
|
+
|
|
17
|
+
# Define the id accessor
|
|
18
|
+
aggregate_class.define_method(id_name) do
|
|
19
|
+
read_model.public_send(id_name)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Define the aggregate accessor
|
|
23
|
+
aggregate_class.define_method(name) do
|
|
24
|
+
id = read_model.public_send(id_name)
|
|
25
|
+
return nil unless id
|
|
26
|
+
|
|
27
|
+
"#{self.class.context}::#{name.to_s.camelize}::Aggregate".constantize.new(id)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
class Aggregate
|
|
6
|
+
module Dsl
|
|
7
|
+
module MethodDefiners
|
|
8
|
+
module Attribute
|
|
9
|
+
# Base class for attribute method definers that provides common functionality
|
|
10
|
+
# for defining attribute-related methods on aggregate classes.
|
|
11
|
+
#
|
|
12
|
+
# @abstract Subclass and override {#call} to implement custom attribute method definition
|
|
13
|
+
class Base
|
|
14
|
+
# Initializes a new attribute method definer
|
|
15
|
+
#
|
|
16
|
+
# @param attribute_data [Object] Object containing attribute configuration
|
|
17
|
+
# @option attribute_data [Symbol] :name The attribute name
|
|
18
|
+
# @option attribute_data [Class] :aggregate_class The target aggregate class where methods will be defined
|
|
19
|
+
def initialize(attribute_data)
|
|
20
|
+
@name = attribute_data.name
|
|
21
|
+
@aggregate_class = attribute_data.aggregate_class
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Defines the attribute-related methods on the aggregate class
|
|
25
|
+
#
|
|
26
|
+
# @abstract
|
|
27
|
+
# @raise [NotImplementedError] when called on the base class
|
|
28
|
+
# @return [void]
|
|
29
|
+
def call
|
|
30
|
+
raise NotImplementedError, "#{self.class} must implement #call"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
attr_reader :name, :aggregate_class
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
class Aggregate
|
|
6
|
+
module Dsl
|
|
7
|
+
module MethodDefiners
|
|
8
|
+
module Command
|
|
9
|
+
# Base class for command method definers that provides common functionality
|
|
10
|
+
# for defining command-related methods on aggregate classes.
|
|
11
|
+
#
|
|
12
|
+
# @abstract Subclass and override {#call} to implement custom command method definition
|
|
13
|
+
class Base
|
|
14
|
+
# Initializes a new command method definer
|
|
15
|
+
#
|
|
16
|
+
# @param command_data [Object] Object containing command configuration
|
|
17
|
+
# @option command_data [Symbol] :name The command name
|
|
18
|
+
# @option command_data [Class] :aggregate_class The target aggregate class where methods will be defined
|
|
19
|
+
def initialize(command_data)
|
|
20
|
+
@name = command_data.name
|
|
21
|
+
@aggregate_class = command_data.aggregate_class
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Defines the command-related methods on the aggregate class
|
|
25
|
+
#
|
|
26
|
+
# @abstract
|
|
27
|
+
# @raise [NotImplementedError] when called on the base class
|
|
28
|
+
# @return [void]
|
|
29
|
+
def call
|
|
30
|
+
raise NotImplementedError, "#{self.class} must implement #call"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
attr_reader :name, :aggregate_class
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
class Aggregate
|
|
6
|
+
module Dsl
|
|
7
|
+
module MethodDefiners
|
|
8
|
+
module Command
|
|
9
|
+
# Defines a can_<command_name>? method on the aggregate class
|
|
10
|
+
class CanCommand < Base
|
|
11
|
+
# @return [void]
|
|
12
|
+
def call
|
|
13
|
+
can_change_method = :"can_#{name}?"
|
|
14
|
+
error_method = :"#{name}_error"
|
|
15
|
+
|
|
16
|
+
aggregate_class.attr_accessor error_method
|
|
17
|
+
command_name = @name
|
|
18
|
+
|
|
19
|
+
aggregate_class.define_method(can_change_method) do |payload = {}|
|
|
20
|
+
payload = command_utilities.prepare_default_payload(command_name, payload, self.class)
|
|
21
|
+
payload = command_utilities.prepare_command_payload(command_name, payload, self.class)
|
|
22
|
+
payload = command_utilities.prepare_assign_command_payload(command_name, payload)
|
|
23
|
+
cmd = command_utilities.build_command(command_name, payload)
|
|
24
|
+
guard_evaluator_class = command_utilities.fetch_guard_evaluator_class(command_name)
|
|
25
|
+
|
|
26
|
+
Yes::Core::CommandHandling::GuardRunner.new(self).call(
|
|
27
|
+
cmd, command_name, guard_evaluator_class, skip_guards: false
|
|
28
|
+
).present?
|
|
29
|
+
rescue Yes::Core::CommandHandling::GuardEvaluator::InvalidTransition,
|
|
30
|
+
Yes::Core::CommandHandling::GuardEvaluator::NoChangeTransition,
|
|
31
|
+
Yes::Core::Command::Invalid
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
class Aggregate
|
|
6
|
+
module Dsl
|
|
7
|
+
module MethodDefiners
|
|
8
|
+
module Command
|
|
9
|
+
# Defines a command method on the aggregate class
|
|
10
|
+
class Command < Base
|
|
11
|
+
# @return [void]
|
|
12
|
+
def call
|
|
13
|
+
command_name = @name
|
|
14
|
+
|
|
15
|
+
aggregate_class.define_method(command_name) do |payload = nil, **options|
|
|
16
|
+
payload = payload.clone if payload.is_a?(Hash)
|
|
17
|
+
|
|
18
|
+
# Extract and remove guards option from options, default to true
|
|
19
|
+
guards = options.delete(:guards)
|
|
20
|
+
guards = true if guards.nil?
|
|
21
|
+
|
|
22
|
+
# Extract and remove metadata option from options if present
|
|
23
|
+
metadata = options.delete(:metadata)
|
|
24
|
+
|
|
25
|
+
# Handle different calling patterns:
|
|
26
|
+
# 1. command(value) or command(value, guards: false) - shorthand form with single value
|
|
27
|
+
# 2. command({attr: value}) or command({attr: value}, guards: false) - hash form
|
|
28
|
+
# 3. command(attr: value) - ALL kwargs are treated as payload (no options)
|
|
29
|
+
# 4. command() - no arguments (for commands without payload)
|
|
30
|
+
|
|
31
|
+
# If no positional argument was provided but kwargs were given (after removing guards/metadata),
|
|
32
|
+
# treat all remaining kwargs as payload
|
|
33
|
+
if payload.nil? && !options.empty?
|
|
34
|
+
payload = options
|
|
35
|
+
{}
|
|
36
|
+
elsif payload.nil?
|
|
37
|
+
payload = {}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Pass metadata to CommandHandler which will merge it into the event metadata
|
|
41
|
+
Yes::Core::CommandHandling::CommandHandler.new(self).call(command_name, payload, guards:, metadata:)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
class Aggregate
|
|
6
|
+
# Provides authorizer functionality for aggregates.
|
|
7
|
+
#
|
|
8
|
+
# This concern automatically sets up an authorizer class for the aggregate
|
|
9
|
+
# using `Yes::Core::Aggregate::Dsl::ClassResolvers::Authorizer`.
|
|
10
|
+
# The authorizer class is typically used for command authorization via Cerbos.
|
|
11
|
+
#
|
|
12
|
+
module HasAuthorizer
|
|
13
|
+
extend ActiveSupport::Concern
|
|
14
|
+
|
|
15
|
+
# Mutable struct for holding authorizer options
|
|
16
|
+
AuthorizerOptions = Struct.new(
|
|
17
|
+
:authorizer_base_class,
|
|
18
|
+
:context,
|
|
19
|
+
:aggregate,
|
|
20
|
+
:read_model_class,
|
|
21
|
+
:resource_name,
|
|
22
|
+
:authorizer_block,
|
|
23
|
+
:draftable,
|
|
24
|
+
:changes_read_model_class,
|
|
25
|
+
keyword_init: true
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
included do
|
|
29
|
+
class << self
|
|
30
|
+
# @!attribute [rw] authorizer_class
|
|
31
|
+
# @return [Class] The resolved authorizer class used for command authorization.
|
|
32
|
+
#
|
|
33
|
+
# @!attribute [rw] authorizer_options
|
|
34
|
+
# @return [AuthorizerOptions] Options used when building the authorizer.
|
|
35
|
+
attr_accessor :authorizer_class, :authorizer_options
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class_methods do
|
|
40
|
+
# Finds or generates the authorizer class using `ClassResolvers::Authorizer`,
|
|
41
|
+
# passing the authorizer parameters.
|
|
42
|
+
# Registers the authorizer in the configuration and stores the resolved class internally.
|
|
43
|
+
# @return [Class] The resolved authorizer class.
|
|
44
|
+
def setup_authorizer_classes
|
|
45
|
+
return unless authorizer_options
|
|
46
|
+
|
|
47
|
+
authorizer_options.read_model_class ||= read_model_class
|
|
48
|
+
authorizer_options.resource_name ||= aggregate.underscore
|
|
49
|
+
|
|
50
|
+
# Set draftable and changes read model class if the aggregate is draftable
|
|
51
|
+
authorizer_options.draftable ||= draftable?
|
|
52
|
+
authorizer_options.changes_read_model_class ||= changes_read_model_class if draftable?
|
|
53
|
+
|
|
54
|
+
self.authorizer_class = Yes::Core::Aggregate::Dsl::ClassResolvers::Authorizer.new(authorizer_options).call
|
|
55
|
+
|
|
56
|
+
commands.each_value do |command_data|
|
|
57
|
+
Dsl::ClassResolvers::Command::AuthorizerFactory.create(command_data)&.call
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @param cerbos [Boolean] Whether to use Cerbos authorizer.
|
|
62
|
+
# @param read_model_class [Class] The read model class to use for the authorizer.
|
|
63
|
+
# @param resource_name [String] The resource name to use for the authorizer.
|
|
64
|
+
# @param block [Proc] The block to use for the authorizer.
|
|
65
|
+
# @return [Class] The authorizer class.
|
|
66
|
+
def authorize(cerbos: false, read_model_class: nil, resource_name: nil, &block)
|
|
67
|
+
authorizer_base_class = if cerbos
|
|
68
|
+
Yes::Core::Authorization::CommandCerbosAuthorizer
|
|
69
|
+
else
|
|
70
|
+
Yes::Core::Authorization::CommandAuthorizer
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
self.authorizer_options = AuthorizerOptions.new(
|
|
74
|
+
authorizer_base_class:,
|
|
75
|
+
context:,
|
|
76
|
+
aggregate:,
|
|
77
|
+
read_model_class:,
|
|
78
|
+
resource_name:,
|
|
79
|
+
authorizer_block: block
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|