sequent 3.1.1 → 3.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/configuration.rb +7 -1
- data/lib/sequent/core/aggregate_repository.rb +10 -0
- data/lib/sequent/core/aggregate_root.rb +27 -0
- data/lib/sequent/core/command.rb +1 -0
- data/lib/sequent/core/event_record.rb +42 -0
- data/lib/sequent/core/event_store.rb +5 -12
- data/lib/sequent/core/ext/ext.rb +2 -2
- data/lib/sequent/core/helpers/autoset_attributes.rb +56 -0
- data/lib/sequent/core/helpers/boolean_validator.rb +26 -0
- data/lib/sequent/core/helpers/default_validators.rb +31 -3
- data/lib/sequent/core/helpers/helpers.rb +1 -13
- data/lib/sequent/core/helpers/message_handler.rb +31 -11
- data/lib/sequent/core/helpers/secret.rb +124 -0
- data/lib/sequent/core/helpers/string_to_value_parsers.rb +4 -3
- data/lib/sequent/core/helpers/string_validator.rb +24 -0
- data/lib/sequent/core/helpers/value_validators.rb +13 -1
- data/lib/sequent/core/persistors/replay_optimized_postgres_persistor.rb +11 -5
- data/lib/sequent/core/transactions/active_record_transaction_provider.rb +16 -0
- data/lib/sequent/core/transactions/no_transactions.rb +5 -1
- data/lib/sequent/core/workflow.rb +17 -0
- data/lib/sequent/test/event_handler_helpers.rb +20 -1
- data/lib/version.rb +1 -1
- metadata +30 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8c1a651a4d982304df2f61362bcefc1736a1eecb6a0693f0da66e95319199311
|
4
|
+
data.tar.gz: '0168575dabe5b65335e56b3feb815c3e8c8abb7b413ae94c271f3480e58fc327'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 39d2faaf084d719c881b9901977bc96b38d8054fd24061d6104ee9036a5e9971565c60cde02f9b874efe722c79d847d544639a08eea6a693d7d027ba8ac49348
|
7
|
+
data.tar.gz: 3a4d7fdd19a09aad5e603d04c571d1c84ff53a63cfaf5571259dc250a4a5b08e7872a98e2ea367a27d719131d49d102da4dfab621e33c8209c88c24b173409e0
|
@@ -24,6 +24,8 @@ module Sequent
|
|
24
24
|
DEFAULT_OFFLINE_REPLAY_PERSISTOR_CLASS = Sequent::Core::Persistors::ActiveRecordPersistor
|
25
25
|
DEFAULT_ONLINE_REPLAY_PERSISTOR_CLASS = Sequent::Core::Persistors::ActiveRecordPersistor
|
26
26
|
|
27
|
+
DEFAULT_EVENT_RECORD_HOOKS_CLASS = Sequent::Core::EventRecordHooks
|
28
|
+
|
27
29
|
attr_accessor :aggregate_repository
|
28
30
|
|
29
31
|
attr_accessor :event_store,
|
@@ -34,6 +36,8 @@ module Sequent
|
|
34
36
|
:transaction_provider,
|
35
37
|
:event_publisher
|
36
38
|
|
39
|
+
attr_accessor :event_record_hooks_class
|
40
|
+
|
37
41
|
attr_accessor :command_handlers,
|
38
42
|
:command_filters
|
39
43
|
|
@@ -80,7 +84,7 @@ module Sequent
|
|
80
84
|
self.event_record_class = Sequent::Core::EventRecord
|
81
85
|
self.stream_record_class = Sequent::Core::StreamRecord
|
82
86
|
self.snapshot_event_class = Sequent::Core::SnapshotEvent
|
83
|
-
self.transaction_provider = Sequent::Core::Transactions::
|
87
|
+
self.transaction_provider = Sequent::Core::Transactions::ActiveRecordTransactionProvider.new
|
84
88
|
self.uuid_generator = Sequent::Core::RandomUuidGenerator
|
85
89
|
self.event_publisher = Sequent::Core::EventPublisher.new
|
86
90
|
self.disable_event_handlers = false
|
@@ -92,6 +96,8 @@ module Sequent
|
|
92
96
|
self.migrations_class_name = MIGRATIONS_CLASS_NAME
|
93
97
|
self.number_of_replay_processes = DEFAULT_NUMBER_OF_REPLAY_PROCESSES
|
94
98
|
|
99
|
+
self.event_record_hooks_class = DEFAULT_EVENT_RECORD_HOOKS_CLASS
|
100
|
+
|
95
101
|
self.offline_replay_persistor_class = DEFAULT_OFFLINE_REPLAY_PERSISTOR_CLASS
|
96
102
|
self.online_replay_persistor_class = DEFAULT_ONLINE_REPLAY_PERSISTOR_CLASS
|
97
103
|
self.database_config_directory = DEFAULT_DATABASE_CONFIG_DIRECTORY
|
@@ -28,6 +28,8 @@ module Sequent
|
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
31
|
+
class HasUncommittedEvents < StandardError; end
|
32
|
+
|
31
33
|
# Adds the given aggregate to the repository (or unit of work).
|
32
34
|
#
|
33
35
|
# Only when +commit+ is called all aggregates in the unit of work are 'processed'
|
@@ -123,6 +125,14 @@ module Sequent
|
|
123
125
|
Thread.current[AGGREGATES_KEY] = nil
|
124
126
|
end
|
125
127
|
|
128
|
+
# Clears the Unit of Work.
|
129
|
+
#
|
130
|
+
# A +HasUncommittedEvents+ is raised when there are uncommitted_events in the Unit of Work.
|
131
|
+
def clear!
|
132
|
+
fail HasUncommittedEvents if aggregates.values.any? { |x| !x.uncommitted_events.empty? }
|
133
|
+
clear
|
134
|
+
end
|
135
|
+
|
126
136
|
private
|
127
137
|
|
128
138
|
def aggregates
|
@@ -1,9 +1,11 @@
|
|
1
1
|
require 'base64'
|
2
2
|
require_relative 'helpers/message_handler'
|
3
|
+
require_relative 'helpers/autoset_attributes'
|
3
4
|
require_relative 'stream_record'
|
4
5
|
|
5
6
|
module Sequent
|
6
7
|
module Core
|
8
|
+
|
7
9
|
module SnapshotConfiguration
|
8
10
|
module ClassMethods
|
9
11
|
##
|
@@ -33,6 +35,7 @@ module Sequent
|
|
33
35
|
#
|
34
36
|
class AggregateRoot
|
35
37
|
include Helpers::MessageHandler
|
38
|
+
include Helpers::AutosetAttributes
|
36
39
|
include SnapshotConfiguration
|
37
40
|
|
38
41
|
attr_reader :id, :uncommitted_events, :sequence_number, :event_stream
|
@@ -102,6 +105,30 @@ module Sequent
|
|
102
105
|
apply_event(event)
|
103
106
|
@uncommitted_events << event
|
104
107
|
end
|
108
|
+
|
109
|
+
# Only apply the event if one of the attributes of the event changed
|
110
|
+
#
|
111
|
+
# on NameSet do |event|
|
112
|
+
# @first_name = event.first_name
|
113
|
+
# @last_name = event.last_name
|
114
|
+
# end
|
115
|
+
#
|
116
|
+
# # The event is applied
|
117
|
+
# apply_if_changed NameSet, first_name: 'Ben', last_name: 'Vonk'
|
118
|
+
#
|
119
|
+
# # This event is not applied
|
120
|
+
# apply_if_changed NameSet, first_name: 'Ben', last_name: 'Vonk'
|
121
|
+
#
|
122
|
+
def apply_if_changed(event_class, args = {})
|
123
|
+
if args.empty?
|
124
|
+
apply event_class
|
125
|
+
elsif self.class
|
126
|
+
.event_attribute_keys(event_class)
|
127
|
+
.any? { |k| instance_variable_get(:"@#{k.to_s}") != args[k.to_sym] }
|
128
|
+
apply event_class, args
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
105
132
|
end
|
106
133
|
end
|
107
134
|
end
|
data/lib/sequent/core/command.rb
CHANGED
@@ -16,6 +16,7 @@ module Sequent
|
|
16
16
|
Sequent::Core::Helpers::EqualSupport,
|
17
17
|
Sequent::Core::Helpers::ParamSupport,
|
18
18
|
Sequent::Core::Helpers::Mergable
|
19
|
+
include ActiveModel::Validations::Callbacks
|
19
20
|
include Sequent::Core::Helpers::TypeConversionSupport
|
20
21
|
|
21
22
|
attrs created_at: DateTime
|
@@ -4,6 +4,46 @@ require_relative 'sequent_oj'
|
|
4
4
|
module Sequent
|
5
5
|
module Core
|
6
6
|
|
7
|
+
# == Event Record Hooks
|
8
|
+
#
|
9
|
+
# These hooks are called during the life cycle of
|
10
|
+
# Sequent::Core::EventRecord. It is recommended to create a subclass of
|
11
|
+
# +Sequent::Core::EventRecordHooks+ when implementing this in your
|
12
|
+
# application.
|
13
|
+
#
|
14
|
+
# Sequent.configure do |config|
|
15
|
+
# config.event_record_hooks_class = MyApp::EventRecordHooks
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# module MyApp
|
19
|
+
# class EventRecordHooks < Sequent::EventRecordHooks
|
20
|
+
#
|
21
|
+
# # Adds additional metadata to the +event_records+ table.
|
22
|
+
# def self.after_serialization(event_record, event)
|
23
|
+
# event_record.metadata = event.metadata if event.respond_to?(:metadata)
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
class EventRecordHooks
|
29
|
+
|
30
|
+
# Called after assigning Sequent's event attributes to the +event_record+.
|
31
|
+
#
|
32
|
+
# *Params*
|
33
|
+
# - +event_record+ An instance of Sequent.configuration.event_record_class
|
34
|
+
# - +event+ An instance of the Sequent::Core::Event being persisted
|
35
|
+
#
|
36
|
+
# class EventRecordHooks < Sequent::EventRecordHooks
|
37
|
+
# def self.after_serialization(event_record, event)
|
38
|
+
# event_record.seen_by_hook = true
|
39
|
+
# end
|
40
|
+
# end
|
41
|
+
def self.after_serialization(event_record, event)
|
42
|
+
# noop
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
7
47
|
module SerializesEvent
|
8
48
|
def event
|
9
49
|
payload = Sequent::Core::Oj.strict_load(self.event_json)
|
@@ -17,6 +57,8 @@ module Sequent
|
|
17
57
|
self.event_type = event.class.name
|
18
58
|
self.created_at = event.created_at
|
19
59
|
self.event_json = self.class.serialize_to_json(event)
|
60
|
+
|
61
|
+
Sequent.configuration.event_record_hooks_class.after_serialization(self, event)
|
20
62
|
end
|
21
63
|
|
22
64
|
module ClassMethods
|
@@ -187,18 +187,11 @@ SELECT aggregate_id
|
|
187
187
|
event_stream.stream_record_id = stream_record.id
|
188
188
|
end
|
189
189
|
uncommitted_events.map do |event|
|
190
|
-
|
191
|
-
command_record_id
|
192
|
-
stream_record_id
|
193
|
-
|
194
|
-
|
195
|
-
event_type: event.class.name,
|
196
|
-
event_json: Sequent.configuration.event_record_class.serialize_to_json(event),
|
197
|
-
created_at: event.created_at
|
198
|
-
}
|
199
|
-
values = values.merge(organization_id: event.organization_id) if event.respond_to?(:organization_id)
|
200
|
-
|
201
|
-
Sequent.configuration.event_record_class.new(values)
|
190
|
+
Sequent.configuration.event_record_class.new.tap do |record|
|
191
|
+
record.command_record_id = command_record.id
|
192
|
+
record.stream_record_id = event_stream.stream_record_id
|
193
|
+
record.event = event
|
194
|
+
end
|
202
195
|
end
|
203
196
|
end
|
204
197
|
connection = Sequent.configuration.event_record_class.connection
|
data/lib/sequent/core/ext/ext.rb
CHANGED
@@ -6,7 +6,7 @@ end
|
|
6
6
|
|
7
7
|
class String
|
8
8
|
def self.deserialize_from_json(value)
|
9
|
-
value
|
9
|
+
value&.to_s
|
10
10
|
end
|
11
11
|
end
|
12
12
|
|
@@ -25,7 +25,7 @@ end
|
|
25
25
|
class BigDecimal
|
26
26
|
def self.deserialize_from_json(value)
|
27
27
|
return nil if value.nil?
|
28
|
-
BigDecimal
|
28
|
+
BigDecimal(value)
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Sequent
|
2
|
+
module Core
|
3
|
+
module Helpers
|
4
|
+
##
|
5
|
+
# In some cases you just want to store the events as instance variables
|
6
|
+
# on the AggregateRoot. In that case you can use the following code:
|
7
|
+
#
|
8
|
+
# class LineItemsSet < Sequent::Event
|
9
|
+
# attrs line_items: array(LineItem)
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# class Invoice < Sequent::AggregateRoot
|
13
|
+
# self.autoset_attributes_for_events LineItemsSet
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# This will automatically create the following block
|
17
|
+
#
|
18
|
+
# on LineItemSet do |event|
|
19
|
+
# @line_items = event.line_items
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# The +autoset_attributes_for_events+ will set all the defined +attrs+
|
23
|
+
# as instance variable, except for the ones defined in +autoset_ignore_attributes+.
|
24
|
+
#
|
25
|
+
module AutosetAttributes
|
26
|
+
module ClassMethods
|
27
|
+
|
28
|
+
@@autoset_ignore_attributes = %w{aggregate_id sequence_number created_at}
|
29
|
+
|
30
|
+
def set_autoset_ignore_attributes(attribute_names)
|
31
|
+
@@autoset_ignore_attributes = attribute_names
|
32
|
+
end
|
33
|
+
|
34
|
+
def event_attribute_keys(event_class)
|
35
|
+
event_class.types.keys.reject { |k| @@autoset_ignore_attributes.include?(k.to_s) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def autoset_attributes_for_events(*event_classes)
|
39
|
+
event_classes.each do |event_class|
|
40
|
+
on event_class do |event|
|
41
|
+
self.class.event_attribute_keys(event_class).each do |attribute_name|
|
42
|
+
instance_variable_set(:"@#{attribute_name.to_s}", event.send(attribute_name.to_sym))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.included(host_class)
|
50
|
+
host_class.extend(ClassMethods)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'active_model'
|
2
|
+
require_relative 'value_validators'
|
3
|
+
|
4
|
+
module Sequent
|
5
|
+
module Core
|
6
|
+
module Helpers
|
7
|
+
# Validates Boolean's
|
8
|
+
# Automatically included when using a
|
9
|
+
#
|
10
|
+
# attrs value: Boolean
|
11
|
+
#
|
12
|
+
# The values:
|
13
|
+
#
|
14
|
+
# `true`, `false`, `'true'`, `'false'`, `nil`, and `blank?`
|
15
|
+
#
|
16
|
+
# are considered valid Booleans.
|
17
|
+
#
|
18
|
+
# They will be converted to `true`, `false` or `nil`
|
19
|
+
class BooleanValidator < ActiveModel::EachValidator
|
20
|
+
def validate_each(subject, attribute, value)
|
21
|
+
subject.errors.add attribute, :invalid_boolean unless Sequent::Core::Helpers::ValueValidators.for(Boolean).valid_value?(value)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -1,11 +1,39 @@
|
|
1
|
+
require_relative 'string_validator'
|
2
|
+
require_relative 'boolean_validator'
|
3
|
+
require_relative 'date_time_validator'
|
4
|
+
require_relative 'date_validator'
|
5
|
+
require_relative 'secret'
|
6
|
+
|
1
7
|
module Sequent
|
2
8
|
module Core
|
3
9
|
module Helpers
|
4
10
|
class DefaultValidators
|
5
11
|
VALIDATORS = {
|
6
|
-
Integer => ->(klass, field)
|
7
|
-
|
8
|
-
|
12
|
+
Integer => ->(klass, field) do
|
13
|
+
klass.validates_numericality_of field, only_integer: true, allow_nil: true, allow_blank: true
|
14
|
+
end,
|
15
|
+
Date => ->(klass, field) do
|
16
|
+
klass.validates field, "sequent::Core::Helpers::Date" => true
|
17
|
+
end,
|
18
|
+
DateTime => ->(klass, field) do
|
19
|
+
klass.validates field, "sequent::Core::Helpers::DateTime" => true
|
20
|
+
end,
|
21
|
+
Boolean => -> (klass, field) do
|
22
|
+
klass.validates field, "sequent::Core::Helpers::Boolean" => true
|
23
|
+
end,
|
24
|
+
String => -> (klass, field) do
|
25
|
+
klass.validates field, "sequent::Core::Helpers::String" => true
|
26
|
+
end,
|
27
|
+
Sequent::Core::Helpers::Secret => -> (klass, field) do
|
28
|
+
klass.after_validation do |object|
|
29
|
+
if object.errors&.any?
|
30
|
+
object.send("#{field}=", nil)
|
31
|
+
else
|
32
|
+
raw_value = object.send(field)
|
33
|
+
object.send("#{field}=", Sequent::Secret.new(raw_value)) if raw_value
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
9
37
|
}
|
10
38
|
|
11
39
|
def self.for(type)
|
@@ -1,13 +1 @@
|
|
1
|
-
require_relative
|
2
|
-
require_relative 'copyable'
|
3
|
-
require_relative 'value_validators'
|
4
|
-
require_relative 'string_to_value_parsers'
|
5
|
-
require_relative 'attribute_support'
|
6
|
-
require_relative 'equal_support'
|
7
|
-
require_relative 'param_support'
|
8
|
-
require_relative 'mergable'
|
9
|
-
require_relative 'association_validator'
|
10
|
-
require_relative 'string_support'
|
11
|
-
require_relative 'type_conversion_support'
|
12
|
-
require_relative 'date_validator'
|
13
|
-
require_relative 'date_time_validator'
|
1
|
+
Dir["#{File.dirname(__FILE__)}/*rb"].each { |f| require_relative f }
|
@@ -3,22 +3,42 @@ module Sequent
|
|
3
3
|
module Helpers
|
4
4
|
##
|
5
5
|
# Creates ability to use DSL like:
|
6
|
-
# class MyProjector < Sequent::Projector
|
7
6
|
#
|
8
|
-
#
|
9
|
-
#
|
7
|
+
# class MyProjector < Sequent::Projector
|
8
|
+
#
|
9
|
+
# on MyEvent do |event|
|
10
|
+
# @foo = event.foo
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# If you extend from +Sequent::AggregateRoot+, +Sequent::Projector+, +Sequent::Workflow+
|
16
|
+
# or +Sequent::CommandHandler+ you will get this functionality
|
17
|
+
# for free.
|
18
|
+
#
|
19
|
+
# It is possible to register multiple handler blocks in the same +MessageHandler+
|
20
|
+
#
|
21
|
+
# class MyProjector < Sequent::Projector
|
22
|
+
#
|
23
|
+
# on MyEvent do |event|
|
24
|
+
# @foo = event.foo
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# on MyEvent, OtherEvent do |event|
|
28
|
+
# @bar = event.bar
|
29
|
+
# end
|
30
|
+
#
|
10
31
|
# end
|
11
|
-
# end
|
12
32
|
#
|
13
|
-
#
|
14
|
-
# Sequent::AggregateRoot, Sequent::Projector, Sequent::Workflow or Sequent::CommandHandler
|
15
|
-
# you will get this functionality for free.
|
33
|
+
# The order of which handler block is executed first is not guaranteed.
|
16
34
|
#
|
17
35
|
module MessageHandler
|
18
|
-
|
19
36
|
module ClassMethods
|
20
37
|
def on(*message_classes, &block)
|
21
|
-
message_classes.each
|
38
|
+
message_classes.each do |message_class|
|
39
|
+
message_mapping[message_class] ||= []
|
40
|
+
message_mapping[message_class] << block
|
41
|
+
end
|
22
42
|
end
|
23
43
|
|
24
44
|
def message_mapping
|
@@ -35,8 +55,8 @@ module Sequent
|
|
35
55
|
end
|
36
56
|
|
37
57
|
def handle_message(message)
|
38
|
-
|
39
|
-
self.instance_exec(message, &handler) if
|
58
|
+
handlers = self.class.message_mapping[message.class]
|
59
|
+
handlers.each { |handler| self.instance_exec(message, &handler) } if handlers
|
40
60
|
end
|
41
61
|
end
|
42
62
|
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'bcrypt'
|
2
|
+
|
3
|
+
module Sequent
|
4
|
+
module Core
|
5
|
+
module Helpers
|
6
|
+
|
7
|
+
#
|
8
|
+
# You can use this in Commands to handle for instance passwords
|
9
|
+
# safely. It uses BCrypt to encrypt the Secret.
|
10
|
+
#
|
11
|
+
# Attributes that are of type Secret are encrypted **after** successful validation in the CommandService
|
12
|
+
# automatically. So there is no need to do this yourself, Sequent will take care of this for you.
|
13
|
+
# As a result the CommandHandlers will receive the encrypted values.
|
14
|
+
#
|
15
|
+
# Since this is meant to be used in +Command+s based on input you can
|
16
|
+
# put in +String+s and +Secret+s.
|
17
|
+
#
|
18
|
+
# Example usage:
|
19
|
+
#
|
20
|
+
# class CreateUser < Sequent::Command
|
21
|
+
# attrs email: String, password: Sequent::Secret
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# command = CreateUser.new(
|
25
|
+
# aggregate_id: Sequent.new_uuid,
|
26
|
+
# email: 'ben@sequent.io',
|
27
|
+
# password: 'secret',
|
28
|
+
# )
|
29
|
+
#
|
30
|
+
# puts command.password
|
31
|
+
# => secret
|
32
|
+
#
|
33
|
+
# command.valid?
|
34
|
+
# => true
|
35
|
+
#
|
36
|
+
# command = command.parse_attrs_to_correct_types
|
37
|
+
# puts command.password
|
38
|
+
# => SAasdf239as$%^@#%dasfgasasdf (or something similar :-))
|
39
|
+
#
|
40
|
+
# When command validation fails attributes of type Sequent::Secret are cleared.
|
41
|
+
#
|
42
|
+
# command.valid?
|
43
|
+
# => false
|
44
|
+
#
|
45
|
+
# puts command.password
|
46
|
+
# => ''
|
47
|
+
#
|
48
|
+
# There is no real need to use this type in Events since there we are
|
49
|
+
# only interested in the encrypted String at that point.
|
50
|
+
#
|
51
|
+
# Besides the Sequent::Secret type there are also some helper methods available to
|
52
|
+
# assist in verifying secrets.
|
53
|
+
#
|
54
|
+
# See +encrypt_secret+
|
55
|
+
# See +re_encrypt_secret+
|
56
|
+
# See +verify_secret+
|
57
|
+
class Secret
|
58
|
+
|
59
|
+
class << self
|
60
|
+
def deserialize_from_json(value)
|
61
|
+
new(value)
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
65
|
+
# Creates a hash for the given clear text password.
|
66
|
+
#
|
67
|
+
def encrypt_secret(clear_text_secret)
|
68
|
+
fail ArgumentError.new('clear_text_secret can not be blank') if clear_text_secret.blank?
|
69
|
+
BCrypt::Password.create(clear_text_secret)
|
70
|
+
end
|
71
|
+
|
72
|
+
##
|
73
|
+
# Creates a hash for the given clear text secret using the given hashed secret as a salt
|
74
|
+
# (essentially re-creating the secret hash).
|
75
|
+
#
|
76
|
+
def re_encrypt_secret(clear_text_secret, hashed_secret)
|
77
|
+
fail ArgumentError.new('clear_text_secret can not be blank') if clear_text_secret.blank?
|
78
|
+
fail ArgumentError.new('hashed_secret can not be blank') if hashed_secret.blank?
|
79
|
+
|
80
|
+
BCrypt::Engine.hash_secret(clear_text_secret, hashed_secret)
|
81
|
+
end
|
82
|
+
|
83
|
+
##
|
84
|
+
# Verifies that the hashed and clear text secret are equal.
|
85
|
+
#
|
86
|
+
def verify_secret(hashed_secret, clear_text_secret)
|
87
|
+
return false if (hashed_secret.blank? || clear_text_secret.blank?)
|
88
|
+
|
89
|
+
BCrypt::Password.new(hashed_secret) == clear_text_secret
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
attr_reader :value
|
94
|
+
|
95
|
+
def initialize(value)
|
96
|
+
fail ArgumentError.new('value can not be blank') if value.blank?
|
97
|
+
if value.is_a?(Secret)
|
98
|
+
@value = value.value
|
99
|
+
else
|
100
|
+
@value = value
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def encrypt
|
105
|
+
@value = self.class.encrypt_secret(@value)
|
106
|
+
self
|
107
|
+
end
|
108
|
+
|
109
|
+
def verify_secret(clear_text_secret)
|
110
|
+
self.class.verify_secret(@value, clear_text_secret)
|
111
|
+
end
|
112
|
+
|
113
|
+
def ==(other)
|
114
|
+
return false unless other&.class == Secret
|
115
|
+
|
116
|
+
other.value == @value
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Shortcut
|
123
|
+
Secret = Core::Helpers::Secret
|
124
|
+
end
|
@@ -7,14 +7,15 @@ module Sequent
|
|
7
7
|
class StringToValueParsers
|
8
8
|
PARSERS = {
|
9
9
|
::Symbol => ->(value) { Symbol.deserialize_from_json(value) },
|
10
|
-
::String => ->(value) { value },
|
10
|
+
::String => ->(value) { value&.to_s },
|
11
11
|
::Integer => ->(value) { parse_to_integer(value) },
|
12
12
|
::BigDecimal => ->(value) { parse_to_bigdecimal(value) },
|
13
13
|
::Float => ->(value) { parse_to_float(value) },
|
14
14
|
::Boolean => ->(value) { parse_to_bool(value) },
|
15
15
|
::Date => ->(value) { parse_to_date(value) },
|
16
16
|
::DateTime => ->(value) { parse_to_date_time(value) },
|
17
|
-
::Sequent::Core::Helpers::ArrayWithType => ->(values, type_in_array) { parse_array(values, type_in_array) }
|
17
|
+
::Sequent::Core::Helpers::ArrayWithType => ->(values, type_in_array) { parse_array(values, type_in_array) },
|
18
|
+
::Sequent::Core::Helpers::Secret => ->(value) { Sequent::Core::Helpers::Secret.new(value).encrypt },
|
18
19
|
}
|
19
20
|
|
20
21
|
def self.parse_to_integer(value)
|
@@ -23,7 +24,7 @@ module Sequent
|
|
23
24
|
end
|
24
25
|
|
25
26
|
def self.parse_to_bigdecimal(value)
|
26
|
-
BigDecimal
|
27
|
+
BigDecimal(value) unless value.blank?
|
27
28
|
end
|
28
29
|
|
29
30
|
def self.parse_to_float(value)
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'active_model'
|
2
|
+
require_relative 'value_validators'
|
3
|
+
|
4
|
+
module Sequent
|
5
|
+
module Core
|
6
|
+
module Helpers
|
7
|
+
# Validates String's
|
8
|
+
# Automatically included when using a
|
9
|
+
#
|
10
|
+
# attrs value: String
|
11
|
+
#
|
12
|
+
# Basically all ruby String are valid Strings.
|
13
|
+
#
|
14
|
+
# For now we do fail when value is not a String
|
15
|
+
# or contains a any chars defined in ValueValidators::INVALID_CHARS
|
16
|
+
#
|
17
|
+
class StringValidator < ActiveModel::EachValidator
|
18
|
+
def validate_each(subject, attribute, value)
|
19
|
+
subject.errors.add attribute, :invalid_string unless Sequent::Core::Helpers::ValueValidators.for(String).valid_value?(value)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -4,9 +4,13 @@ module Sequent
|
|
4
4
|
module Core
|
5
5
|
module Helpers
|
6
6
|
class ValueValidators
|
7
|
+
INVALID_STRING_CHARS = [
|
8
|
+
"\u0000",
|
9
|
+
]
|
10
|
+
|
7
11
|
VALIDATORS = {
|
8
12
|
::Symbol => ->(_) { true },
|
9
|
-
::String => ->(value) {
|
13
|
+
::String => ->(value) { valid_string?(value) },
|
10
14
|
::Integer => ->(value) { valid_integer?(value) },
|
11
15
|
::Boolean => ->(value) { valid_bool?(value) },
|
12
16
|
::Date => ->(value) { valid_date?(value) },
|
@@ -36,6 +40,14 @@ module Sequent
|
|
36
40
|
value.is_a?(DateTime) || !!DateTime.iso8601(value.dup) rescue false
|
37
41
|
end
|
38
42
|
|
43
|
+
def self.valid_string?(value)
|
44
|
+
return true if value.nil?
|
45
|
+
value.to_s && !INVALID_STRING_CHARS.any? { |invalid_char| value.to_s.include?(invalid_char) }
|
46
|
+
rescue => e
|
47
|
+
p foo: e
|
48
|
+
false
|
49
|
+
end
|
50
|
+
|
39
51
|
def self.for(klass)
|
40
52
|
new(klass)
|
41
53
|
end
|
@@ -172,7 +172,7 @@ module Sequent
|
|
172
172
|
|
173
173
|
def create_record(record_class, values)
|
174
174
|
column_names = record_class.column_names
|
175
|
-
values = record_class.column_defaults.merge(values)
|
175
|
+
values = record_class.column_defaults.with_indifferent_access.merge(values)
|
176
176
|
values.merge!(updated_at: values[:created_at]) if column_names.include?("updated_at")
|
177
177
|
struct_class_name = "#{record_class.to_s}Struct"
|
178
178
|
if self.class.struct_cache.has_key?(struct_class_name)
|
@@ -300,9 +300,9 @@ module Sequent
|
|
300
300
|
if records.size > @insert_with_csv_size
|
301
301
|
csv = CSV.new("")
|
302
302
|
column_names = clazz.column_names.reject { |name| name == "id" }
|
303
|
-
records.each do |
|
303
|
+
records.each do |record|
|
304
304
|
csv << column_names.map do |column_name|
|
305
|
-
|
305
|
+
cast_value_to_column_type(clazz, column_name, record)
|
306
306
|
end
|
307
307
|
end
|
308
308
|
|
@@ -321,9 +321,9 @@ module Sequent
|
|
321
321
|
inserts = []
|
322
322
|
column_names = clazz.column_names.reject { |name| name == "id" }
|
323
323
|
prepared_values = (1..column_names.size).map { |i| "$#{i}" }.join(",")
|
324
|
-
records.each do |
|
324
|
+
records.each do |record|
|
325
325
|
values = column_names.map do |column_name|
|
326
|
-
|
326
|
+
cast_value_to_column_type(clazz, column_name, record)
|
327
327
|
end
|
328
328
|
inserts << values
|
329
329
|
end
|
@@ -342,6 +342,12 @@ module Sequent
|
|
342
342
|
@record_store.clear
|
343
343
|
@record_index.clear
|
344
344
|
end
|
345
|
+
|
346
|
+
private
|
347
|
+
|
348
|
+
def cast_value_to_column_type(clazz, column_name, record)
|
349
|
+
ActiveRecord::Base.connection.type_cast(record[column_name.to_sym], @column_cache[clazz.name][column_name])
|
350
|
+
end
|
345
351
|
end
|
346
352
|
end
|
347
353
|
end
|
@@ -7,8 +7,24 @@ module Sequent
|
|
7
7
|
ActiveRecord::Base.transaction(requires_new: true) do
|
8
8
|
yield
|
9
9
|
end
|
10
|
+
after_commit_queue.each &:call
|
11
|
+
ensure
|
12
|
+
clear_after_commit_queue
|
10
13
|
end
|
11
14
|
|
15
|
+
def after_commit(&block)
|
16
|
+
after_commit_queue << block
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def after_commit_queue
|
22
|
+
Thread.current[:after_commit_queue] ||= []
|
23
|
+
end
|
24
|
+
|
25
|
+
def clear_after_commit_queue
|
26
|
+
Thread.current[:after_commit_queue] = []
|
27
|
+
end
|
12
28
|
end
|
13
29
|
|
14
30
|
end
|
@@ -1,7 +1,11 @@
|
|
1
1
|
module Sequent
|
2
2
|
module Core
|
3
3
|
module Transactions
|
4
|
-
|
4
|
+
#
|
5
|
+
# NoTransactions is used when replaying the +ViewSchema+ for
|
6
|
+
# view schema upgrades. Transactions are not needed there since the
|
7
|
+
# view state will always be recreated anyway.
|
8
|
+
#
|
5
9
|
class NoTransactions
|
6
10
|
def transactional
|
7
11
|
yield
|
@@ -8,6 +8,23 @@ module Sequent
|
|
8
8
|
def execute_commands(*commands)
|
9
9
|
Sequent.configuration.command_service.execute_commands(*commands)
|
10
10
|
end
|
11
|
+
|
12
|
+
# Workflow#after_commit will accept a block to execute
|
13
|
+
# after the transaction commits. This is very useful to
|
14
|
+
# isolate side-effects. They will run only on the
|
15
|
+
# transaction's success and will not be able to roll it
|
16
|
+
# back when there is an exception. Useful if your background
|
17
|
+
# jobs processor is not using the same database connection
|
18
|
+
# to enqueue jobs.
|
19
|
+
def after_commit(ignore_errors: false, &block)
|
20
|
+
Sequent.configuration.transaction_provider.after_commit &block
|
21
|
+
rescue StandardError => error
|
22
|
+
if ignore_errors
|
23
|
+
Sequent.logger.warn("An exception was raised in an after_commit hook: #{error}, #{error.inspect}")
|
24
|
+
else
|
25
|
+
raise error
|
26
|
+
end
|
27
|
+
end
|
11
28
|
end
|
12
29
|
end
|
13
30
|
end
|
@@ -27,6 +27,21 @@ module Sequent
|
|
27
27
|
# end
|
28
28
|
module WorkflowHelpers
|
29
29
|
|
30
|
+
class FakeTransactionProvider
|
31
|
+
def initialize
|
32
|
+
@after_commit_blocks = []
|
33
|
+
end
|
34
|
+
|
35
|
+
def transactional
|
36
|
+
yield
|
37
|
+
@after_commit_blocks.each(&:call)
|
38
|
+
end
|
39
|
+
|
40
|
+
def after_commit(&block)
|
41
|
+
@after_commit_blocks << block
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
30
45
|
class FakeCommandService
|
31
46
|
attr_reader :recorded_commands
|
32
47
|
|
@@ -66,8 +81,12 @@ module Sequent
|
|
66
81
|
|
67
82
|
def self.included(spec)
|
68
83
|
spec.let(:fake_command_service) { FakeCommandService.new }
|
84
|
+
spec.let(:fake_transaction_provider) { FakeTransactionProvider.new }
|
69
85
|
spec.before do
|
70
|
-
Sequent.configure
|
86
|
+
Sequent.configure do |c|
|
87
|
+
c.command_service = fake_command_service
|
88
|
+
c.transaction_provider = fake_transaction_provider
|
89
|
+
end
|
71
90
|
end
|
72
91
|
end
|
73
92
|
end
|
data/lib/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sequent
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.1.
|
4
|
+
version: 3.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lars Vonk
|
@@ -12,7 +12,7 @@ authors:
|
|
12
12
|
autorequire:
|
13
13
|
bindir: bin
|
14
14
|
cert_chain: []
|
15
|
-
date:
|
15
|
+
date: 2019-04-01 00:00:00.000000000 Z
|
16
16
|
dependencies:
|
17
17
|
- !ruby/object:Gem::Dependency
|
18
18
|
name: activerecord
|
@@ -21,9 +21,9 @@ dependencies:
|
|
21
21
|
- - ">="
|
22
22
|
- !ruby/object:Gem::Version
|
23
23
|
version: '5.0'
|
24
|
-
- - "
|
24
|
+
- - "<"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '5.
|
26
|
+
version: '5.3'
|
27
27
|
type: :runtime
|
28
28
|
prerelease: false
|
29
29
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -31,9 +31,9 @@ dependencies:
|
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '5.0'
|
34
|
-
- - "
|
34
|
+
- - "<"
|
35
35
|
- !ruby/object:Gem::Version
|
36
|
-
version: '5.
|
36
|
+
version: '5.3'
|
37
37
|
- !ruby/object:Gem::Dependency
|
38
38
|
name: activemodel
|
39
39
|
requirement: !ruby/object:Gem::Requirement
|
@@ -41,9 +41,9 @@ dependencies:
|
|
41
41
|
- - ">="
|
42
42
|
- !ruby/object:Gem::Version
|
43
43
|
version: '5.0'
|
44
|
-
- - "
|
44
|
+
- - "<"
|
45
45
|
- !ruby/object:Gem::Version
|
46
|
-
version: '5.
|
46
|
+
version: '5.3'
|
47
47
|
type: :runtime
|
48
48
|
prerelease: false
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -51,9 +51,9 @@ dependencies:
|
|
51
51
|
- - ">="
|
52
52
|
- !ruby/object:Gem::Version
|
53
53
|
version: '5.0'
|
54
|
-
- - "
|
54
|
+
- - "<"
|
55
55
|
- !ruby/object:Gem::Version
|
56
|
-
version: '5.
|
56
|
+
version: '5.3'
|
57
57
|
- !ruby/object:Gem::Dependency
|
58
58
|
name: pg
|
59
59
|
requirement: !ruby/object:Gem::Requirement
|
@@ -124,20 +124,34 @@ dependencies:
|
|
124
124
|
- - "~>"
|
125
125
|
- !ruby/object:Gem::Version
|
126
126
|
version: 1.12.1
|
127
|
+
- !ruby/object:Gem::Dependency
|
128
|
+
name: bcrypt
|
129
|
+
requirement: !ruby/object:Gem::Requirement
|
130
|
+
requirements:
|
131
|
+
- - "~>"
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '3.1'
|
134
|
+
type: :runtime
|
135
|
+
prerelease: false
|
136
|
+
version_requirements: !ruby/object:Gem::Requirement
|
137
|
+
requirements:
|
138
|
+
- - "~>"
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: '3.1'
|
127
141
|
- !ruby/object:Gem::Dependency
|
128
142
|
name: parser
|
129
143
|
requirement: !ruby/object:Gem::Requirement
|
130
144
|
requirements:
|
131
145
|
- - "~>"
|
132
146
|
- !ruby/object:Gem::Version
|
133
|
-
version: 2.5
|
147
|
+
version: '2.5'
|
134
148
|
type: :runtime
|
135
149
|
prerelease: false
|
136
150
|
version_requirements: !ruby/object:Gem::Requirement
|
137
151
|
requirements:
|
138
152
|
- - "~>"
|
139
153
|
- !ruby/object:Gem::Version
|
140
|
-
version: 2.5
|
154
|
+
version: '2.5'
|
141
155
|
- !ruby/object:Gem::Dependency
|
142
156
|
name: rspec
|
143
157
|
requirement: !ruby/object:Gem::Requirement
|
@@ -268,6 +282,8 @@ files:
|
|
268
282
|
- lib/sequent/core/helpers/array_with_type.rb
|
269
283
|
- lib/sequent/core/helpers/association_validator.rb
|
270
284
|
- lib/sequent/core/helpers/attribute_support.rb
|
285
|
+
- lib/sequent/core/helpers/autoset_attributes.rb
|
286
|
+
- lib/sequent/core/helpers/boolean_validator.rb
|
271
287
|
- lib/sequent/core/helpers/copyable.rb
|
272
288
|
- lib/sequent/core/helpers/date_time_validator.rb
|
273
289
|
- lib/sequent/core/helpers/date_validator.rb
|
@@ -277,8 +293,10 @@ files:
|
|
277
293
|
- lib/sequent/core/helpers/mergable.rb
|
278
294
|
- lib/sequent/core/helpers/message_handler.rb
|
279
295
|
- lib/sequent/core/helpers/param_support.rb
|
296
|
+
- lib/sequent/core/helpers/secret.rb
|
280
297
|
- lib/sequent/core/helpers/string_support.rb
|
281
298
|
- lib/sequent/core/helpers/string_to_value_parsers.rb
|
299
|
+
- lib/sequent/core/helpers/string_validator.rb
|
282
300
|
- lib/sequent/core/helpers/type_conversion_support.rb
|
283
301
|
- lib/sequent/core/helpers/uuid_helper.rb
|
284
302
|
- lib/sequent/core/helpers/value_validators.rb
|