sequent 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|