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