sequent 3.1.1 → 3.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b29c09f7745423baaf4a1fdf5954fbc1c3b2da0dbcd6fcacc39f736bf182d4a
4
- data.tar.gz: cdf428e608251b1e258b9c8cf55cece3994fb8c06861960be788c0ab285701f9
3
+ metadata.gz: 8c1a651a4d982304df2f61362bcefc1736a1eecb6a0693f0da66e95319199311
4
+ data.tar.gz: '0168575dabe5b65335e56b3feb815c3e8c8abb7b413ae94c271f3480e58fc327'
5
5
  SHA512:
6
- metadata.gz: dcfcea2c8f881ab307d2416573da1f3e751d893471cf2d8997b13f29a1bef15e57f6dffb5167f9b822baaff92670a9db95a32dc62d0b66ecf046390a47f5e3d8
7
- data.tar.gz: 14b30c86e2f66d037278fb3819e8604c9c264556f30c7c817eadbb698d3bdb1d343b5f26cf6989ddeef90b06f4d830392afb19ccbeab1ccc19e3ab2334e06731
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::NoTransactions.new
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
@@ -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
- values = {
191
- command_record_id: command_record.id,
192
- stream_record_id: event_stream.stream_record_id,
193
- aggregate_id: event.aggregate_id,
194
- sequence_number: event.sequence_number,
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
@@ -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.new(value)
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) { klass.validates_numericality_of field, only_integer: true, allow_nil: true, allow_blank: true },
7
- Date => ->(klass, field) { klass.validates field, "sequent::Core::Helpers::Date" => true },
8
- DateTime => ->(klass, field) { klass.validates field, "sequent::Core::Helpers::DateTime" => true }
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 'uuid_helper'
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
- # on MyEvent do |event|
9
- # do_some_logic
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
- # You typically do not need to include this module in your classes. If you extend from
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 { |message_class| message_mapping[message_class] = block }
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
- handler = self.class.message_mapping[message.class]
39
- self.instance_exec(message, &handler) if handler
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.new(value) unless value.blank?
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) { value.nil? || value.is_a?(String) },
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 |obj|
303
+ records.each do |record|
304
304
  csv << column_names.map do |column_name|
305
- ActiveRecord::Base.connection.type_cast(obj[column_name], @column_cache[clazz.name][column_name])
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 |r|
324
+ records.each do |record|
325
325
  values = column_names.map do |column_name|
326
- ActiveRecord::Base.connection.type_cast(r[column_name.to_sym], @column_cache[clazz.name][column_name])
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 { |c| c.command_service = fake_command_service }
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
@@ -1,3 +1,3 @@
1
1
  module Sequent
2
- VERSION = '3.1.1'
2
+ VERSION = '3.1.2'
3
3
  end
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.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: 2018-09-28 00:00:00.000000000 Z
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.2'
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.2'
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.2'
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.2'
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.1.0
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.1.0
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