sequent 0.1.1 → 0.1.2
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 +4 -4
- data/lib/sequent/core/aggregate_repository.rb +94 -0
- data/lib/sequent/core/aggregate_root.rb +87 -0
- data/lib/sequent/core/base_command_handler.rb +39 -0
- data/lib/sequent/core/base_event_handler.rb +51 -0
- data/lib/sequent/core/command.rb +79 -0
- data/lib/sequent/core/command_record.rb +26 -0
- data/lib/sequent/core/command_service.rb +118 -0
- data/lib/sequent/core/core.rb +15 -0
- data/lib/sequent/core/event.rb +62 -0
- data/lib/sequent/core/event_record.rb +34 -0
- data/lib/sequent/core/event_store.rb +110 -0
- data/lib/sequent/core/helpers/association_validator.rb +39 -0
- data/lib/sequent/core/helpers/attribute_support.rb +207 -0
- data/lib/sequent/core/helpers/boolean_support.rb +36 -0
- data/lib/sequent/core/helpers/copyable.rb +25 -0
- data/lib/sequent/core/helpers/equal_support.rb +41 -0
- data/lib/sequent/core/helpers/helpers.rb +9 -0
- data/lib/sequent/core/helpers/mergable.rb +21 -0
- data/lib/sequent/core/helpers/param_support.rb +80 -0
- data/lib/sequent/core/helpers/self_applier.rb +45 -0
- data/lib/sequent/core/helpers/string_support.rb +22 -0
- data/lib/sequent/core/helpers/uuid_helper.rb +17 -0
- data/lib/sequent/core/record_sessions/active_record_session.rb +92 -0
- data/lib/sequent/core/record_sessions/record_sessions.rb +2 -0
- data/lib/sequent/core/record_sessions/replay_events_session.rb +306 -0
- data/lib/sequent/core/tenant_event_store.rb +18 -0
- data/lib/sequent/core/transactions/active_record_transaction_provider.rb +16 -0
- data/lib/sequent/core/transactions/no_transactions.rb +13 -0
- data/lib/sequent/core/transactions/transactions.rb +2 -0
- data/lib/sequent/core/value_object.rb +48 -0
- data/lib/sequent/migrations/migrate_events.rb +53 -0
- data/lib/sequent/migrations/migrations.rb +7 -0
- data/lib/sequent/sequent.rb +3 -0
- data/lib/sequent/test/command_handler_helpers.rb +101 -0
- data/lib/sequent/test/test.rb +1 -0
- data/lib/version.rb +3 -0
- metadata +38 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ffdbe1d8e772c148815f98b0d7baa96f32dd5870
|
4
|
+
data.tar.gz: 3b7ed01b18a23c5d7b8522a4b5d0c1af311be02f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5ec2840094aa3f915222047623d3f570563efa800736883c9d07b724773b0948b74602d26560337bb20455fde183a066181cbe594959ca52ebd1d73fea6a0f9e
|
7
|
+
data.tar.gz: 60ac65d32eda16ce7db21cb51ba0a61b6b741b86636185569b1683c76a983c35d6ca806a744befdd195df997ecb3b8df481fd17a15e312d648a7fa816d3127a7
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Sequent
|
2
|
+
module Core
|
3
|
+
# Repository for aggregates.
|
4
|
+
#
|
5
|
+
# Implements the Unit-Of-Work and Identity-Map patterns
|
6
|
+
# to ensure each aggregate is only loaded once per transaction
|
7
|
+
# and that you always get the same aggregate instance back.
|
8
|
+
#
|
9
|
+
# On commit all aggregates associated with the Unit-Of-Work are
|
10
|
+
# queried for uncommitted events. After persisting these events
|
11
|
+
# the uncommitted events are cleared from the aggregate.
|
12
|
+
#
|
13
|
+
# The repository is keeps track of the Unit-Of-Work per thread,
|
14
|
+
# so can be shared between threads.
|
15
|
+
class AggregateRepository
|
16
|
+
# Key used in thread local
|
17
|
+
AGGREGATES_KEY = 'Sequent::Core::AggregateRepository::aggregates'.to_sym
|
18
|
+
|
19
|
+
class NonUniqueAggregateId < Exception
|
20
|
+
def initialize(existing, new)
|
21
|
+
super "Duplicate aggregate #{new} with same key as existing #{existing}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(event_store)
|
26
|
+
@event_store = event_store
|
27
|
+
clear
|
28
|
+
end
|
29
|
+
|
30
|
+
# Adds the given aggregate to the repository (or unit of work).
|
31
|
+
#
|
32
|
+
# Only when +commit+ is called all aggregates in the unit of work are 'processed'
|
33
|
+
# and all uncammited_events are stored in the +event_store+
|
34
|
+
#
|
35
|
+
def add_aggregate(aggregate)
|
36
|
+
if aggregates.has_key?(aggregate.id)
|
37
|
+
raise NonUniqueAggregateId.new(aggregate, aggregates[aggregate.id]) unless aggregates[aggregate.id].equal?(aggregate)
|
38
|
+
else
|
39
|
+
aggregates[aggregate.id] = aggregate
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Throws exception if not exists.
|
44
|
+
def ensure_exists(aggregate_id, clazz)
|
45
|
+
!load_aggregate(aggregate_id, clazz).nil?
|
46
|
+
end
|
47
|
+
|
48
|
+
# Loads aggregate by given id and class
|
49
|
+
# Returns the one in the current Unit Of Work otherwise loads it from history.
|
50
|
+
#
|
51
|
+
# If we implement snapshotting this is the place.
|
52
|
+
def load_aggregate(aggregate_id, clazz)
|
53
|
+
if aggregates.has_key?(aggregate_id)
|
54
|
+
result = aggregates[aggregate_id]
|
55
|
+
raise TypeError, "#{result.class} is not a #{clazz}" unless result.is_a?(clazz)
|
56
|
+
result
|
57
|
+
else
|
58
|
+
events = @event_store.load_events(aggregate_id)
|
59
|
+
aggregates[aggregate_id] = clazz.load_from_history(events)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Gets all uncommitted_events from the 'registered' aggregates
|
64
|
+
# and stores them in the event store.
|
65
|
+
# The command is 'attached' for traceability purpose so we can see
|
66
|
+
# which command resulted in which events.
|
67
|
+
#
|
68
|
+
# This is all abstracted away if you use the Sequent::Core::CommandService
|
69
|
+
#
|
70
|
+
def commit(command)
|
71
|
+
all_events = []
|
72
|
+
aggregates.each_value { |aggregate| all_events += aggregate.uncommitted_events }
|
73
|
+
return if all_events.empty?
|
74
|
+
aggregates.each_value { |aggregate| aggregate.clear_events }
|
75
|
+
store_events command, all_events
|
76
|
+
end
|
77
|
+
|
78
|
+
# Clears the Unit of Work.
|
79
|
+
def clear
|
80
|
+
Thread.current[AGGREGATES_KEY] = {}
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def aggregates
|
86
|
+
Thread.current[AGGREGATES_KEY]
|
87
|
+
end
|
88
|
+
|
89
|
+
def store_events(command, events)
|
90
|
+
@event_store.commit_events(command, events)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require_relative 'helpers/self_applier'
|
2
|
+
|
3
|
+
module Sequent
|
4
|
+
module Core
|
5
|
+
# Base class for all your domain classes.
|
6
|
+
#
|
7
|
+
# +load_from_history+ functionality to be loaded_from_history, meaning a stream of events.
|
8
|
+
#
|
9
|
+
class AggregateRoot
|
10
|
+
include Helpers::SelfApplier
|
11
|
+
|
12
|
+
attr_reader :id, :uncommitted_events, :sequence_number
|
13
|
+
|
14
|
+
def self.load_from_history(events)
|
15
|
+
aggregate_root = allocate() # allocate without calling new
|
16
|
+
aggregate_root.load_from_history(events)
|
17
|
+
aggregate_root
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(id)
|
21
|
+
@id = id
|
22
|
+
@uncommitted_events = []
|
23
|
+
@sequence_number = 1
|
24
|
+
end
|
25
|
+
|
26
|
+
def load_from_history(events)
|
27
|
+
raise "Empty history" if events.empty?
|
28
|
+
@id = events.first.aggregate_id
|
29
|
+
@uncommitted_events = []
|
30
|
+
@sequence_number = events.size + 1
|
31
|
+
events.each { |event| handle_message(event) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_s
|
35
|
+
"#{self.class.name}: #{@id}"
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
def clear_events
|
40
|
+
uncommitted_events.clear
|
41
|
+
end
|
42
|
+
|
43
|
+
protected
|
44
|
+
|
45
|
+
def build_event(event, params = {})
|
46
|
+
event.new({aggregate_id: @id, sequence_number: @sequence_number}.merge(params))
|
47
|
+
end
|
48
|
+
|
49
|
+
# Provide subclasses nice DSL to 'apply' events via:
|
50
|
+
#
|
51
|
+
# def send_invoice
|
52
|
+
# apply InvoiceSentEvent, send_date: DateTime.now
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
def apply(event, params={})
|
56
|
+
event = build_event(event, params) if event.is_a?(Class)
|
57
|
+
handle_message(event)
|
58
|
+
@uncommitted_events << event
|
59
|
+
@sequence_number += 1
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# You can use this class when running in a multi tenant environment
|
64
|
+
# It basically makes sure that the +organization_id+ (the tenant_id for historic reasons)
|
65
|
+
# is available for the subclasses
|
66
|
+
class TenantAggregateRoot < AggregateRoot
|
67
|
+
attr_reader :organization_id
|
68
|
+
|
69
|
+
def initialize(id, organization_id)
|
70
|
+
super(id)
|
71
|
+
@organization_id = organization_id
|
72
|
+
end
|
73
|
+
|
74
|
+
def load_from_history(events)
|
75
|
+
raise "Empty history" if events.empty?
|
76
|
+
@organization_id = events.first.organization_id
|
77
|
+
super(events)
|
78
|
+
end
|
79
|
+
|
80
|
+
protected
|
81
|
+
|
82
|
+
def build_event(event, params = {})
|
83
|
+
super(event, {organization_id: @organization_id}.merge(params))
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require_relative 'helpers/self_applier'
|
2
|
+
require_relative 'helpers/uuid_helper'
|
3
|
+
|
4
|
+
module Sequent
|
5
|
+
module Core
|
6
|
+
# Base class for command handlers
|
7
|
+
# CommandHandlers are responsible for propagating a command to the correct Sequent::Core::AggregateRoot
|
8
|
+
# or creating a new one. For example:
|
9
|
+
#
|
10
|
+
# class InvoiceCommandHandler < Sequent::Core::BaseCommandHandler
|
11
|
+
# on CreateInvoiceCommand do |command|
|
12
|
+
# repository.add_aggregate Invoice.new(command.aggregate_id)
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# on PayInvoiceCommanddo |command|
|
16
|
+
# do_with_aggregate(command, Invoice) {|invoice|invoice.pay(command.pay_date)}
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
class BaseCommandHandler
|
20
|
+
include Sequent::Core::Helpers::SelfApplier,
|
21
|
+
Sequent::Core::Helpers::UuidHelper
|
22
|
+
|
23
|
+
def initialize(repository)
|
24
|
+
@repository = repository
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
def do_with_aggregate(command, clazz, aggregate_id = nil)
|
29
|
+
aggregate = @repository.load_aggregate(aggregate_id.nil? ? command.aggregate_id : aggregate_id, clazz)
|
30
|
+
yield aggregate if block_given?
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
def repository
|
35
|
+
@repository
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require_relative 'helpers/self_applier'
|
2
|
+
|
3
|
+
module Sequent
|
4
|
+
module Core
|
5
|
+
# EventHandlers listen to events and handle them according to their responsibility.
|
6
|
+
#
|
7
|
+
# Examples:
|
8
|
+
# * Updating view states
|
9
|
+
# * Sending emails
|
10
|
+
# * Executing other commands based on events (chainging)
|
11
|
+
#
|
12
|
+
# Example of updating view state, in this case the InvoiceRecord table representing an Invoice
|
13
|
+
#
|
14
|
+
# class InvoiceCommandHandler < Sequent::Core::BaseCommandHandler
|
15
|
+
# on CreateInvoiceCommand do |command|
|
16
|
+
# create_record(
|
17
|
+
# InvoiceRecord,
|
18
|
+
# recipient: command.recipient,
|
19
|
+
# amount: command.amount
|
20
|
+
# )
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# Please note that the actual storage is abstracted away in the +record_session+. Reason
|
25
|
+
# is when replaying the entire event_store our default choice, active_record, is too slow.
|
26
|
+
# Also we want to give the opportunity to use other storage mechanisms for the view state.
|
27
|
+
# See the +def_delegators+ which method to implement.
|
28
|
+
# Due to this abstraction you can not traverse into child objects when using ActiveRecord
|
29
|
+
# like you are used to:
|
30
|
+
#
|
31
|
+
# invoice_record.line_item_records << create_record(LineItemRecord, ...)
|
32
|
+
#
|
33
|
+
# In this case you should simply do:
|
34
|
+
#
|
35
|
+
# create_record(LineItemRecord, invoice_id: invoice_record.aggregate_id)
|
36
|
+
#
|
37
|
+
class BaseEventHandler
|
38
|
+
extend Forwardable
|
39
|
+
include Helpers::SelfApplier
|
40
|
+
|
41
|
+
def initialize(record_session = Sequent::Core::RecordSessions::ActiveRecordSession.new)
|
42
|
+
@record_session = record_session
|
43
|
+
end
|
44
|
+
|
45
|
+
def_delegators :@record_session, :update_record, :create_record, :create_or_update_record, :get_record!, :get_record,
|
46
|
+
:delete_all_records, :update_all_records, :do_with_records, :do_with_record, :delete_record,
|
47
|
+
:find_records, :last_record
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require_relative 'helpers/copyable'
|
2
|
+
require_relative 'helpers/attribute_support'
|
3
|
+
require_relative 'helpers/uuid_helper'
|
4
|
+
require_relative 'helpers/equal_support'
|
5
|
+
require_relative 'helpers/param_support'
|
6
|
+
require_relative 'helpers/mergable'
|
7
|
+
|
8
|
+
module Sequent
|
9
|
+
module Core
|
10
|
+
# Base command
|
11
|
+
class BaseCommand
|
12
|
+
include ActiveModel::Validations,
|
13
|
+
ActiveModel::Serializers::JSON,
|
14
|
+
Sequent::Core::Helpers::Copyable,
|
15
|
+
Sequent::Core::Helpers::AttributeSupport,
|
16
|
+
Sequent::Core::Helpers::UuidHelper,
|
17
|
+
Sequent::Core::Helpers::EqualSupport,
|
18
|
+
Sequent::Core::Helpers::ParamSupport,
|
19
|
+
Sequent::Core::Helpers::Mergable
|
20
|
+
|
21
|
+
attrs created_at: DateTime
|
22
|
+
|
23
|
+
self.include_root_in_json = false
|
24
|
+
|
25
|
+
def initialize(args = {})
|
26
|
+
update_all_attributes args
|
27
|
+
@created_at = DateTime.now
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
module UpdateSequenceNumber
|
33
|
+
extend ActiveSupport::Concern
|
34
|
+
included do
|
35
|
+
attrs sequence_number: Integer
|
36
|
+
validates_presence_of :sequence_number
|
37
|
+
validates_numericality_of :sequence_number, only_integer: true, allow_nil: true, allow_blank: true, greater_than: 0
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Most commonly used command
|
42
|
+
# Command can be instantiated just by using:
|
43
|
+
#
|
44
|
+
# Command.new(aggregate_id: "1", user_id: "joe")
|
45
|
+
#
|
46
|
+
# But the Sequent::Core::Helpers::ParamSupport also enables Commands
|
47
|
+
# to be created from a params hash (like the one from Sinatra) as follows:
|
48
|
+
#
|
49
|
+
# command = Command.from_params(params)
|
50
|
+
#
|
51
|
+
class Command < BaseCommand
|
52
|
+
attrs aggregate_id: String, user_id: String
|
53
|
+
|
54
|
+
def initialize(args = {})
|
55
|
+
raise ArgumentError, "Missing aggregate_id" if args[:aggregate_id].nil?
|
56
|
+
super
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
class UpdateCommand < Command
|
62
|
+
include UpdateSequenceNumber
|
63
|
+
end
|
64
|
+
|
65
|
+
class TenantCommand < Command
|
66
|
+
attrs organization_id: String
|
67
|
+
|
68
|
+
def initialize(args = {})
|
69
|
+
raise ArgumentError, "Missing organization_id" if args[:organization_id].nil?
|
70
|
+
super
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class UpdateTenantCommand < TenantCommand
|
75
|
+
include UpdateSequenceNumber
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
module Sequent
|
3
|
+
module Core
|
4
|
+
# For storing Sequent::Core::Command in the database using active_record
|
5
|
+
class CommandRecord < ActiveRecord::Base
|
6
|
+
|
7
|
+
self.table_name = "command_records"
|
8
|
+
|
9
|
+
validates_presence_of :command_type, :command_json
|
10
|
+
|
11
|
+
def command
|
12
|
+
args = JSON.parse(command_json)
|
13
|
+
Class.const_get(command_type.to_sym).deserialize_from_json(args)
|
14
|
+
end
|
15
|
+
|
16
|
+
def command=(command)
|
17
|
+
self.created_at = command.created_at
|
18
|
+
self.aggregate_id = command.aggregate_id if command.respond_to? :aggregate_id
|
19
|
+
self.organization_id = command.organization_id if command.respond_to? :organization_id
|
20
|
+
self.user_id = command.user_id if command.respond_to? :user_id
|
21
|
+
self.command_type = command.class.name
|
22
|
+
self.command_json = command.to_json.to_s
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require_relative 'transactions/no_transactions'
|
2
|
+
|
3
|
+
module Sequent
|
4
|
+
module Core
|
5
|
+
|
6
|
+
class CommandServiceConfiguration
|
7
|
+
attr_accessor :event_store,
|
8
|
+
:command_handler_classes,
|
9
|
+
:transaction_provider,
|
10
|
+
:filters
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@command_handler_classes = []
|
14
|
+
@transaction_provider = Sequent::Core::Transactions::NoTransactions.new
|
15
|
+
@filters = []
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
# Single point in the application where subclasses of Sequent::Core::BaseCommand
|
22
|
+
# are executed. This will initiate the entire flow of:
|
23
|
+
#
|
24
|
+
# * Validate command
|
25
|
+
# * Call correct Sequent::Core::BaseCommandHandler
|
26
|
+
# * CommandHandler decides which Sequent::Core::AggregateRoot (s) to call
|
27
|
+
# * Events are stored in the Sequent::Core::EventStore
|
28
|
+
# * Unit of Work is cleared
|
29
|
+
#
|
30
|
+
class CommandService
|
31
|
+
|
32
|
+
class << self
|
33
|
+
attr_accessor :configuration,
|
34
|
+
:instance
|
35
|
+
end
|
36
|
+
|
37
|
+
# Creates a new CommandService and overwrites all existing config.
|
38
|
+
# The new CommandService can be retrieved via the +CommandService.instance+ method.
|
39
|
+
#
|
40
|
+
# If you don't want a singleton you can always instantiate it yourself using the +CommandService.new+.
|
41
|
+
def self.configure
|
42
|
+
self.configuration = CommandServiceConfiguration.new
|
43
|
+
yield(configuration) if block_given?
|
44
|
+
self.instance = CommandService.new(configuration)
|
45
|
+
end
|
46
|
+
|
47
|
+
# +DefaultCommandServiceConfiguration+ Configuration class for the CommandService containing:
|
48
|
+
#
|
49
|
+
# +event_store+ The Sequent::Core::EventStore
|
50
|
+
# +command_handler_classes+ Array of BaseCommandHandler classes that need to handle commands
|
51
|
+
# +transaction_provider+ How to do transaction management. Defaults to Sequent::Core::Transactions::NoTransactions
|
52
|
+
# +filters+ List of filter that respond_to :execute(command). Can be useful to do extra checks (security and such).
|
53
|
+
def initialize(configuration = CommandServiceConfiguration.new)
|
54
|
+
@event_store = configuration.event_store
|
55
|
+
@repository = AggregateRepository.new(configuration.event_store)
|
56
|
+
@filters = configuration.filters
|
57
|
+
@transaction_provider = configuration.transaction_provider
|
58
|
+
@command_handlers = configuration.command_handler_classes.map { |handler| handler.new(@repository) }
|
59
|
+
end
|
60
|
+
|
61
|
+
# Executes the given commands in a single transactional block as implemented by the +transaction_provider+
|
62
|
+
#
|
63
|
+
# For each command:
|
64
|
+
#
|
65
|
+
# * All filters are executed. Any exception raised will rollback the transaction and propagate up
|
66
|
+
# * If the command is valid all +command_handlers+ that +handles_message?+ is invoked
|
67
|
+
# * The +repository+ commits the command and all uncommitted_events resulting from the command
|
68
|
+
def execute_commands(*commands)
|
69
|
+
begin
|
70
|
+
@transaction_provider.transactional do
|
71
|
+
commands.each do |command|
|
72
|
+
@filters.each { |filter| filter.execute(command) }
|
73
|
+
|
74
|
+
if command.valid?
|
75
|
+
@command_handlers.each do |command_handler|
|
76
|
+
command_handler.handle_message command if command_handler.handles_message? command
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
@repository.commit(command)
|
81
|
+
raise CommandNotValid.new(command) unless command.validation_errors.empty?
|
82
|
+
end
|
83
|
+
end
|
84
|
+
ensure
|
85
|
+
@repository.clear
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
def remove_event_handler(clazz)
|
91
|
+
@event_store.remove_event_handler(clazz)
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
# Raised when BaseCommand.valid? returns false
|
97
|
+
class CommandNotValid < ArgumentError
|
98
|
+
|
99
|
+
def initialize(command)
|
100
|
+
@command = command
|
101
|
+
msg = @command.respond_to?(:aggregate_id) ? " #{@command.aggregate_id}" : ""
|
102
|
+
super "Invalid command #{@command.class.to_s}#{msg}, errors: #{@command.validation_errors}"
|
103
|
+
end
|
104
|
+
|
105
|
+
def errors(prefix = nil)
|
106
|
+
@command.validation_errors(prefix)
|
107
|
+
end
|
108
|
+
|
109
|
+
def errors_with_command_prefix
|
110
|
+
errors(@command.class.to_s.underscore)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require_relative 'helpers/helpers'
|
2
|
+
require_relative 'record_sessions/record_sessions'
|
3
|
+
require_relative 'transactions/transactions'
|
4
|
+
require_relative 'aggregate_repository'
|
5
|
+
require_relative 'aggregate_root'
|
6
|
+
require_relative 'base_command_handler'
|
7
|
+
require_relative 'command'
|
8
|
+
require_relative 'command_service'
|
9
|
+
require_relative 'event'
|
10
|
+
require_relative 'value_object'
|
11
|
+
require_relative 'base_event_handler'
|
12
|
+
require_relative 'event_store'
|
13
|
+
require_relative 'tenant_event_store'
|
14
|
+
require_relative 'event_record'
|
15
|
+
require_relative 'command_record'
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'active_model'
|
2
|
+
require_relative 'helpers/string_support'
|
3
|
+
require_relative 'helpers/equal_support'
|
4
|
+
require_relative 'helpers/attribute_support'
|
5
|
+
require_relative 'helpers/copyable'
|
6
|
+
|
7
|
+
module Sequent
|
8
|
+
module Core
|
9
|
+
class Event
|
10
|
+
include Sequent::Core::Helpers::StringSupport,
|
11
|
+
Sequent::Core::Helpers::EqualSupport,
|
12
|
+
Sequent::Core::Helpers::AttributeSupport,
|
13
|
+
Sequent::Core::Helpers::Copyable,
|
14
|
+
ActiveModel::Serializers::JSON
|
15
|
+
attrs aggregate_id: String, sequence_number: Integer, created_at: DateTime
|
16
|
+
self.include_root_in_json = false
|
17
|
+
|
18
|
+
def initialize(args = {})
|
19
|
+
update_all_attributes args
|
20
|
+
raise "Missing aggregate_id" unless @aggregate_id
|
21
|
+
raise "Missing sequence_number" unless @sequence_number
|
22
|
+
@created_at ||= DateTime.now
|
23
|
+
end
|
24
|
+
|
25
|
+
def payload
|
26
|
+
result = {}
|
27
|
+
instance_variables.reject { |k| payload_variables.include?(k.to_s) }.each do |k|
|
28
|
+
result[k.to_s[1 .. -1].to_sym] = instance_variable_get(k)
|
29
|
+
end
|
30
|
+
result
|
31
|
+
end
|
32
|
+
protected
|
33
|
+
def payload_variables
|
34
|
+
%w{@aggregate_id @sequence_number @created_at @underscored}
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
class TenantEvent < Event
|
40
|
+
|
41
|
+
attrs organization_id: String
|
42
|
+
|
43
|
+
def initialize(args = {})
|
44
|
+
super
|
45
|
+
raise "Missing organization_id" unless @organization_id
|
46
|
+
end
|
47
|
+
|
48
|
+
protected
|
49
|
+
def payload_variables
|
50
|
+
super << "@organization_id"
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
class CreateEvent < TenantEvent
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'oj'
|
2
|
+
require 'active_record'
|
3
|
+
|
4
|
+
module Sequent
|
5
|
+
module Core
|
6
|
+
class EventRecord < ActiveRecord::Base
|
7
|
+
|
8
|
+
self.table_name = "event_records"
|
9
|
+
|
10
|
+
belongs_to :command_record
|
11
|
+
|
12
|
+
validates_presence_of :aggregate_id, :sequence_number, :event_type, :event_json
|
13
|
+
validates_numericality_of :sequence_number
|
14
|
+
|
15
|
+
|
16
|
+
def event
|
17
|
+
payload = Oj.strict_load(event_json)
|
18
|
+
Class.const_get(event_type.to_sym).deserialize_from_json(payload)
|
19
|
+
end
|
20
|
+
|
21
|
+
def event=(event)
|
22
|
+
self.aggregate_id = event.aggregate_id
|
23
|
+
self.sequence_number = event.sequence_number
|
24
|
+
self.organization_id = event.organization_id if event.respond_to?(:organization_id)
|
25
|
+
self.event_type = event.class.name
|
26
|
+
self.created_at = event.created_at
|
27
|
+
self.event_json = event.to_json.to_s
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|