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/command_group_data.rb,
lib/yes/core/aggregate/dsl/class_resolvers/base.rb,
lib/yes/core/aggregate/dsl/class_name_convention.rb,
lib/yes/core/aggregate/dsl/command_group_definer.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/class_resolvers/command_group/base.rb,
lib/yes/core/aggregate/dsl/method_definers/attribute/accessor.rb,
lib/yes/core/aggregate/dsl/method_definers/command_group/base.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/command_group/command.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/command_group/command_group.rb,
lib/yes/core/aggregate/dsl/method_definers/attribute/aggregate_accessor.rb,
lib/yes/core/aggregate/dsl/class_resolvers/command_group/guard_evaluator.rb,
lib/yes/core/aggregate/dsl/method_definers/command_group/can_command_group.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.
-
.command_group(name) { ... } ⇒ void
Defines a command_group on the aggregate.
-
.command_groups ⇒ Hash
The command groups defined on this aggregate.
-
.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.
-
.validate_command_groups! ⇒ void
Validates that each command_group references commands actually defined on this 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
431 432 433 434 435 436 437 438 439 440 441 442 |
# File 'lib/yes/core/aggregate.rb', line 431 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.
178 179 180 |
# File 'lib/yes/core/aggregate.rb', line 178 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
322 323 324 |
# File 'lib/yes/core/aggregate.rb', line 322 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
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 |
# File 'lib/yes/core/aggregate.rb', line 214 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.).
332 333 334 |
# File 'lib/yes/core/aggregate.rb', line 332 def @attribute_options ||= {} end |
.attributes ⇒ Hash
Returns The attributes defined on this aggregate.
327 328 329 |
# File 'lib/yes/core/aggregate.rb', line 327 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 `[]`.
295 296 297 298 299 300 301 302 303 304 305 306 |
# File 'lib/yes/core/aggregate.rb', line 295 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 |
.command_group(name) { ... } ⇒ void
This method returns an undefined value.
Defines a command_group on the aggregate.
A command_group declares a compound action that runs multiple existing aggregate commands in declaration order, atomically, with the sub-commands’ guards bypassed. The group itself has its own, leaner guard set declared inside the block via ‘guard :name`.
361 362 363 364 365 366 |
# File 'lib/yes/core/aggregate.rb', line 361 def command_group(name, &) @command_groups ||= {} group_data = Dsl::CommandGroupData.new(name, self, context:, aggregate:) @command_groups[name] = group_data Dsl::CommandGroupDefiner.new(group_data).call(&) end |
.command_groups ⇒ Hash
Returns The command groups defined on this aggregate.
369 370 371 |
# File 'lib/yes/core/aggregate.rb', line 369 def command_groups @command_groups ||= {} end |
.commands ⇒ Hash
Returns The commands defined on this aggregate.
337 338 339 |
# File 'lib/yes/core/aggregate.rb', line 337 def commands @commands ||= {} end |
.context ⇒ String
Returns the context namespace for the aggregate
313 314 315 |
# File 'lib/yes/core/aggregate.rb', line 313 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 73 |
# 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. subclass.validate_command_groups! 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.
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/yes/core/aggregate.rb', line 89 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.
110 111 112 |
# File 'lib/yes/core/aggregate.rb', line 110 def parent_aggregates @parent_aggregates ||= {} end |
.primary_context(context) ⇒ void
This method returns an undefined value.
Sets the primary context for the aggregate.
184 185 186 |
# File 'lib/yes/core/aggregate.rb', line 184 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).
161 162 163 164 165 166 167 168 169 170 |
# File 'lib/yes/core/aggregate.rb', line 161 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 |
.validate_command_groups! ⇒ void
This method returns an undefined value.
Validates that each command_group references commands actually defined on this aggregate. Called from the end-of-class TracePoint hook set up in inherited.
379 380 381 382 383 384 385 386 387 388 |
# File 'lib/yes/core/aggregate.rb', line 379 def validate_command_groups! command_groups.each do |group_name, data| unknown = data.sub_command_names - commands.keys next if unknown.empty? raise Dsl::CommandGroupDefiner::UnknownSubCommandError, "command_group :#{group_name} on #{name} references unknown commands: " \ "#{unknown.join(', ')}. Define them with `command :<name>` before using them." 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
485 486 487 488 489 490 491 492 |
# File 'lib/yes/core/aggregate.rb', line 485 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
471 472 473 |
# File 'lib/yes/core/aggregate.rb', line 471 def event_revision latest_event.stream_revision end |
#events ⇒ Enumerator<PgEventstore::Event>
Returns the events for the aggregate
454 455 456 457 458 |
# File 'lib/yes/core/aggregate.rb', line 454 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
462 463 464 465 466 |
# File 'lib/yes/core/aggregate.rb', line 462 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
446 447 448 449 450 |
# File 'lib/yes/core/aggregate.rb', line 446 def reload read_model&.reload self end |