yes-core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +69 -0
  5. data/lib/yes/core/active_job_serializers/command_group_serializer.rb +29 -0
  6. data/lib/yes/core/active_job_serializers/dry_struct_serializer.rb +57 -0
  7. data/lib/yes/core/aggregate/draftable.rb +205 -0
  8. data/lib/yes/core/aggregate/dsl/attribute_data.rb +37 -0
  9. data/lib/yes/core/aggregate/dsl/attribute_definer.rb +54 -0
  10. data/lib/yes/core/aggregate/dsl/attribute_definers/aggregate.rb +36 -0
  11. data/lib/yes/core/aggregate/dsl/attribute_definers/standard.rb +36 -0
  12. data/lib/yes/core/aggregate/dsl/class_name_convention.rb +80 -0
  13. data/lib/yes/core/aggregate/dsl/class_resolvers/authorizer.rb +132 -0
  14. data/lib/yes/core/aggregate/dsl/class_resolvers/base.rb +80 -0
  15. data/lib/yes/core/aggregate/dsl/class_resolvers/command/authorizer.rb +30 -0
  16. data/lib/yes/core/aggregate/dsl/class_resolvers/command/authorizer_factory.rb +34 -0
  17. data/lib/yes/core/aggregate/dsl/class_resolvers/command/base.rb +38 -0
  18. data/lib/yes/core/aggregate/dsl/class_resolvers/command/cerbos_authorizer.rb +114 -0
  19. data/lib/yes/core/aggregate/dsl/class_resolvers/command/command.rb +70 -0
  20. data/lib/yes/core/aggregate/dsl/class_resolvers/command/event.rb +88 -0
  21. data/lib/yes/core/aggregate/dsl/class_resolvers/command/guard_evaluator.rb +84 -0
  22. data/lib/yes/core/aggregate/dsl/class_resolvers/command/simple_authorizer.rb +50 -0
  23. data/lib/yes/core/aggregate/dsl/class_resolvers/command/state_updater.rb +46 -0
  24. data/lib/yes/core/aggregate/dsl/class_resolvers/read_model.rb +75 -0
  25. data/lib/yes/core/aggregate/dsl/class_resolvers/read_model_filter.rb +88 -0
  26. data/lib/yes/core/aggregate/dsl/class_resolvers/read_model_serializer.rb +76 -0
  27. data/lib/yes/core/aggregate/dsl/command_data.rb +54 -0
  28. data/lib/yes/core/aggregate/dsl/command_definer.rb +263 -0
  29. data/lib/yes/core/aggregate/dsl/command_shortcut_expander.rb +233 -0
  30. data/lib/yes/core/aggregate/dsl/constant_resolver.rb +67 -0
  31. data/lib/yes/core/aggregate/dsl/method_definers/attribute/accessor.rb +28 -0
  32. data/lib/yes/core/aggregate/dsl/method_definers/attribute/aggregate_accessor.rb +36 -0
  33. data/lib/yes/core/aggregate/dsl/method_definers/attribute/base.rb +42 -0
  34. data/lib/yes/core/aggregate/dsl/method_definers/command/base.rb +42 -0
  35. data/lib/yes/core/aggregate/dsl/method_definers/command/can_command.rb +41 -0
  36. data/lib/yes/core/aggregate/dsl/method_definers/command/command.rb +50 -0
  37. data/lib/yes/core/aggregate/has_authorizer.rb +86 -0
  38. data/lib/yes/core/aggregate/has_read_model.rb +169 -0
  39. data/lib/yes/core/aggregate/read_model_rebuilder.rb +40 -0
  40. data/lib/yes/core/aggregate/shared_read_model_rebuilder.rb +158 -0
  41. data/lib/yes/core/aggregate.rb +404 -0
  42. data/lib/yes/core/authentication_error.rb +8 -0
  43. data/lib/yes/core/authorization/cerbos_client_provider.rb +27 -0
  44. data/lib/yes/core/authorization/command_authorizer.rb +40 -0
  45. data/lib/yes/core/authorization/command_cerbos_authorizer.rb +182 -0
  46. data/lib/yes/core/authorization/read_model_authorizer.rb +22 -0
  47. data/lib/yes/core/authorization/read_models_authorizer.rb +49 -0
  48. data/lib/yes/core/authorization/read_request_authorizer.rb +32 -0
  49. data/lib/yes/core/authorization/read_request_cerbos_authorizer.rb +112 -0
  50. data/lib/yes/core/command.rb +35 -0
  51. data/lib/yes/core/command_handling/aggregate_tracker.rb +33 -0
  52. data/lib/yes/core/command_handling/command_executor.rb +171 -0
  53. data/lib/yes/core/command_handling/command_handler.rb +124 -0
  54. data/lib/yes/core/command_handling/event_publisher.rb +189 -0
  55. data/lib/yes/core/command_handling/guard_evaluator.rb +165 -0
  56. data/lib/yes/core/command_handling/guard_runner.rb +76 -0
  57. data/lib/yes/core/command_handling/payload_proxy.rb +159 -0
  58. data/lib/yes/core/command_handling/read_model_recovery_service.rb +264 -0
  59. data/lib/yes/core/command_handling/read_model_revision_guard.rb +198 -0
  60. data/lib/yes/core/command_handling/read_model_updater.rb +103 -0
  61. data/lib/yes/core/command_handling/state_updater.rb +113 -0
  62. data/lib/yes/core/commands/bus.rb +46 -0
  63. data/lib/yes/core/commands/group.rb +135 -0
  64. data/lib/yes/core/commands/group_response.rb +13 -0
  65. data/lib/yes/core/commands/helper.rb +126 -0
  66. data/lib/yes/core/commands/notifier.rb +65 -0
  67. data/lib/yes/core/commands/processor.rb +137 -0
  68. data/lib/yes/core/commands/response.rb +63 -0
  69. data/lib/yes/core/commands/stateless/group_handler.rb +186 -0
  70. data/lib/yes/core/commands/stateless/group_response.rb +15 -0
  71. data/lib/yes/core/commands/stateless/handler.rb +292 -0
  72. data/lib/yes/core/commands/stateless/handler_helpers.rb +321 -0
  73. data/lib/yes/core/commands/stateless/response.rb +14 -0
  74. data/lib/yes/core/commands/stateless/subject.rb +41 -0
  75. data/lib/yes/core/commands/validator.rb +28 -0
  76. data/lib/yes/core/configuration.rb +432 -0
  77. data/lib/yes/core/data_decryptor.rb +59 -0
  78. data/lib/yes/core/data_encryptor.rb +60 -0
  79. data/lib/yes/core/encryption_metadata.rb +33 -0
  80. data/lib/yes/core/error.rb +14 -0
  81. data/lib/yes/core/error_messages.rb +37 -0
  82. data/lib/yes/core/event.rb +222 -0
  83. data/lib/yes/core/event_class_resolver.rb +40 -0
  84. data/lib/yes/core/generators/read_models/add_pending_update_tracking_generator.rb +43 -0
  85. data/lib/yes/core/generators/read_models/templates/add_pending_update_tracking.rb.erb +122 -0
  86. data/lib/yes/core/generators/read_models/templates/migration.rb.erb +9 -0
  87. data/lib/yes/core/generators/read_models/update_generator.rb +147 -0
  88. data/lib/yes/core/jobs/read_model_recovery_job.rb +219 -0
  89. data/lib/yes/core/middlewares/encryptor.rb +48 -0
  90. data/lib/yes/core/middlewares/timestamp.rb +29 -0
  91. data/lib/yes/core/middlewares/with_indifferent_access.rb +22 -0
  92. data/lib/yes/core/models/application_record.rb +9 -0
  93. data/lib/yes/core/open_telemetry/otl_span.rb +110 -0
  94. data/lib/yes/core/open_telemetry/trackable.rb +101 -0
  95. data/lib/yes/core/payload_store/base.rb +33 -0
  96. data/lib/yes/core/payload_store/errors.rb +13 -0
  97. data/lib/yes/core/payload_store/lookup.rb +44 -0
  98. data/lib/yes/core/process_managers/access_token_client.rb +107 -0
  99. data/lib/yes/core/process_managers/base.rb +40 -0
  100. data/lib/yes/core/process_managers/command_runner.rb +109 -0
  101. data/lib/yes/core/process_managers/service_client.rb +57 -0
  102. data/lib/yes/core/process_managers/state.rb +118 -0
  103. data/lib/yes/core/railtie.rb +58 -0
  104. data/lib/yes/core/read_model/builder.rb +267 -0
  105. data/lib/yes/core/read_model/event_handler.rb +64 -0
  106. data/lib/yes/core/read_model/filter.rb +118 -0
  107. data/lib/yes/core/read_model/filter_query_builder.rb +104 -0
  108. data/lib/yes/core/serializer.rb +21 -0
  109. data/lib/yes/core/subscriptions.rb +94 -0
  110. data/lib/yes/core/test_support/event_helpers.rb +27 -0
  111. data/lib/yes/core/test_support/jwt_helpers.rb +30 -0
  112. data/lib/yes/core/test_support/subscriptions_helper.rb +88 -0
  113. data/lib/yes/core/test_support/test_helper.rb +27 -0
  114. data/lib/yes/core/test_support.rb +5 -0
  115. data/lib/yes/core/transaction_details.rb +90 -0
  116. data/lib/yes/core/type_lookup.rb +88 -0
  117. data/lib/yes/core/types.rb +110 -0
  118. data/lib/yes/core/utils/aggregate_shortcuts.rb +164 -0
  119. data/lib/yes/core/utils/caller_utils.rb +37 -0
  120. data/lib/yes/core/utils/command_utils.rb +226 -0
  121. data/lib/yes/core/utils/error_notifier.rb +101 -0
  122. data/lib/yes/core/utils/event_name_resolver.rb +67 -0
  123. data/lib/yes/core/utils/exponential_retrier.rb +180 -0
  124. data/lib/yes/core/utils/hash_utils.rb +63 -0
  125. data/lib/yes/core/version.rb +7 -0
  126. data/lib/yes/core.rb +85 -0
  127. data/lib/yes.rb +0 -0
  128. metadata +324 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c43247d2dbf4c79fac0069c1fb4887a9fba4b38f845736b67dbb9ee2f213e96f
4
+ data.tar.gz: f78fa82e84433fb209a0e0dc5258ee7569e1993af11436112c590c7094f73b9c
5
+ SHA512:
6
+ metadata.gz: 1c82e17ddceef537ce4f473577e318032b71c46c090ae70b44ea0264e29ad28b6eedef7115db1aafba3f1878a238a7269d533eb0b06ba07de17422b5e2ce9d70
7
+ data.tar.gz: 1ed596dba1c9b64292a3a517f823163fb2d1e36261b35fc96812be85abd919ab6b4d537da353114f45f799e0d33a47edd313d0731ec35c014d7cabd86ea8b396
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## [1.0.0] - 2026-03-21
4
+
5
+ - Initial open-source release (see root CHANGELOG.md for details)
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 ncri
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # Yes Core
2
+
3
+ Core event sourcing framework providing the aggregate DSL, commands, events, read models, and supporting infrastructure for the [Yes](https://github.com/yousty/yes) framework.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'yes-core'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ ```bash
16
+ bundle install
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ See the [root README](../README.md) for the full DSL documentation and usage examples.
22
+
23
+ ## Development
24
+
25
+ ### Prerequisites
26
+
27
+ - Docker and Docker Compose
28
+ - Ruby >= 3.2.0
29
+ - Bundler
30
+
31
+ ### Setup
32
+
33
+ Start PostgreSQL from the **repository root**:
34
+
35
+ ```shell
36
+ docker compose up -d
37
+ ```
38
+
39
+ Install dependencies:
40
+
41
+ ```shell
42
+ bundle install
43
+ ```
44
+
45
+ Set up the EventStore database:
46
+
47
+ ```shell
48
+ PG_EVENTSTORE_URI="postgresql://postgres:postgres@localhost:5532/eventstore_test" bundle exec rake pg_eventstore:create pg_eventstore:migrate
49
+ ```
50
+
51
+ Set up the test database:
52
+
53
+ ```shell
54
+ RAILS_ENV=test bundle exec rake db:create db:migrate
55
+ ```
56
+
57
+ ### Running Specs
58
+
59
+ ```shell
60
+ bundle exec rspec
61
+ ```
62
+
63
+ ## Contributing
64
+
65
+ Bug reports and pull requests are welcome on GitHub at https://github.com/yousty/yes.
66
+
67
+ ## License
68
+
69
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Core
5
+ module ActiveJobSerializers
6
+ # ActiveJob serializer for CommandGroup objects.
7
+ class CommandGroupSerializer < ActiveJob::Serializers::ObjectSerializer
8
+ # @param argument [Object] the argument to check
9
+ # @return [Boolean] true if the argument can be serialized
10
+ def serialize?(argument)
11
+ argument.is_a? Yes::Core::Commands::Group
12
+ end
13
+
14
+ # @param command_group [Yes::Core::Commands::Group] the command group to serialize
15
+ # @return [Hash] the serialized representation
16
+ def serialize(command_group)
17
+ super(command_group.to_h.merge(_type: command_group.class.name))
18
+ end
19
+
20
+ # @param hash [Hash] the serialized representation
21
+ # @return [Yes::Core::Commands::Group] the deserialized command group
22
+ def deserialize(hash)
23
+ symbolized_hash = hash.deep_symbolize_keys
24
+ Object.const_get(symbolized_hash[:_type]).new(symbolized_hash.except(:_aj_serialized, :_type))
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Core
5
+ module ActiveJobSerializers
6
+ # ActiveJob serializer for Dry::Struct objects (including Commands).
7
+ class DryStructSerializer < ActiveJob::Serializers::ObjectSerializer
8
+ # @param argument [Object] the argument to check
9
+ # @return [Boolean] true if the argument can be serialized
10
+ def serialize?(argument)
11
+ argument.is_a? Dry::Struct
12
+ end
13
+
14
+ # @param dry_struct [Dry::Struct] the dry struct to serialize
15
+ # @return [Hash] the serialized representation
16
+ def serialize(dry_struct)
17
+ super(dry_struct.attributes.merge(_type: dry_struct.class.name).as_json)
18
+ end
19
+
20
+ # @param hash [Hash] the serialized representation
21
+ # @return [Dry::Struct] the deserialized object
22
+ def deserialize(hash)
23
+ symbolized_hash = hash.deep_symbolize_keys
24
+ object = Object.const_get(symbolized_hash[:_type])
25
+
26
+ if object < Yes::Core::Command
27
+ deserialize_command(symbolized_hash, object)
28
+ else
29
+ object.new(symbolized_hash.except(:_aj_serialized, :_type))
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ # @param symbolized_hash [Hash] the symbolized hash
36
+ # @param object [Class] the command class
37
+ # @return [Yes::Core::Command] the deserialized command
38
+ def deserialize_command(symbolized_hash, object)
39
+ if symbolized_hash[:transaction].is_a?(Hash) && symbolized_hash[:transaction][:otl_contexts].present?
40
+ symbolized_hash[:transaction] = Yes::Core::TransactionDetails.new(
41
+ **symbolized_hash[:transaction].except(:otl_contexts),
42
+ otl_contexts: Yes::Core::TransactionDetailsTypes::OtlContexts.new(
43
+ symbolized_hash[:transaction][:otl_contexts]
44
+ )
45
+ )
46
+
47
+ return object.new(symbolized_hash.except(:_aj_serialized, :_type))
48
+ end
49
+
50
+ symbolized_hash[:transaction] = Yes::Core::TransactionDetails.new(symbolized_hash[:transaction]) if symbolized_hash[:transaction].is_a?(Hash)
51
+
52
+ object.new(symbolized_hash.except(:_aj_serialized, :_type))
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Core
5
+ class Aggregate
6
+ # Provides draftable functionality for aggregates
7
+ #
8
+ # @example Include in an aggregate
9
+ # class UserAggregate < Yes::Core::Aggregate
10
+ # draftable
11
+ # end
12
+ module Draftable
13
+ extend ActiveSupport::Concern
14
+
15
+ included do
16
+ class << self
17
+ attr_accessor :_draft_context, :_draft_aggregate, :_changes_read_model_name,
18
+ :_draft_foreign_key, :_is_draftable, :_changes_read_model_public
19
+ end
20
+ end
21
+
22
+ class_methods do
23
+ # Configures the aggregate as draftable
24
+ #
25
+ # @param draft_aggregate [Hash, nil] The draft aggregate configuration with :context and :aggregate keys
26
+ # @param changes_read_model [String, Symbol, nil] The changes read model name (defaults to "<read_model>_change")
27
+ # @param changes_read_model_public [Boolean] Whether the changes read model should be public via read API (default: true)
28
+ # @return [void]
29
+ #
30
+ # @example Use defaults
31
+ # draftable
32
+ #
33
+ # @example Override all parameters
34
+ # draftable draft_aggregate: { context: 'ApprenticeshipPresentation', aggregate: 'MyAggregateDraft' },
35
+ # changes_read_model: 'custom_change', changes_read_model_public: false
36
+ #
37
+ # @example Override only context
38
+ # draftable draft_aggregate: { context: 'CustomContext' }
39
+ #
40
+ # @example Override only aggregate
41
+ # draftable draft_aggregate: { aggregate: 'CustomDraft' }
42
+ #
43
+ # @example Override only changes_read_model
44
+ # draftable changes_read_model: :article_change
45
+ #
46
+ # @example Make changes read model private
47
+ # draftable changes_read_model_public: false
48
+ def draftable(draft_aggregate: nil, changes_read_model: nil, changes_read_model_public: true)
49
+ self._is_draftable = true
50
+
51
+ draft_config = draft_aggregate || {}
52
+ self._draft_context = draft_config[:context] || context
53
+ self._draft_aggregate = draft_config[:aggregate] || "#{aggregate}Draft"
54
+
55
+ self._changes_read_model_name = if changes_read_model
56
+ changes_read_model.to_s
57
+ else
58
+ "#{read_model_name}_change"
59
+ end
60
+
61
+ self._changes_read_model_public = changes_read_model_public
62
+ end
63
+
64
+ # Checks if the aggregate is draftable
65
+ #
66
+ # @return [Boolean] true if draftable, false otherwise
67
+ def draftable?
68
+ _is_draftable == true
69
+ end
70
+
71
+ # Returns the draft context
72
+ #
73
+ # @return [String, nil] the draft context
74
+ def draft_context
75
+ _draft_context
76
+ end
77
+
78
+ # Returns the draft aggregate name
79
+ #
80
+ # @return [String, nil] the draft aggregate name
81
+ def draft_aggregate
82
+ _draft_aggregate
83
+ end
84
+
85
+ # Returns the changes read model name
86
+ #
87
+ # @return [String, nil] the changes read model name
88
+ def changes_read_model_name
89
+ _changes_read_model_name
90
+ end
91
+
92
+ # Returns the changes read model class
93
+ #
94
+ # @return [Class, nil] the changes read model class
95
+ def changes_read_model_class
96
+ return nil unless changes_read_model_name
97
+
98
+ Yes::Core::Aggregate::Dsl::ClassResolvers::ReadModel.new(
99
+ changes_read_model_name,
100
+ context,
101
+ aggregate,
102
+ draft: true
103
+ ).call
104
+ end
105
+
106
+ # Returns whether the changes read model is public
107
+ #
108
+ # @return [Boolean] true if public, false otherwise
109
+ def changes_read_model_public?
110
+ _changes_read_model_public.nil? || _changes_read_model_public
111
+ end
112
+
113
+ private
114
+
115
+ def draft_aggregate_class
116
+ "#{draft_context}::#{draft_aggregate}::Aggregate".constantize
117
+ end
118
+
119
+ def draft_read_model_class
120
+ "::#{draft_aggregate}".constantize
121
+ end
122
+
123
+ def main_changes_model_foreign_key
124
+ if draft_aggregate_class.respond_to?(:changes_read_model_foreign_key)
125
+ draft_aggregate_class.changes_read_model_foreign_key
126
+ else
127
+ draft_aggregate.to_s.underscore.sub(/_(draft|batch)$/, '_change_id')
128
+ end
129
+ end
130
+ end
131
+
132
+ # Override read_model to return changes read model when initialized as draft
133
+ #
134
+ # @return [ApplicationRecord] The read model or draft read model instance
135
+ def read_model
136
+ return super unless draft? && self.class.draftable?
137
+
138
+ @read_model ||= changes_read_model_class.find_or_create_by(id:)
139
+ end
140
+
141
+ # Updates the read model and handles draft aggregate updates
142
+ #
143
+ # @param attributes [Hash] The attributes to update
144
+ # @return [Boolean] true if successful
145
+ def update_read_model(attributes)
146
+ result = super
147
+
148
+ update_draft_aggregate if self.class.draftable? && draft?
149
+
150
+ result
151
+ end
152
+
153
+ # Checks if this instance is a draft
154
+ #
155
+ # @return [Boolean] true if draft, false otherwise
156
+ def draft?
157
+ @draft == true
158
+ end
159
+
160
+ private
161
+
162
+ # Returns the changes read model class
163
+ #
164
+ # @return [Class] the changes read model class
165
+ def changes_read_model_class
166
+ @changes_read_model_class ||= resolve_changes_read_model_class
167
+ end
168
+
169
+ # Resolves the changes read model class
170
+ #
171
+ # @return [Class] the resolved changes read model class
172
+ def resolve_changes_read_model_class
173
+ changes_read_model_name = self.class.changes_read_model_name
174
+ return self.class.read_model_class unless changes_read_model_name
175
+
176
+ Yes::Core::Aggregate::Dsl::ClassResolvers::ReadModel.new(
177
+ changes_read_model_name,
178
+ self.class.context,
179
+ self.class.aggregate
180
+ ).call
181
+ end
182
+
183
+ # Updates the connected draft aggregate's read model
184
+ #
185
+ # @return [void]
186
+ def update_draft_aggregate
187
+ main_id_method = :"#{self.class.read_model_name}_id"
188
+
189
+ # Check if the changes read model has a foreign key that relates it to the main read model
190
+ #
191
+ # e.g. ApprenticeshipDraft is a draft for Apprenticeship, so main_read_model_id is :apprenticeship_id
192
+ #
193
+ return unless read_model.respond_to?(main_id_method)
194
+
195
+ main_changes_model_id = read_model.send(main_id_method)
196
+
197
+ # e.g. ::ApprenticeshipBatch.find_by(apprenticeship_edit_template_id: apprenticeship_id)&.state_draft!
198
+ self.class.send(:draft_read_model_class).find_by(
199
+ self.class.send(:main_changes_model_foreign_key) => main_changes_model_id
200
+ )&.state_draft!
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,37 @@
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 an attribute definition in an aggregate
8
+ #
9
+ # @example
10
+ # AttributeData.new(:name, :string, MyAggregate, context: 'users', aggregate: 'user')
11
+ #
12
+ class AttributeData
13
+ attr_reader :name, :type, :context_name,
14
+ :aggregate_name, :aggregate_class, :localized, :encrypted
15
+
16
+ # @param name [Symbol] The name of the attribute
17
+ # @param type [Symbol] The type of the attribute
18
+ # @param aggregate_class [Class] The aggregate class this attribute belongs to
19
+ # @param options [Hash] Additional options for the attribute
20
+ # @option options [String] :context The context name for the attribute
21
+ # @option options [String] :aggregate The aggregate name
22
+ # @option options [Boolean] :localized Whether the attribute is localized
23
+ # @option options [Boolean] :encrypted Whether the attribute should be encrypted
24
+ def initialize(name, type, aggregate_class, options = {})
25
+ @name = name
26
+ @type = type
27
+ @aggregate_class = aggregate_class
28
+ @context_name = options.delete(:context)
29
+ @aggregate_name = options.delete(:aggregate)
30
+ @localized = options.delete(:localized) || false
31
+ @encrypted = options.delete(:encrypted) || false
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Core
5
+ class Aggregate
6
+ module Dsl
7
+ # Factory class that creates the appropriate attribute definer based on the attribute type
8
+ #
9
+ # @example
10
+ # attribute_data = AttributeData.new(name: :name, type: :string, aggregate_class: User, validate: true)
11
+ # AttributeDefiner.new(attribute_data).call do
12
+ # guard :no_change do
13
+ # payload.name == name
14
+ # end
15
+ # end
16
+ #
17
+ class AttributeDefiner
18
+ # @return [AttributeData] the data object containing attribute configuration
19
+ attr_reader :attribute_data
20
+ private :attribute_data
21
+
22
+ # Initializes a new AttributeDefiner instance
23
+ #
24
+ # @param attribute_data [AttributeData] the data object containing attribute configuration
25
+ # @return [AttributeDefiner] a new instance of AttributeDefiner
26
+ def initialize(attribute_data)
27
+ @attribute_data = attribute_data
28
+ end
29
+
30
+ # Creates the appropriate definer and calls it to generate the necessary classes and methods
31
+ #
32
+ # @yield Block for defining guards and other attribute configurations
33
+ # @yieldreturn [void]
34
+ # @return [void]
35
+ def call(&)
36
+ definer_for_type.new(attribute_data).call(&)
37
+ end
38
+
39
+ private
40
+
41
+ # Returns the appropriate definer class based on the attribute type
42
+ #
43
+ # @return [Class] The definer class to use
44
+ def definer_for_type
45
+ case attribute_data.type
46
+ when :aggregate then AttributeDefiners::Aggregate
47
+ else AttributeDefiners::Standard
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ 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 AttributeDefiners
8
+ # Handles the definition and generation of aggregate attribute-related classes and methods
9
+ class Aggregate
10
+ # @return [AttributeData] the data object containing attribute configuration
11
+ attr_reader :attribute_data, :guard_evaluator_class
12
+
13
+ private :attribute_data, :guard_evaluator_class
14
+
15
+ # Initializes a new Base instance
16
+ #
17
+ # @param attribute_data [AttributeData] the data object containing attribute configuration
18
+ # @return [Base] a new instance of Base
19
+ def initialize(attribute_data)
20
+ @attribute_data = attribute_data
21
+ end
22
+
23
+ # Generates and registers all necessary classes for the attribute.
24
+ #
25
+ # @yield Block for defining guards and other attribute configurations
26
+ # @yieldreturn [void]
27
+ # @return [void]
28
+ def call
29
+ MethodDefiners::Attribute::AggregateAccessor.new(attribute_data).call
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ 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 AttributeDefiners
8
+ # Handles the definition and generation of standard attribute-related classes and methods
9
+ class Standard
10
+ # @return [AttributeData] the data object containing attribute configuration
11
+ attr_reader :attribute_data, :guard_evaluator_class
12
+
13
+ private :attribute_data, :guard_evaluator_class
14
+
15
+ # Initializes a new Base instance
16
+ #
17
+ # @param attribute_data [AttributeData] the data object containing attribute configuration
18
+ # @return [Base] a new instance of Base
19
+ def initialize(attribute_data)
20
+ @attribute_data = attribute_data
21
+ end
22
+
23
+ # Generates and registers all necessary classes for the attribute.
24
+ #
25
+ # @yield Block for defining guards and other attribute configurations
26
+ # @yieldreturn [void]
27
+ # @return [void]
28
+ def call
29
+ MethodDefiners::Attribute::Accessor.new(attribute_data).call
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Core
5
+ class Aggregate
6
+ module Dsl
7
+ # Handles the naming conventions for aggregate-related classes
8
+ #
9
+ # @example
10
+ # convention = ClassNameConvention.new(context: 'MyContext', aggregate: 'User')
11
+ # convention.command_class_name('change_name') # => "MyContext::User::Commands::ChangeName::Command"
12
+ #
13
+ class ClassNameConvention
14
+ # @param context [String] The context name
15
+ # @param aggregate [String] The aggregate name
16
+ def initialize(context:, aggregate:)
17
+ @context = context
18
+ @aggregate = aggregate
19
+ end
20
+
21
+ # Returns the conventional class name for a given type and name
22
+ #
23
+ # @param type [Symbol] The type of class (:command, :event, or :guard_evaluator, ...)
24
+ # @param name [Symbol, String] The name of the class
25
+ # @return [String] The conventional class name
26
+ def class_name_for(type, name)
27
+ send(:"#{type}_class_name", name)
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :context, :aggregate
33
+
34
+ def command_class_name(name)
35
+ "#{context}::#{aggregate}::Commands::#{name.to_s.camelize}::Command"
36
+ end
37
+
38
+ def event_class_name(name)
39
+ "#{context}::#{aggregate}::Events::#{name.to_s.camelize}"
40
+ end
41
+
42
+ def guard_evaluator_class_name(name)
43
+ "#{context}::#{aggregate}::Commands::#{name.to_s.camelize}::GuardEvaluator"
44
+ end
45
+
46
+ def state_updater_class_name(name)
47
+ "#{context}::#{aggregate}::Commands::#{name.to_s.camelize}::StateUpdater"
48
+ end
49
+
50
+ def read_model_class_name(name)
51
+ name.to_s.camelize
52
+ end
53
+
54
+ def read_model_filter_class_name(name)
55
+ "ReadModels::#{name.to_s.camelize}::Filter"
56
+ end
57
+
58
+ def read_model_serializer_class_name(name)
59
+ "ReadModels::#{name.to_s.camelize}::Serializers::#{name.to_s.camelize}"
60
+ end
61
+
62
+ # Returns the conventional authorizer class name.
63
+ #
64
+ # If +name+ is nil, it refers to the aggregate-level authorizer:
65
+ # <Context>::<Aggregate>::Commands::<Aggregate>Authorizer
66
+ # Otherwise it refers to a command-level authorizer:
67
+ # <Context>::<Aggregate>::Commands::<CommandName>::Authorizer
68
+ #
69
+ # @param name [Symbol, String, nil] the command name (optional)
70
+ # @return [String]
71
+ def authorizer_class_name(name)
72
+ return "#{context}::#{aggregate}::Commands::#{aggregate}Authorizer" if name.nil? || name.to_s.empty?
73
+
74
+ "#{context}::#{aggregate}::Commands::#{name.to_s.camelize}::Authorizer"
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end