eventsimple 1.8.1 → 2.0.1

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.
@@ -17,22 +17,22 @@ module Eventsimple
17
17
  case value
18
18
  when String
19
19
  decoded = ActiveSupport::JSON.decode(value)
20
- return event_klass::Message.new(decoded) if event_klass.const_defined?(:Message)
21
-
22
- decoded
20
+ prepare_data(decoded)
23
21
  when Hash
24
- return event_klass::Message.new(value) if event_klass.const_defined?(:Message)
25
-
26
- value
27
- when event_klass::Message
22
+ prepare_data(value)
23
+ when message_class
28
24
  value
29
25
  end
30
26
  end
31
27
 
32
28
  def serialize(value)
33
29
  case value
34
- when Hash, event_klass::Message
35
- ActiveSupport::JSON.encode(value)
30
+ when Hash
31
+ encrypted_data = encrypt_message_data(value)
32
+ ActiveSupport::JSON.encode(encrypted_data)
33
+ when event_klass::Message
34
+ encrypted_data = encrypt_message_data(value.attributes)
35
+ ActiveSupport::JSON.encode(encrypted_data)
36
36
  else
37
37
  super
38
38
  end
@@ -40,9 +40,62 @@ module Eventsimple
40
40
 
41
41
  def deserialize(value)
42
42
  decoded = ActiveSupport::JSON.decode(value)
43
- return event_klass::Message.new(decoded) if event_klass.const_defined?(:Message)
43
+ decrypted_data = decrypt_message_data(decoded)
44
+ message_class ? message_class.new(decrypted_data) : decrypted_data
45
+ end
46
+
47
+ def changed_in_place?(raw_old_value, new_value)
48
+ old_value = deserialize(raw_old_value)
49
+ old_value != new_value
50
+ end
51
+
52
+ def mutable?
53
+ true
54
+ end
55
+
56
+ private
57
+
58
+ def message_class
59
+ event_klass.const_defined?(:Message) ? event_klass::Message : nil
60
+ end
61
+
62
+ def prepare_data(data_hash)
63
+ decrypted_data = decrypt_message_data(data_hash)
64
+ message_class ? message_class.new(decrypted_data) : decrypted_data
65
+ end
66
+
67
+ def encrypt_message_data(data_hash)
68
+ transform_message_data(data_hash, :encrypt)
69
+ end
70
+
71
+ def decrypt_message_data(data_hash)
72
+ transform_message_data(data_hash, :decrypt)
73
+ end
74
+
75
+ def transform_message_data(data_hash, operation)
76
+ return data_hash unless message_class
77
+
78
+ schema = message_class.schema
79
+ result = data_hash.transform_keys(&:to_sym).dup
80
+
81
+ schema.keys.each do |key| # rubocop:disable Style/HashEachMethods
82
+ attr_name = key.name
83
+ type = find_type_with_operation(key.type, operation)
84
+ value = result[attr_name]
85
+
86
+ next if value.nil? || type.nil?
87
+
88
+ result[attr_name] = type.public_send(operation, value.to_s)
89
+ end
90
+
91
+ result
92
+ end
93
+
94
+ def find_type_with_operation(type, operation)
95
+ return type if type.respond_to?(operation)
44
96
 
45
- decoded
97
+ # For Sum types (optional), check if the right side has the operation
98
+ type.respond_to?(:right) && type.right.respond_to?(operation) ? type.right : nil
46
99
  end
47
100
  end
48
101
  end
@@ -51,10 +51,13 @@ module Eventsimple
51
51
 
52
52
  default_scope { order(:created_at) }
53
53
 
54
+ after_initialize :readonly!, if: :persisted?
55
+
54
56
  before_validation :extend_validation
55
- after_validation :perform_transition_checks
57
+ after_validation :perform_transition_checks, on: :create
56
58
  before_create :apply_and_persist
57
59
  after_create :dispatch
60
+ after_create :readonly!
58
61
 
59
62
  include InstanceMethods
60
63
  extend ClassMethods
@@ -73,7 +76,6 @@ module Eventsimple
73
76
  false
74
77
  end
75
78
 
76
- # Apply the event to the aggregate passed in. The default behaviour is a no-op
77
79
  def apply(aggregate); end
78
80
 
79
81
  def can_apply?(_aggregate)
@@ -101,12 +103,10 @@ module Eventsimple
101
103
  self.aggregate = aggregate.extend(validate_form) if validate_form
102
104
  end
103
105
 
104
- # Apply the transformation to the aggregate and save it.
105
106
  def apply_and_persist
106
107
  apply_timestamps(aggregate)
107
108
  apply(aggregate)
108
109
 
109
- # Persist!
110
110
  aggregate.enable_writes!
111
111
  aggregate.save!
112
112
  aggregate.readonly!
@@ -125,6 +125,16 @@ module Eventsimple
125
125
  def aggregate=(aggregate)
126
126
  public_send(:"#{_aggregate_klass.model_name.element}=", aggregate)
127
127
  end
128
+
129
+ def enable_writes!(&block)
130
+ was_readonly = @readonly
131
+ @readonly = false
132
+
133
+ return unless block
134
+
135
+ yield self
136
+ @readonly = was_readonly
137
+ end
128
138
  end
129
139
 
130
140
  module ClassMethods
@@ -169,8 +179,7 @@ module Eventsimple
169
179
 
170
180
  # We want to automatically retry writes on concurrency failures. However events with sync
171
181
  # reactors may have multiple nested events that are written within the same transaction.
172
- # We can only catch and retry writes when they the outermost event encapsulating the whole
173
- # transaction.
182
+ # We can only catch and retry writes when we are in the outermost transaction.
174
183
  def create(*args, &block)
175
184
  with_locks do
176
185
  with_retries(args) { super }
@@ -185,7 +194,7 @@ module Eventsimple
185
194
 
186
195
  def with_locks(&)
187
196
  if _outbox_enabled
188
- base_class.with_advisory_lock(base_class.name, { transaction: true }, &)
197
+ base_class.with_advisory_lock(base_class.name, { transaction: existing_transaction_in_progress? }, &)
189
198
  else
190
199
  yield
191
200
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'dry-struct'
4
+
3
5
  module Eventsimple
4
6
  class Message < Dry::Struct
5
7
  transform_keys(&:to_sym)
@@ -19,5 +21,46 @@ module Eventsimple
19
21
  def inspect
20
22
  as_json
21
23
  end
24
+
25
+ def self.attribute(name, type = Dry::Types::Any, **options)
26
+ validate_type(type)
27
+ super
28
+ end
29
+
30
+ def self.attribute?(name, type = Dry::Types::Any, **options)
31
+ validate_type(type)
32
+ super
33
+ end
34
+
35
+ class << self
36
+ def validate_type(type)
37
+ return if valid_eventsimple_type?(type)
38
+
39
+ source_location = begin
40
+ source = const_source_location(name)&.first if respond_to?(:const_source_location) && name
41
+ source || caller_locations.find { |loc| !loc.path.include?('eventsimple') }&.path
42
+ rescue StandardError
43
+ 'unknown'
44
+ end
45
+
46
+ message = [
47
+ "Only Eventsimple::Types are allowed in Message attributes. \n",
48
+ "File: #{source_location} \n",
49
+ "Received: #{type.inspect} \n",
50
+ "Use Eventsimple::Types::String, Eventsimple::Types::Integer, etc. \n",
51
+ ].join(' ')
52
+
53
+ deprecator = ActiveSupport::Deprecation.new
54
+ deprecator.behavior = Rails.application.config.active_support.deprecation
55
+ deprecator.warn(message)
56
+ end
57
+
58
+ def valid_eventsimple_type?(type)
59
+ return true if type.respond_to?(:meta) && type.meta[:eventsimple] == true
60
+ return true if type.is_a?(Class) && type < Eventsimple::Message
61
+
62
+ false
63
+ end
64
+ end
22
65
  end
23
66
  end
@@ -5,7 +5,7 @@ module Eventsimple
5
5
  class Metadata < Eventsimple::Message
6
6
  schema schema.strict
7
7
 
8
- attribute? :actor_id, DryTypes::Strict::String
9
- attribute? :reason, DryTypes::Strict::String
8
+ attribute? :actor_id, Eventsimple::Types::String.optional
9
+ attribute? :reason, Eventsimple::Types::String.optional
10
10
  end
11
11
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ # require 'active_support/core_ext/object/blank'
4
+
5
+ module Eventsimple
6
+ module Types
7
+ # Encrypted decorator type that wraps any type and adds encryption methods
8
+ class EncryptedType
9
+ include Dry::Types::Type
10
+ include Dry::Types::Decorator
11
+ include Dry::Types::Builder
12
+
13
+ def initialize(type, **options)
14
+ super
15
+ @type = type
16
+ freeze
17
+ end
18
+
19
+ def meta(data = nil)
20
+ return { eventsimple: true } if data.nil?
21
+
22
+ self.class.new(@type.meta(data))
23
+ end
24
+
25
+ def encrypt(value)
26
+ return value if value.blank?
27
+
28
+ return value if ActiveRecord::Encryption::Encryptor.new.encrypted?(value)
29
+
30
+ ActiveRecord::Encryption::Encryptor.new.encrypt(value)
31
+ end
32
+
33
+ def decrypt(value)
34
+ return value if value.blank?
35
+
36
+ ActiveRecord::Encryption::Encryptor.new.decrypt(value)
37
+ rescue StandardError
38
+ value
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'eventsimple/types/encrypted_type'
4
+
5
+ module Eventsimple
6
+ module DryTypes
7
+ include Dry.Types(:strict, :json)
8
+ end
9
+
10
+ module Types
11
+ EVENTSIMPLE_META = { eventsimple: true }.freeze
12
+
13
+ # Extension that preserves eventsimple meta through all type transformations
14
+ module MetaPreservingBuilder
15
+ CHAINABLE_METHODS = %i[optional default enum constrained of].freeze
16
+
17
+ CHAINABLE_METHODS.each do |method_name|
18
+ define_method(method_name) do |*args, &block|
19
+ result = super(*args, &block)
20
+ # Re-apply eventsimple meta if the original type had it
21
+ if respond_to?(:meta) && meta[:eventsimple]
22
+ result.meta(EVENTSIMPLE_META)
23
+ else
24
+ result
25
+ end
26
+ end
27
+ end
28
+
29
+ def encrypted
30
+ is_string_type = respond_to?(:primitive) && primitive == ::String
31
+ raise ArgumentError, "encrypted is only supported for String types" unless is_string_type
32
+
33
+ EncryptedType.new(self)
34
+ end
35
+ end
36
+
37
+ # Include the meta-preserving builder in Dry::Types
38
+ Dry::Types::Builder.prepend(MetaPreservingBuilder)
39
+
40
+ Bool = DryTypes::Strict::Bool.meta(EVENTSIMPLE_META)
41
+ Array = DryTypes::Strict::Array.meta(EVENTSIMPLE_META)
42
+ Hash = DryTypes::Strict::Hash.meta(EVENTSIMPLE_META)
43
+ Integer = DryTypes::Strict::Integer.meta(EVENTSIMPLE_META)
44
+ String = DryTypes::Strict::String.meta(EVENTSIMPLE_META)
45
+
46
+ Decimal = DryTypes::JSON::Decimal.meta(EVENTSIMPLE_META)
47
+ Date = DryTypes::JSON::Date.meta(EVENTSIMPLE_META)
48
+ DateTime = DryTypes::JSON::DateTime.meta(EVENTSIMPLE_META)
49
+ Time = DryTypes::JSON::Time.meta(EVENTSIMPLE_META)
50
+ end
51
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Eventsimple
4
- VERSION = '1.8.1'
4
+ VERSION = '2.0.1'
5
5
  end
data/lib/eventsimple.rb CHANGED
@@ -7,13 +7,11 @@ require 'active_model'
7
7
  require 'active_job'
8
8
  require 'active_support'
9
9
  require 'dry-types'
10
- require 'dry-struct'
11
10
  require 'retriable'
12
11
 
13
- require 'dry_types'
14
-
15
12
  require 'eventsimple/active_job/arguments'
16
13
  require 'eventsimple/configuration'
14
+ require 'eventsimple/types'
17
15
  require 'eventsimple/message'
18
16
  require 'eventsimple/data_type'
19
17
  require 'eventsimple/metadata_type'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eventsimple
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.1
4
+ version: 2.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zulfiqar Ali
@@ -27,30 +27,30 @@ dependencies:
27
27
  name: dry-struct
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
- - - "~>"
30
+ - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: '1.6'
32
+ version: 1.8.0
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - "~>"
37
+ - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: '1.6'
39
+ version: 1.8.0
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: dry-types
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
- - - "~>"
44
+ - - ">="
45
45
  - !ruby/object:Gem::Version
46
- version: '1.7'
46
+ version: 1.7.0
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
- - - "~>"
51
+ - - ">="
52
52
  - !ruby/object:Gem::Version
53
- version: '1.7'
53
+ version: 1.7.0
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: pg
56
56
  requirement: !ruby/object:Gem::Requirement
@@ -340,7 +340,6 @@ files:
340
340
  - catalog-info.yaml
341
341
  - config/routes.rb
342
342
  - eventsimple.gemspec
343
- - lib/dry_types.rb
344
343
  - lib/eventsimple.rb
345
344
  - lib/eventsimple/active_job/arguments.rb
346
345
  - lib/eventsimple/configuration.rb
@@ -364,6 +363,8 @@ files:
364
363
  - lib/eventsimple/reactor.rb
365
364
  - lib/eventsimple/reactor_worker.rb
366
365
  - lib/eventsimple/support/spec_helpers.rb
366
+ - lib/eventsimple/types.rb
367
+ - lib/eventsimple/types/encrypted_type.rb
367
368
  - lib/eventsimple/version.rb
368
369
  - log/development.log
369
370
  - sonar-project.properties
data/lib/dry_types.rb DELETED
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module DryTypes
4
- include Dry.Types()
5
- end