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,50 @@
|
|
|
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 simple non-Cerbos authorizers
|
|
10
|
+
class SimpleAuthorizer < Authorizer
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# Generates a simple authorizer class with a call method
|
|
14
|
+
#
|
|
15
|
+
# @return [Class] The generated authorizer class
|
|
16
|
+
def generate_class
|
|
17
|
+
klass = Class.new(command_data.aggregate_class.authorizer_class)
|
|
18
|
+
|
|
19
|
+
# Only add call method if we have a block
|
|
20
|
+
apply_call_override(klass) if command_data.authorizer_block
|
|
21
|
+
|
|
22
|
+
klass
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Adds a `call` class method that executes the user-provided block
|
|
26
|
+
#
|
|
27
|
+
# @param klass [Class] the class being generated
|
|
28
|
+
def apply_call_override(klass)
|
|
29
|
+
user_block = command_data.authorizer_block
|
|
30
|
+
|
|
31
|
+
klass.define_singleton_method(:call) do |cmd, auth|
|
|
32
|
+
@_cmd = cmd
|
|
33
|
+
@_auth = auth
|
|
34
|
+
define_singleton_method(:command) { @_cmd }
|
|
35
|
+
define_singleton_method(:auth_data) { @_auth }
|
|
36
|
+
begin
|
|
37
|
+
instance_exec(&user_block)
|
|
38
|
+
ensure
|
|
39
|
+
@_cmd = nil
|
|
40
|
+
@_auth = nil
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
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 state updater classes for aggregate commands
|
|
10
|
+
#
|
|
11
|
+
# This class resolver generates plain state updater class that process
|
|
12
|
+
# custom state updates in aggregates.
|
|
13
|
+
#
|
|
14
|
+
class StateUpdater < Base
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
# Returns the class type symbol for the state updater
|
|
18
|
+
#
|
|
19
|
+
# @return [Symbol] Returns :state_updater as the class type
|
|
20
|
+
# @api private
|
|
21
|
+
def class_type
|
|
22
|
+
:state_updater
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Returns the name for the state updater class
|
|
26
|
+
#
|
|
27
|
+
# @return [String] The name of the state updater class derived from the command
|
|
28
|
+
# @api private
|
|
29
|
+
def class_name
|
|
30
|
+
command_data.name
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Creates a new state updater class
|
|
34
|
+
#
|
|
35
|
+
# @return [Class] A new state updater class inheriting from Yes::Core::CommandHandling::StateUpdater
|
|
36
|
+
# @api private
|
|
37
|
+
def generate_class
|
|
38
|
+
Class.new(Yes::Core::CommandHandling::StateUpdater)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
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 read model classes for aggregates
|
|
9
|
+
#
|
|
10
|
+
# This class resolver generates ActiveRecord-based read model classes
|
|
11
|
+
# that represent the queryable state of aggregates. Each read model class
|
|
12
|
+
# is automatically configured with basic scopes and table name conventions.
|
|
13
|
+
#
|
|
14
|
+
# @example Generated read model class structure
|
|
15
|
+
# class User < ApplicationRecord
|
|
16
|
+
# self.table_name = 'users'
|
|
17
|
+
# scope :by_ids, ->(ids) { where(id: ids) }
|
|
18
|
+
# end
|
|
19
|
+
class ReadModel < Base
|
|
20
|
+
# Initializes a new read model class resolver
|
|
21
|
+
#
|
|
22
|
+
# @param read_model_name [String] The name of the read model
|
|
23
|
+
# @param context_name [String] The name of the context
|
|
24
|
+
# @param aggregate_name [String] The name of the aggregate
|
|
25
|
+
def initialize(read_model_name, context_name, aggregate_name, draft: false)
|
|
26
|
+
@read_model_name = read_model_name
|
|
27
|
+
@draft = draft
|
|
28
|
+
|
|
29
|
+
super(context_name, aggregate_name)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Creates and registers the read model class in the Yes::Core configuration
|
|
33
|
+
#
|
|
34
|
+
# @return [Class] The found or generated read model class that was registered
|
|
35
|
+
def call
|
|
36
|
+
Yes::Core.configuration.register_read_model_class(
|
|
37
|
+
context_name,
|
|
38
|
+
aggregate_name,
|
|
39
|
+
find_or_generate_class,
|
|
40
|
+
draft:
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# @return [String] The name of the read model
|
|
47
|
+
attr_reader :read_model_name, :draft
|
|
48
|
+
|
|
49
|
+
# @return [Symbol] Returns :read_model as the class type
|
|
50
|
+
def class_type
|
|
51
|
+
:read_model
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @return [String] The name of the read model class
|
|
55
|
+
def class_name
|
|
56
|
+
read_model_name
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Generates a new read model class with the required configuration
|
|
60
|
+
#
|
|
61
|
+
# @return [Class] A new read model class inheriting from ApplicationRecord
|
|
62
|
+
def generate_class
|
|
63
|
+
table_name = class_name.tableize
|
|
64
|
+
|
|
65
|
+
Class.new(ApplicationRecord) do
|
|
66
|
+
self.table_name = table_name
|
|
67
|
+
scope :by_ids, ->(ids) { where(id: ids) }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
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
|
+
# Creates and registers read model filter classes for aggregates
|
|
9
|
+
#
|
|
10
|
+
# This class resolver generates filter classes that provide query scopes
|
|
11
|
+
# for read models. Each filter class is automatically configured with
|
|
12
|
+
# basic scopes and a reference to its corresponding read model class.
|
|
13
|
+
#
|
|
14
|
+
# @example Generated read model filter class structure
|
|
15
|
+
# class UserFilter < Yes::Core::ReadModel::Filter
|
|
16
|
+
# has_scope :ids do |_, scope, value|
|
|
17
|
+
# scope.by_ids(value.split(','))
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# private
|
|
21
|
+
#
|
|
22
|
+
# def read_model_class
|
|
23
|
+
# Test::User::Aggregate.read_model_class
|
|
24
|
+
# end
|
|
25
|
+
# end
|
|
26
|
+
class ReadModelFilter < Base
|
|
27
|
+
# Initializes a new read model filter class resolver
|
|
28
|
+
#
|
|
29
|
+
# @param read_model_name [String] The name of the read model
|
|
30
|
+
# @param context_name [String] The name of the context
|
|
31
|
+
# @param aggregate_name [String] The name of the aggregate
|
|
32
|
+
def initialize(read_model_name, context_name, aggregate_name)
|
|
33
|
+
@read_model_name = read_model_name
|
|
34
|
+
|
|
35
|
+
super(context_name, aggregate_name)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Creates and registers the read model filter class in the Yes::Core configuration
|
|
39
|
+
#
|
|
40
|
+
# @return [Class] The found or generated read model filter class that was registered
|
|
41
|
+
def call
|
|
42
|
+
Yes::Core.configuration.register_read_model_filter_class(
|
|
43
|
+
context_name,
|
|
44
|
+
aggregate_name,
|
|
45
|
+
find_or_generate_class
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# @return [String] The name of the read model
|
|
52
|
+
attr_reader :read_model_name
|
|
53
|
+
|
|
54
|
+
# @return [Symbol] Returns :read_model_filter as the class type
|
|
55
|
+
def class_type
|
|
56
|
+
:read_model_filter
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @return [String] The name of the read model filter class
|
|
60
|
+
def class_name
|
|
61
|
+
read_model_name
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Generates a new read model filter class with the required scopes and configuration
|
|
65
|
+
#
|
|
66
|
+
# @return [Class] A new filter class inheriting from Yes::Core::ReadModel::Filter
|
|
67
|
+
def generate_class
|
|
68
|
+
klass = Class.new(Yes::Core::ReadModel::Filter)
|
|
69
|
+
|
|
70
|
+
klass.has_scope :ids do |_, scope, value|
|
|
71
|
+
scope.by_ids(value.split(','))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Define read_model_class method
|
|
75
|
+
aggregate_class_name = "#{context_name}::#{aggregate_name}::Aggregate"
|
|
76
|
+
klass.define_method(:read_model_class) do
|
|
77
|
+
aggregate_class_name.constantize.read_model_class
|
|
78
|
+
end
|
|
79
|
+
klass.send :private, :read_model_class
|
|
80
|
+
|
|
81
|
+
klass
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
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 read model serializer classes for aggregates
|
|
9
|
+
#
|
|
10
|
+
# This class resolver generates JSON:API compliant serializer classes
|
|
11
|
+
# for read models. Each serializer class is automatically configured with
|
|
12
|
+
# the specified attributes and type naming conventions.
|
|
13
|
+
#
|
|
14
|
+
# @example Generated read model serializer class structure
|
|
15
|
+
# class UserSerializer < Yes::Core::Serializer
|
|
16
|
+
# set_type 'users'
|
|
17
|
+
# attributes :id, :email, :first_name, :last_name
|
|
18
|
+
# end
|
|
19
|
+
class ReadModelSerializer < Base
|
|
20
|
+
# Initializes a new read model serializer class resolver
|
|
21
|
+
#
|
|
22
|
+
# @param read_model_name [String] The name of the read model
|
|
23
|
+
# @param context_name [String] The name of the context
|
|
24
|
+
# @param aggregate_name [String] The name of the aggregate
|
|
25
|
+
# @param read_model_attributes [Array<Symbol>] The attributes to be serialized
|
|
26
|
+
def initialize(read_model_name, context_name, aggregate_name, read_model_attributes)
|
|
27
|
+
@read_model_name = read_model_name
|
|
28
|
+
@read_model_attributes = read_model_attributes
|
|
29
|
+
|
|
30
|
+
super(context_name, aggregate_name)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Creates and registers the read model serializer class in the Yes::Core configuration
|
|
34
|
+
#
|
|
35
|
+
# @return [Class] The found or generated read model serializer class that was registered
|
|
36
|
+
def call
|
|
37
|
+
Yes::Core.configuration.register_read_model_filter_class(
|
|
38
|
+
context_name,
|
|
39
|
+
aggregate_name,
|
|
40
|
+
find_or_generate_class
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# @return [String] The name of the read model
|
|
47
|
+
# @return [Array<Symbol>] The attributes to be serialized
|
|
48
|
+
attr_reader :read_model_name, :read_model_attributes
|
|
49
|
+
|
|
50
|
+
# @return [Symbol] Returns :read_model_serializer as the class type
|
|
51
|
+
def class_type
|
|
52
|
+
:read_model_serializer
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @return [String] The name of the read model serializer class
|
|
56
|
+
def class_name
|
|
57
|
+
read_model_name
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Generates a new read model serializer class with the required configuration
|
|
61
|
+
#
|
|
62
|
+
# @return [Class] A new serializer class inheriting from Yes::Core::Serializer
|
|
63
|
+
def generate_class
|
|
64
|
+
klass = Class.new(Yes::Core::Serializer)
|
|
65
|
+
|
|
66
|
+
klass.set_type read_model_name.pluralize
|
|
67
|
+
klass.attributes(*read_model_attributes)
|
|
68
|
+
|
|
69
|
+
klass
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
class Aggregate
|
|
6
|
+
module Dsl
|
|
7
|
+
# Data object that holds information about a command definition in an aggregate
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# CommandData.new(:assign_user, UserAggregate, context: 'Users', aggregate: 'User')
|
|
11
|
+
#
|
|
12
|
+
class CommandData
|
|
13
|
+
attr_reader :name, :context_name, :aggregate_name, :aggregate_class
|
|
14
|
+
attr_accessor :event_name, :payload_attributes, :update_state_block, :guard_names, :authorizer_block,
|
|
15
|
+
:encrypted_attributes
|
|
16
|
+
|
|
17
|
+
# @param name [Symbol] The name of the command
|
|
18
|
+
# @param aggregate_class [Class] The aggregate class this command belongs to
|
|
19
|
+
# @param options [Hash] Additional options for the command
|
|
20
|
+
# @option options [String] :context The context name for the command
|
|
21
|
+
# @option options [String] :aggregate The aggregate name
|
|
22
|
+
# @option options [String] :event_name The event name for the command
|
|
23
|
+
# @option options [Hash] :payload_attributes The payload attributes for the command
|
|
24
|
+
def initialize(name, aggregate_class, options = {})
|
|
25
|
+
@name = name
|
|
26
|
+
@aggregate_class = aggregate_class
|
|
27
|
+
@context_name = options.delete(:context)
|
|
28
|
+
@aggregate_name = options.delete(:aggregate)
|
|
29
|
+
|
|
30
|
+
# Default event name based on command name (will be overridden if specified in DSL)
|
|
31
|
+
@event_name = options.delete(:event_name) || Yes::Core::Utils::EventNameResolver.call(name)
|
|
32
|
+
|
|
33
|
+
# Default payload is just the aggregate_id
|
|
34
|
+
@payload_attributes = options.delete(:payload_attributes) || {}
|
|
35
|
+
|
|
36
|
+
# Store guard names
|
|
37
|
+
@guard_names = []
|
|
38
|
+
|
|
39
|
+
# Track encrypted attributes for event schema generation
|
|
40
|
+
@encrypted_attributes = []
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Add a guard name to the list of guards
|
|
44
|
+
#
|
|
45
|
+
# @param name [Symbol] The name of the guard
|
|
46
|
+
# @return [void]
|
|
47
|
+
def add_guard(name)
|
|
48
|
+
@guard_names << name
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
class Aggregate
|
|
6
|
+
module Dsl
|
|
7
|
+
# Factory class that creates and defines commands on aggregates.
|
|
8
|
+
# Handles the creation and registration of all necessary command-related classes,
|
|
9
|
+
# including validation of attributes and DSL evaluation.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# command_data = CommandData.new(name: :assign_user, aggregate_class: Company)
|
|
13
|
+
# CommandDefiner.new(command_data).call do
|
|
14
|
+
# payload user_id: :uuid
|
|
15
|
+
#
|
|
16
|
+
# guard :user_already_assigned do
|
|
17
|
+
# user_id.present?
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# update_state do
|
|
21
|
+
# some_attribute { payload.xyz }
|
|
22
|
+
# another_attribute { "#{payload.abc}_#{email}" }
|
|
23
|
+
# end
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
class CommandDefiner
|
|
27
|
+
# Error raised when attributes are used in the command that are not defined on the aggregate
|
|
28
|
+
class UndefinedAttributeError < Yes::Core::Error; end
|
|
29
|
+
|
|
30
|
+
# Error raised when event name cannot be resolved
|
|
31
|
+
class EventNameResolverError < Yes::Core::Error; end
|
|
32
|
+
|
|
33
|
+
# @return [CommandData] The data object containing command configuration
|
|
34
|
+
attr_reader :command_data
|
|
35
|
+
private :command_data
|
|
36
|
+
|
|
37
|
+
# Initializes a new CommandDefiner instance
|
|
38
|
+
#
|
|
39
|
+
# @param command_data [CommandData] The data object containing command configuration
|
|
40
|
+
# @return [CommandDefiner] A new instance of CommandDefiner
|
|
41
|
+
def initialize(command_data)
|
|
42
|
+
@command_data = command_data
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Generates and registers all necessary classes for the command.
|
|
46
|
+
# This includes command classes, event classes, a guard evaluator class,
|
|
47
|
+
# a state updater class, as well as defining related methods on the aggregate class.
|
|
48
|
+
#
|
|
49
|
+
# @yield Block for defining payload, guards and other command configurations
|
|
50
|
+
# @yieldreturn [void]
|
|
51
|
+
# @return [void]
|
|
52
|
+
# @raise [UndefinedAttributeError] If attributes used in the command are not defined on the aggregate
|
|
53
|
+
def call(&block)
|
|
54
|
+
create_and_register_block_evaluator_classes
|
|
55
|
+
evaluate_dsl_block(&block) if block
|
|
56
|
+
validate_event_name
|
|
57
|
+
validate_accessed_attributes
|
|
58
|
+
create_and_register_command_classes
|
|
59
|
+
register_command_events
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# Validates that the event name is present (either because it was specified explicitly or because it was
|
|
65
|
+
# resolved from the command name).
|
|
66
|
+
#
|
|
67
|
+
# @return [void]
|
|
68
|
+
# @raise [EventNameResolverError] If the event name is not specified explicitly
|
|
69
|
+
def validate_event_name
|
|
70
|
+
return if command_data.event_name
|
|
71
|
+
|
|
72
|
+
raise EventNameResolverError,
|
|
73
|
+
"Event name for command #{command_data.context_name}::#{command_data.aggregate_name} " \
|
|
74
|
+
"#{command_data.name} cannot be resolved, please specify explicitly."
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Creates and registers the guard evaluator and state updater classes
|
|
78
|
+
#
|
|
79
|
+
# @return [void]
|
|
80
|
+
def create_and_register_block_evaluator_classes
|
|
81
|
+
@guard_evaluator_class = ClassResolvers::Command::GuardEvaluator.new(command_data).call
|
|
82
|
+
@state_updater_class = ClassResolvers::Command::StateUpdater.new(command_data).call
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Creates and registers command-related classes and defines their methods
|
|
86
|
+
#
|
|
87
|
+
# @return [void]
|
|
88
|
+
def create_and_register_command_classes
|
|
89
|
+
@command_class = ClassResolvers::Command::Command.new(command_data).call
|
|
90
|
+
@event_class = ClassResolvers::Command::Event.new(command_data).call
|
|
91
|
+
|
|
92
|
+
MethodDefiners::Command::Command.new(command_data).call
|
|
93
|
+
MethodDefiners::Command::CanCommand.new(command_data).call
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Validates attributes based on whether the command uses update_state or payload
|
|
97
|
+
#
|
|
98
|
+
# @return [void]
|
|
99
|
+
# @raise [UndefinedAttributeError] If any attributes are not defined on the aggregate
|
|
100
|
+
def validate_accessed_attributes
|
|
101
|
+
if command_data.update_state_block
|
|
102
|
+
validate_attributes!(
|
|
103
|
+
@state_updater_class.updated_attributes,
|
|
104
|
+
'update_state block uses attributes'
|
|
105
|
+
)
|
|
106
|
+
else
|
|
107
|
+
validate_attributes!(
|
|
108
|
+
extract_payload_attribute_names,
|
|
109
|
+
'payload attributes'
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Extract attribute names from payload_attributes, handling the new format for optional attributes
|
|
115
|
+
#
|
|
116
|
+
# @return [Array<Symbol>] The attribute names
|
|
117
|
+
def extract_payload_attribute_names
|
|
118
|
+
command_data.payload_attributes.keys.without(:locale)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Validates that all given attributes are defined on the aggregate
|
|
122
|
+
#
|
|
123
|
+
# @param attributes [Array<Symbol>] The attributes to validate
|
|
124
|
+
# @param context [String] The context in which these attributes are being used
|
|
125
|
+
# @return [void]
|
|
126
|
+
# @raise [UndefinedAttributeError] If any of the attributes are not defined on the aggregate
|
|
127
|
+
def validate_attributes!(attributes, context)
|
|
128
|
+
return if attributes.empty?
|
|
129
|
+
|
|
130
|
+
aggregate_attributes = command_data.aggregate_class.attributes
|
|
131
|
+
aggregate_type_aggregate_attributes = aggregate_attributes.select { _2 == :aggregate }
|
|
132
|
+
undefined_attributes = attributes.reject do |attr_name|
|
|
133
|
+
aggregate_attributes.key?(attr_name) ||
|
|
134
|
+
aggregate_type_aggregate_attributes.key?(attr_name.to_s.delete_suffix('_id').to_sym)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
return if undefined_attributes.empty?
|
|
138
|
+
|
|
139
|
+
raise UndefinedAttributeError, "Command '#{command_data.name}' #{context} " \
|
|
140
|
+
"that are not defined on the aggregate: #{undefined_attributes.join(', ')}. " \
|
|
141
|
+
"Please define these attributes using the 'attribute' method first."
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def register_command_events
|
|
145
|
+
Yes::Core.configuration.register_command_events(
|
|
146
|
+
command_data.context_name,
|
|
147
|
+
command_data.aggregate_name,
|
|
148
|
+
command_data.name,
|
|
149
|
+
[command_data.event_name]
|
|
150
|
+
)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Evaluates the DSL block in the context of a DslEvaluator
|
|
154
|
+
#
|
|
155
|
+
# @yield The block to evaluate
|
|
156
|
+
# @yieldreturn [void]
|
|
157
|
+
# @return [void]
|
|
158
|
+
def evaluate_dsl_block(&block)
|
|
159
|
+
return unless block
|
|
160
|
+
|
|
161
|
+
dsl_evaluator = DslEvaluator.new(command_data, @guard_evaluator_class, @state_updater_class)
|
|
162
|
+
dsl_evaluator.instance_eval(&block)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# DSL evaluator class for command configuration blocks
|
|
166
|
+
class DslEvaluator
|
|
167
|
+
# @return [CommandData] The command data being configured
|
|
168
|
+
# @return [Class] The guard evaluator class for this command
|
|
169
|
+
# @return [Class] The state updater class for this command
|
|
170
|
+
attr_reader :command_data, :guard_evaluator_class, :state_updater_class
|
|
171
|
+
|
|
172
|
+
# @param command_data [CommandData] The command data to configure
|
|
173
|
+
# @param guard_evaluator_class [Class] The guard evaluator class for this command
|
|
174
|
+
# @param state_updater_class [Class] The state updater class for this command
|
|
175
|
+
def initialize(command_data, guard_evaluator_class, state_updater_class)
|
|
176
|
+
@command_data = command_data
|
|
177
|
+
@guard_evaluator_class = guard_evaluator_class
|
|
178
|
+
@state_updater_class = state_updater_class
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Defines a guard for the command
|
|
182
|
+
#
|
|
183
|
+
# @param name [Symbol] The name of the guard
|
|
184
|
+
# @param error_extra [Hash, Proc] The extra information to be added to the error message payload
|
|
185
|
+
# @yield The guard evaluation block
|
|
186
|
+
# @yieldreturn [Boolean] True if the guard passes, false otherwise
|
|
187
|
+
# @return [void]
|
|
188
|
+
def guard(name, error_extra: {}, &)
|
|
189
|
+
command_data.add_guard(name)
|
|
190
|
+
guard_evaluator_class.guard(name, error_extra:, &)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Defines the payload for the command
|
|
194
|
+
# Supports inline encryption declaration: payload email: { type: :email, encrypt: true }
|
|
195
|
+
#
|
|
196
|
+
# @param attributes [Hash] The attributes for the payload
|
|
197
|
+
# @return [void]
|
|
198
|
+
def payload(attributes)
|
|
199
|
+
normalized = attributes.transform_values do |value|
|
|
200
|
+
case value
|
|
201
|
+
when Hash
|
|
202
|
+
# Extract encrypt flag if present, pass everything else through
|
|
203
|
+
value.except(:encrypt)
|
|
204
|
+
else
|
|
205
|
+
# Handle simple syntax: :email or :string
|
|
206
|
+
value
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Collect encrypted attributes
|
|
211
|
+
attributes.each do |key, value|
|
|
212
|
+
command_data.encrypted_attributes << key if value.is_a?(Hash) && value[:encrypt]
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
command_data.payload_attributes = normalized
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Declares which payload attributes should be encrypted in the generated event
|
|
219
|
+
#
|
|
220
|
+
# @param attribute_names [Array<Symbol>] The names of payload attributes to encrypt
|
|
221
|
+
# @return [void]
|
|
222
|
+
# @example
|
|
223
|
+
# command :update_contact_info do
|
|
224
|
+
# payload email: :email, phone: :phone
|
|
225
|
+
# encrypt :email, :phone
|
|
226
|
+
# end
|
|
227
|
+
def encrypt(*attribute_names)
|
|
228
|
+
command_data.encrypted_attributes.concat(attribute_names)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Defines how the state should be updated
|
|
232
|
+
#
|
|
233
|
+
# @param custom [Boolean] Whether the state should be updated using a custom block
|
|
234
|
+
# @yield Block defining how to update the state
|
|
235
|
+
# @yieldreturn [void]
|
|
236
|
+
# @return [void]
|
|
237
|
+
def update_state(custom: false, &block)
|
|
238
|
+
command_data.update_state_block = block
|
|
239
|
+
state_updater_class.update_state(custom:, &block)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Defines the event name for the command
|
|
243
|
+
#
|
|
244
|
+
# @param name [Symbol] The name of the event
|
|
245
|
+
# @return [void]
|
|
246
|
+
def event(name)
|
|
247
|
+
command_data.event_name = name
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Overwrites the authorizer for the command
|
|
251
|
+
#
|
|
252
|
+
# @yield The authorizer block
|
|
253
|
+
# @yieldreturn [void]
|
|
254
|
+
# @return [void]
|
|
255
|
+
def authorize(&block)
|
|
256
|
+
command_data.authorizer_block = block
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|