Class: Yes::Core::Aggregate

Inherits:
Object
  • Object
show all
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

Examples:

Define an aggregate with attributes

class UserAggregate < Yes::Core::Aggregate
  primary_context 'Users'
  attribute :email, :email
  attribute :name, :string
end

Define an aggregate with a parent

class ProfileAggregate < Yes::Core::Aggregate
  parent :user do
    guard(:user_exists) { payload.user.present? }
    guard(:not_removed) { trashed_at.blank? }
  end
  attribute :bio, :string
end

Define an aggregate with a command

class CompanyAggregate < Yes::Core::Aggregate
  primary_context 'Companies'

  command :assign_user do
    payload user_id: :uuid

    guard :user_already_assigned do
      user_id.present?
    end
  end
end

Author:

  • Nico Ritsche

Since:

  • 0.1.0

Defined Under Namespace

Modules: Draftable, Dsl, HasAuthorizer, HasReadModel Classes: ReadModelRebuilder, SharedReadModelRebuilder

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

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

Examples:

Backwards compatibility - single ID parameter

Aggregate.new(some_id)

With draft as keyword argument

Aggregate.new(draft: true)

With positional id and draft keyword

Aggregate.new(some_id, draft: true)

Parameters:

  • id (String) (defaults to: SecureRandom.uuid)

    The aggregate ID (optional, defaults to SecureRandom.uuid)

  • draft (Boolean) (defaults to: false)

    Whether this instance is being edited as a draft (default: false)

Since:

  • 0.1.0



327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/yes/core/aggregate.rb', line 327

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_contextString? (readonly)

Returns The primary context name for this aggregate.

Returns:

  • (String, nil)

    The primary context name for this aggregate

Since:

  • 0.1.0



54
55
56
# File 'lib/yes/core/aggregate.rb', line 54

def _primary_context
  @_primary_context
end

Instance Attribute Details

#idObject (readonly)

Since:

  • 0.1.0



44
45
46
# File 'lib/yes/core/aggregate.rb', line 44

def id
  @id
end

Class Method Details

.aggregateString

Returns the aggregate name without namespace and “Aggregate” suffix

Examples:

Users::User::Aggregate.aggregate #=> "User"

Returns:

  • (String)

    The aggregate name

Since:

  • 0.1.0



267
268
269
# File 'lib/yes/core/aggregate.rb', line 267

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

Examples:

Define a string attribute (without command)

attribute :name, :string

Define an aggregate attribute (without command)

attribute :location, :aggregate

Define an email attribute with command

attribute :email, :email, command: true

Define an attribute with command and guards

attribute :first_name, :string, command: true do
  guard :something do
    first_name == 'John'
  end
end

Define a localized attribute

attribute :description, :string, command: true, localized: true

Parameters:

  • name (Symbol)

    name of the attribute

  • type (Symbol)

    type of the attribute (e.g., :string, :email, :uuid)

  • options (Hash)

    additional options for the attribute

Yields:

  • Block for defining guards and other attribute configurations

Yield Returns:

  • (void)

Since:

  • 0.1.0



171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/yes/core/aggregate.rb', line 171

def attribute(name, type, **options, &)
  raise 'Aggregate attribute definition with command: true is not allowed' if type == :aggregate && options[:command]

  @attributes ||= {}
  @attributes[name] = type

  @attribute_options ||= {}
  @attribute_options[name] = options.slice(:localized)

  options = options.merge(context:, aggregate:)
  Dsl::AttributeDefiner.new(
    Dsl::AttributeData.new(name, type, self, options)
  ).call

  command(:change, name, type, &) if options[:command]
end

.attribute_optionsHash

Returns The attribute options (localized, encrypted, etc.).

Returns:

  • (Hash)

    The attribute options (localized, encrypted, etc.)

Since:

  • 0.1.0



277
278
279
# File 'lib/yes/core/aggregate.rb', line 277

def attribute_options
  @attribute_options ||= {}
end

.attributesHash

Returns The attributes defined on this aggregate.

Returns:

  • (Hash)

    The attributes defined on this aggregate

Since:

  • 0.1.0



272
273
274
# File 'lib/yes/core/aggregate.rb', line 272

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

Examples:

Define a basic command

command :assign_user

Define a command with custom payload and guards

command :assign_user do
  payload user_id: :uuid

  guard :user_already_assigned do
    user_id.present?
  end

  event :user_assigned
end

Define change command and an attribute

command :change, :age, :integer, localized: true

Define set flag to true command an an attribute

command :activate, :dropout, attribute: :dropout_enabled

Define set of toggle commands an an attribute

command [:enable, :disable], :dropout

Define publish command an published attribute

command :publish

Overloads:

  • .command(name) { ... } ⇒ Object

    Parameters:

    • name (Symbol)

      name of the command

    Yields:

    • Block for defining payload, guards, and other command configurations

    Yield Returns:

    • (void)
  • .command(publish) ⇒ void

    This method returns an undefined value.

    Parameters:

    • publish (Symbol)

      passing :publish as a name will generate published attribute and publish command

  • .command(change, attribute, **options) ⇒ void

    This method returns an undefined value.

    Parameters:

    • change (Symbol)

      passing :change as a name will generate a change command and an attribute

    • attribute (Symbol)

      attribute name

    • options (Hash)

      additional options for the attribute

  • .command(enable, attribute, **options) ⇒ void

    This method returns an undefined value.

    Parameters:

    • enable (Symbol)

      passing :enable or :activate as a name will generate a flag set to true command and an attribute

    • attribute (Symbol)

      attribute name

    • options (Hash)

      additional options for the attribute

  • .command(toggle_names, attribute) ⇒ void

    This method returns an undefined value.

    Parameters:

    • toggle_names (Array<Symbol>)

      toggle command names to be generated

    • attribute (Symbol)

      attribute name

Since:

  • 0.1.0



242
243
244
245
246
247
248
249
250
251
# File 'lib/yes/core/aggregate.rb', line 242

def command(*args, **, &)
  return handle_command_shortcut(*args, **, &) unless Dsl::CommandShortcutExpander.base_case?(*args, **, &)

  name = args.first
  @commands ||= {}
  command_data = Dsl::CommandData.new(name, self, { context:, aggregate: })
  @commands[name] = command_data

  Dsl::CommandDefiner.new(command_data).call(&)
end

.commandsHash

Returns The commands defined on this aggregate.

Returns:

  • (Hash)

    The commands defined on this aggregate

Since:

  • 0.1.0



282
283
284
# File 'lib/yes/core/aggregate.rb', line 282

def commands
  @commands ||= {}
end

.contextString

Returns the context namespace for the aggregate

Examples:

Users::User::Aggregate.context #=> "Users"

Returns:

  • (String)

    The context namespace

Since:

  • 0.1.0



258
259
260
# File 'lib/yes/core/aggregate.rb', line 258

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

Parameters:

  • subclass (Class)

    The class inheriting from Aggregate

Since:

  • 0.1.0



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

Parameters:

  • name (Symbol)

    The name of the parent.

  • options (Hash)

    Options for configuring the parent.

Yields:

  • Block for defining guards and other attribute configurations.

Since:

  • 0.1.0



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/yes/core/aggregate.rb', line 81

def parent(name, **options, &)
  parent_aggregates[name] = options

  attribute name, :aggregate

  return unless options.fetch(:command, true)

  command :"assign_#{name}" 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_aggregatesHash<Symbol, Hash>

Retrieves or initializes the parent_aggregates hash.

Returns:

  • (Hash<Symbol, Hash>)

    A hash containing parent aggregates and their configuration options

Since:

  • 0.1.0



100
101
102
# File 'lib/yes/core/aggregate.rb', line 100

def parent_aggregates
  @parent_aggregates ||= {}
end

.primary_context(context) ⇒ void

This method returns an undefined value.

Sets the primary context for the aggregate.

Parameters:

  • context (String)

    The primary context to set.

Since:

  • 0.1.0



141
142
143
# File 'lib/yes/core/aggregate.rb', line 141

def primary_context(context)
  @_primary_context = context
end

.removable(attr_name: :removed_at) { ... } ⇒ void

This method returns an undefined value.

Defines a default removal behavior for the aggregate.

Examples:

Define a default removal behavior

class UserAggregate < Yes::Core::Aggregate
  removable
end

Define a removal behavior with additional custom guards

class UserAggregate < Yes::Core::Aggregate
  removable do
    guard(:exists) { read_model.name.present? }
  end
end

Define a removal behavior with a custom attribute name

class UserAggregate < Yes::Core::Aggregate
  removable(attr_name: :deleted_at)
end

Parameters:

  • attr_name (Symbol) (defaults to: :removed_at)

    the attribute name to use for marking removal

Yields:

  • Block for defining additional guards and other removal configurations

Since:

  • 0.1.0



127
128
129
130
131
132
133
134
135
# File 'lib/yes/core/aggregate.rb', line 127

def removable(attr_name: :removed_at, &)
  attribute attr_name, :datetime unless attributes.key?(attr_name)

  command :remove 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

#commandsHash<Symbol, Array<Symbol>>

Returns a list of commands that can be executed on this aggregate with their associated events

Examples:

user_aggregate.commands
# => {
#   approve_documents: [:documents_approved],
#   change_age: [:age_changed],
#   change_email: [:email_changed],
#   change_name: [:name_changed]
# }

Returns:

  • (Hash<Symbol, Array<Symbol>>)

    A hash of command names to their event names, sorted alphabetically

Since:

  • 0.1.0



381
382
383
384
385
386
387
388
# File 'lib/yes/core/aggregate.rb', line 381

def commands
  mappings = Yes::Core.configuration.command_event_mappings(
    self.class.context,
    self.class.aggregate
  )

  mappings.sort.to_h
end

#event_revisionInteger

Returns the stream revision number of the latest event

Returns:

  • (Integer)

    The revision number of the latest event in the stream

Raises:

  • (NoMethodError)

    If no events exist for this aggregate

Since:

  • 0.1.0



367
368
369
# File 'lib/yes/core/aggregate.rb', line 367

def event_revision
  latest_event.stream_revision
end

#eventsEnumerator<PgEventstore::Event>

Returns the events for the aggregate

Returns:

  • (Enumerator<PgEventstore::Event>)

    The events for the aggregate

Since:

  • 0.1.0



350
351
352
353
354
# File 'lib/yes/core/aggregate.rb', line 350

def events
  PgEventstore.client.read_paginated(
    command_utilities.build_stream(metadata: { draft: draft? }), options: { direction: 'Forwards' }
  )
end

#latest_eventPgEventstore::Event?

Retrieves the most recent event from the aggregate’s event stream

Returns:

  • (PgEventstore::Event, nil)

    The latest event or nil if no events exist

Since:

  • 0.1.0



358
359
360
361
362
# File 'lib/yes/core/aggregate.rb', line 358

def latest_event
  PgEventstore.client.read(
    command_utilities.build_stream(metadata: { draft: draft? }), options: { max_count: 1, direction: :desc }
  ).first
end

#reloadYes::Core::Aggregate

Reloads the aggregate and its read model

Returns:

Since:

  • 0.1.0



342
343
344
345
346
# File 'lib/yes/core/aggregate.rb', line 342

def reload
  read_model&.reload

  self
end