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