Class: Yes::Core::Aggregate
- Inherits:
-
Object
- Object
- Yes::Core::Aggregate
- Includes:
- Draftable, HasAuthorizer, HasReadModel
- Defined in:
- lib/yes/core/aggregate.rb,
lib/yes/core/aggregate/draftable.rb,
lib/yes/core/aggregate/has_authorizer.rb,
lib/yes/core/aggregate/has_read_model.rb,
lib/yes/core/aggregate/dsl/command_data.rb,
lib/yes/core/aggregate/dsl/attribute_data.rb,
lib/yes/core/aggregate/dsl/command_definer.rb,
lib/yes/core/aggregate/read_model_rebuilder.rb,
lib/yes/core/aggregate/dsl/attribute_definer.rb,
lib/yes/core/aggregate/dsl/constant_resolver.rb,
lib/yes/core/aggregate/dsl/class_resolvers/base.rb,
lib/yes/core/aggregate/dsl/class_name_convention.rb,
lib/yes/core/aggregate/shared_read_model_rebuilder.rb,
lib/yes/core/aggregate/dsl/command_shortcut_expander.rb,
lib/yes/core/aggregate/dsl/class_resolvers/authorizer.rb,
lib/yes/core/aggregate/dsl/class_resolvers/read_model.rb,
lib/yes/core/aggregate/dsl/attribute_definers/standard.rb,
lib/yes/core/aggregate/dsl/attribute_definers/aggregate.rb,
lib/yes/core/aggregate/dsl/class_resolvers/command/base.rb,
lib/yes/core/aggregate/dsl/method_definers/command/base.rb,
lib/yes/core/aggregate/dsl/class_resolvers/command/event.rb,
lib/yes/core/aggregate/dsl/method_definers/attribute/base.rb,
lib/yes/core/aggregate/dsl/class_resolvers/command/command.rb,
lib/yes/core/aggregate/dsl/method_definers/command/command.rb,
lib/yes/core/aggregate/dsl/class_resolvers/read_model_filter.rb,
lib/yes/core/aggregate/dsl/class_resolvers/command/authorizer.rb,
lib/yes/core/aggregate/dsl/method_definers/attribute/accessor.rb,
lib/yes/core/aggregate/dsl/method_definers/command/can_command.rb,
lib/yes/core/aggregate/dsl/class_resolvers/command/state_updater.rb,
lib/yes/core/aggregate/dsl/class_resolvers/read_model_serializer.rb,
lib/yes/core/aggregate/dsl/class_resolvers/command/guard_evaluator.rb,
lib/yes/core/aggregate/dsl/class_resolvers/command/cerbos_authorizer.rb,
lib/yes/core/aggregate/dsl/class_resolvers/command/simple_authorizer.rb,
lib/yes/core/aggregate/dsl/class_resolvers/command/authorizer_factory.rb,
lib/yes/core/aggregate/dsl/method_definers/attribute/aggregate_accessor.rb
Overview
The Aggregate class represents a core entity in the eventsourcing system. It provides functionality for managing event sourcing patterns including:
-
Attribute management with automatic command and event generation
-
Parent-child aggregate relationships
-
Read model associations
-
Context management
Defined Under Namespace
Modules: Draftable, Dsl, HasAuthorizer, HasReadModel Classes: ReadModelRebuilder, SharedReadModelRebuilder
Class Attribute Summary collapse
-
._primary_context ⇒ String?
readonly
The primary context name for this aggregate.
-
.removable_config ⇒ Hash{Symbol => Object}?
readonly
Returns the removable configuration for the aggregate, or nil if Aggregate.removable was never called.
Instance Attribute Summary collapse
- #id ⇒ Object readonly
Class Method Summary collapse
-
.aggregate ⇒ String
Returns the aggregate name without namespace and “Aggregate” suffix.
-
.attribute(name, type, **options) { ... } ⇒ Object
Defines an attribute on the aggregate which creates corresponding command, event and handler.
-
.attribute_options ⇒ Hash
The attribute options (localized, encrypted, etc.).
-
.attributes ⇒ Hash
The attributes defined on this aggregate.
-
.command(*args, **kwargs) ⇒ Object
Defines a command on the aggregate which creates corresponding command and event classes.
-
.commands ⇒ Hash
The commands defined on this aggregate.
-
.context ⇒ String
Returns the context namespace for the aggregate.
-
.inherited(subclass) ⇒ void
Hook that runs when a class inherits from Aggregate.
-
.parent(name, **options) { ... } ⇒ void
Defines a parent aggregate and automatically registers a corresponding Assign command together with a corresponding attribute.
-
.parent_aggregates ⇒ Hash<Symbol, Hash>
Retrieves or initializes the parent_aggregates hash.
-
.primary_context(context) ⇒ void
Sets the primary context for the aggregate.
-
.removable(attr_name: :removed_at, not_removed_guards: true) { ... } ⇒ void
Defines a default removal behavior for the aggregate.
Instance Method Summary collapse
-
#commands ⇒ Hash<Symbol, Array<Symbol>>
Returns a list of commands that can be executed on this aggregate with their associated events.
-
#event_revision ⇒ Integer
Returns the stream revision number of the latest event.
-
#events ⇒ Enumerator<PgEventstore::Event>
Returns the events for the aggregate.
-
#initialize(id = SecureRandom.uuid, draft: false) ⇒ Yes::Core::Aggregate
constructor
Initializes a new aggregate instance.
-
#latest_event ⇒ PgEventstore::Event?
Retrieves the most recent event from the aggregate’s event stream.
-
#reload ⇒ Yes::Core::Aggregate
Reloads the aggregate and its read model.
Methods included from Draftable
#draft?, #read_model, #update_read_model
Methods included from HasReadModel
#init_revision_from_stream, #read_model, #rebuild_read_model, #remove_read_model, #revision, #revision_column, #update_read_model
Constructor Details
#initialize(id = SecureRandom.uuid, draft: false) ⇒ Yes::Core::Aggregate
Initializes a new aggregate instance
381 382 383 384 385 386 387 388 389 390 391 392 |
# File 'lib/yes/core/aggregate.rb', line 381 def initialize(id = SecureRandom.uuid, draft: false) validate_draft_initialization(draft) @id = id @draft = draft @command_utilities = Utils::CommandUtils.new( context: self.class.context, aggregate: self.class.aggregate, aggregate_id: @id ) end |
Class Attribute Details
._primary_context ⇒ String? (readonly)
Returns The primary context name for this aggregate.
54 55 56 |
# File 'lib/yes/core/aggregate.rb', line 54 def _primary_context @_primary_context end |
.removable_config ⇒ Hash{Symbol => Object}? (readonly)
Returns the removable configuration for the aggregate, or nil if removable was never called.
177 178 179 |
# File 'lib/yes/core/aggregate.rb', line 177 def removable_config @removable_config end |
Instance Attribute Details
#id ⇒ Object (readonly)
44 45 46 |
# File 'lib/yes/core/aggregate.rb', line 44 def id @id end |
Class Method Details
.aggregate ⇒ String
Returns the aggregate name without namespace and “Aggregate” suffix
321 322 323 |
# File 'lib/yes/core/aggregate.rb', line 321 def aggregate name.to_s.split('::')[-2] end |
.attribute(name, type, **options) { ... } ⇒ Object
Defines an attribute on the aggregate which creates corresponding command, event and handler
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 |
# File 'lib/yes/core/aggregate.rb', line 213 def attribute(name, type, **, &) raise 'Aggregate attribute definition with command: true is not allowed' if type == :aggregate && [:command] @attributes ||= {} @attributes[name] = type @attribute_options ||= {} @attribute_options[name] = .slice(:localized) = .merge(context:, aggregate:) Dsl::AttributeDefiner.new( Dsl::AttributeData.new(name, type, self, ) ).call command(:change, name, type, &) if [:command] end |
.attribute_options ⇒ Hash
Returns The attribute options (localized, encrypted, etc.).
331 332 333 |
# File 'lib/yes/core/aggregate.rb', line 331 def @attribute_options ||= {} end |
.attributes ⇒ Hash
Returns The attributes defined on this aggregate.
326 327 328 |
# File 'lib/yes/core/aggregate.rb', line 326 def attributes @attributes ||= {} end |
.command(name) { ... } ⇒ Object .command(publish) ⇒ void .command(change, attribute, **options) ⇒ void .command(enable, attribute, **options) ⇒ void .command(toggle_names, attribute) ⇒ void
Defines a command on the aggregate which creates corresponding command and event classes
All overloads accept a ‘skip_default_guards:` keyword argument carrying an array of default-guard symbols (currently only `:not_removed` — see removable) that should not be auto-applied to the command. Defaults to `[]`.
294 295 296 297 298 299 300 301 302 303 304 305 |
# File 'lib/yes/core/aggregate.rb', line 294 def command(*args, **kwargs, &) skip_default_guards = kwargs.delete(:skip_default_guards) || [] base_case = Dsl::CommandShortcutExpander.base_case?(*args, **kwargs, &) return handle_command_shortcut(*args, skip_default_guards:, **kwargs, &) unless base_case name = args.first @commands ||= {} command_data = Dsl::CommandData.new(name, self, { context:, aggregate:, skip_default_guards: }) @commands[name] = command_data Dsl::CommandDefiner.new(command_data).call(&) end |
.commands ⇒ Hash
Returns The commands defined on this aggregate.
336 337 338 |
# File 'lib/yes/core/aggregate.rb', line 336 def commands @commands ||= {} end |
.context ⇒ String
Returns the context namespace for the aggregate
312 313 314 |
# File 'lib/yes/core/aggregate.rb', line 312 def context name.to_s.split('::').first end |
.inherited(subclass) ⇒ void
This method returns an undefined value.
Hook that runs when a class inherits from Aggregate
59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
# File 'lib/yes/core/aggregate.rb', line 59 def inherited(subclass) super # Add an "end of definition" hook using at_exit # Setting up read model classes is done here, because it needs to be done after # the class definition is complete. TracePoint.new(:end) do |tp| if tp.self == subclass subclass.setup_read_model_classes if subclass.read_model_enabled? subclass. tp.disable end end.enable end |
.parent(name, **options) { ... } ⇒ void
This method returns an undefined value.
Defines a parent aggregate and automatically registers a corresponding Assign command together with a corresponding attribute.
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/yes/core/aggregate.rb', line 88 def parent(name, **, &) parent_aggregates[name] = attribute name, :aggregate return unless .fetch(:command, true) skip_default_guards = [:skip_default_guards] || [] command :"assign_#{name}", skip_default_guards: do payload "#{name}_id": :uuid guard(:no_change) { public_send(:"#{name}_id") != payload.public_send(:"#{name}_id") } instance_eval(&) if block_given? end end |
.parent_aggregates ⇒ Hash<Symbol, Hash>
Retrieves or initializes the parent_aggregates hash.
109 110 111 |
# File 'lib/yes/core/aggregate.rb', line 109 def parent_aggregates @parent_aggregates ||= {} end |
.primary_context(context) ⇒ void
This method returns an undefined value.
Sets the primary context for the aggregate.
183 184 185 |
# File 'lib/yes/core/aggregate.rb', line 183 def primary_context(context) @_primary_context = context end |
.removable(attr_name: :removed_at, not_removed_guards: true) { ... } ⇒ void
This method returns an undefined value.
Defines a default removal behavior for the aggregate.
In addition to defining the ‘:remove` command, `removable` records aggregate-level configuration that the CommandHandling::GuardEvaluator reads at runtime to **auto-block every other command on the aggregate** while the removal attribute is set. The auto-block fires before any registered guard (including the auto-injected `:no_change`), so post-remove mutations consistently raise `GuardEvaluator::InvalidTransition` with the i18n message under `aggregates.<context>.<aggregate>.commands.<command>.guards.not_removed.error`. The `:remove` command itself is exempt and remains gated only by `:no_change`.
The auto-block is order-independent: ‘removable` may be declared before or after the other commands on the aggregate.
‘attr_name` must correspond to an attribute readable on the aggregate (the macro auto-defines it as `:datetime` when missing).
160 161 162 163 164 165 166 167 168 169 |
# File 'lib/yes/core/aggregate.rb', line 160 def removable(attr_name: :removed_at, not_removed_guards: true, &) attribute attr_name, :datetime unless attributes.key?(attr_name) @removable_config = { attr_name:, not_removed_guards: } command :remove, skip_default_guards: %i[not_removed] do guard(:no_change) { !public_send(attr_name) } update_state { method(attr_name).call { Time.current } } instance_eval(&) if block_given? end end |
Instance Method Details
#commands ⇒ Hash<Symbol, Array<Symbol>>
Returns a list of commands that can be executed on this aggregate with their associated events
435 436 437 438 439 440 441 442 |
# File 'lib/yes/core/aggregate.rb', line 435 def commands mappings = Yes::Core.configuration.command_event_mappings( self.class.context, self.class.aggregate ) mappings.sort.to_h end |
#event_revision ⇒ Integer
Returns the stream revision number of the latest event
421 422 423 |
# File 'lib/yes/core/aggregate.rb', line 421 def event_revision latest_event.stream_revision end |
#events ⇒ Enumerator<PgEventstore::Event>
Returns the events for the aggregate
404 405 406 407 408 |
# File 'lib/yes/core/aggregate.rb', line 404 def events PgEventstore.client.read_paginated( command_utilities.build_stream(metadata: { draft: draft? }), options: { direction: 'Forwards' } ) end |
#latest_event ⇒ PgEventstore::Event?
Retrieves the most recent event from the aggregate’s event stream
412 413 414 415 416 |
# File 'lib/yes/core/aggregate.rb', line 412 def latest_event PgEventstore.client.read( command_utilities.build_stream(metadata: { draft: draft? }), options: { max_count: 1, direction: :desc } ).first end |
#reload ⇒ Yes::Core::Aggregate
Reloads the aggregate and its read model
396 397 398 399 400 |
# File 'lib/yes/core/aggregate.rb', line 396 def reload read_model&.reload self end |