midnight-rails 0.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 535ce5a0b745470d77c89d9d260029943e858104b4f163d8c1711c0d6eab9438
4
+ data.tar.gz: be0a0a44852886b9b21b8a8056be983a06254effc038d64117ef8e3119348094
5
+ SHA512:
6
+ metadata.gz: ebeb600cfca21c1f21aa457bf597a3891e8dd80b53e757ef0403d414eeadebc826db62b3c58c91acdf7c726720b09415476506e38aaca5c20985fbdbc7fc7b64
7
+ data.tar.gz: 82115430e1d49320ea26d75c4e61bf0ca1d2a45ce7c00d818beb71e5d826468d87660dfb80a9f2084ba5c60da49d307f4c3e6ff772efa9e08c6e990679b40184
@@ -0,0 +1,14 @@
1
+ var FindProxyForURL = (() => {
2
+ var torEndpoint = 'SOCKS5 localhost:9050';
3
+ var onionTester = /\.onion$/i;
4
+ var blockedTester = /^-$/; // nothing is blocking
5
+ return (url, host) => {
6
+ if (onionTester.test(host)) {
7
+ return torEndpoint;
8
+ }
9
+ if (blockedTester.test(host)) {
10
+ return torEndpoint;
11
+ }
12
+ return 'DIRECT';
13
+ };
14
+ })();
@@ -0,0 +1,20 @@
1
+ require_relative 'rails/version'
2
+ require 'active_support/dependencies/autoload'
3
+
4
+ module Midnight
5
+ module Rails
6
+ extend ::ActiveSupport::Autoload
7
+
8
+ autoload :ActiveRecordInteractor
9
+ autoload :ActiveRecordWrapper
10
+ autoload :CommitHooker
11
+ autoload :StateAdapter
12
+ autoload :DefaultState
13
+ autoload :ActiveRecordEventAdapter
14
+ autoload :DefaultEvent
15
+ autoload :Command
16
+ autoload :DryEvent
17
+ autoload :DefaultMigration
18
+ autoload :StaleObjectError
19
+ end
20
+ end
@@ -0,0 +1,47 @@
1
+ require 'activerecord-import'
2
+ require 'midnight/utilities'
3
+ require 'time'
4
+
5
+ module Midnight
6
+ module Rails
7
+ # noinspection RubyDefParenthesesInspection
8
+ # noinspection RubyParameterNamingConvention
9
+ class ActiveRecordEventAdapter
10
+ attr_reader :active_record
11
+
12
+ def initialize(\
13
+ active_record: DefaultEvent,
14
+ serializer: Utilities::JsonSerializer,
15
+ fetch_event_timestamp: lambda do |event|
16
+ ::Time.parse(event.metadata[:occurred_at])
17
+ end
18
+ )
19
+ @active_record = active_record
20
+ @serializer = serializer
21
+ @fetch_event_timestamp = fetch_event_timestamp
22
+ freeze
23
+ end
24
+
25
+ def save_events(events = [], next_position:, state_id:, **_)
26
+ serialized_events = events.each_with_index.map do |unserialized_event, position_offset|
27
+ {
28
+ event_type: unserialized_event.class.name,
29
+ data: @serializer.dump(unserialized_event.data),
30
+ metadata: @serializer.dump(unserialized_event.metadata),
31
+ position: next_position + position_offset,
32
+ state_id: state_id,
33
+ occurred_at: @fetch_event_timestamp.call(unserialized_event),
34
+ }
35
+ end
36
+ # https://github.com/zdennis/activerecord-import
37
+ active_record.import(serialized_events, validate: false)
38
+ end
39
+
40
+ # load functionality intentionally omitted
41
+ # different use cases requires different load implementation
42
+ # so we don't include it in Midnight::Rails
43
+ # but you can implement one easily
44
+
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,70 @@
1
+ require 'midnight/business_logic'
2
+
3
+ module Midnight
4
+ module Rails
5
+ DEFAULT_COMMAND_VALIDATOR = lambda(&:validate!)
6
+
7
+ # noinspection RubyParameterNamingConvention
8
+ class ActiveRecordInteractor
9
+ def initialize(
10
+ aggregate_key:,
11
+ build_aggregate:,
12
+ transaction_handler: Commons::NULL_EVENT_HANDLER,
13
+ committed_handler: Commons::NULL_EVENT_HANDLER,
14
+ command_validator: DEFAULT_COMMAND_VALIDATOR,
15
+ state_persistence: StateAdapter.new(
16
+ active_record: DefaultState
17
+ ),
18
+ advance_state_metadata: lambda(&:advance_metadata),
19
+ save_state: lambda(&:save!)
20
+ )
21
+ @aggregate_key = aggregate_key
22
+ @build_aggregate = build_aggregate
23
+ @transaction_handler = transaction_handler
24
+ @committed_handler = committed_handler
25
+ @command_validator = command_validator
26
+ @state_persistence = state_persistence
27
+ @advance_state_metadata = advance_state_metadata
28
+ @save_state = save_state
29
+ freeze
30
+ end
31
+
32
+ def call(*commands)
33
+ commands.each(&@command_validator)
34
+ transaction do |aggregate|
35
+ commands.each(&aggregate.method(:dispatch))
36
+ aggregate.pending_events
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def transaction(&aggregate_operator)
43
+ @state_persistence.transaction do
44
+ state_record = @state_persistence.load(
45
+ key: @aggregate_key
46
+ )
47
+ aggregate = @build_aggregate.call(
48
+ state: state_record.state
49
+ )
50
+ state_metadata = state_record.metadata
51
+ pending_events = aggregate_operator.call(aggregate)
52
+ @transaction_handler.call(
53
+ pending_events,
54
+ state_metadata
55
+ )
56
+ @state_persistence.hook_commit do
57
+ @committed_handler.call(
58
+ pending_events,
59
+ state_metadata
60
+ )
61
+ end
62
+ state_record.state = aggregate.state
63
+ @advance_state_metadata.call(state_record, pending_events)
64
+ @save_state.call(state_record)
65
+ pending_events
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,55 @@
1
+ require 'active_record'
2
+ require 'midnight/business_logic'
3
+
4
+ module Midnight
5
+ module Rails
6
+ class ActiveRecordWrapper
7
+
8
+ def initialize(
9
+ record,
10
+ serializer:,
11
+ metadata_adapter:,
12
+ advance_metadata:
13
+ )
14
+ @record = record
15
+ @serializer = serializer
16
+ @metadata_adapter = metadata_adapter
17
+ @advance_metadata = advance_metadata
18
+ @state_loaded = false
19
+ @state_changed = false
20
+ end
21
+
22
+ def metadata
23
+ @metadata_adapter.call(@record)
24
+ end
25
+
26
+ def state
27
+ return @changed_state if @state_changed
28
+ return @loaded_state if @state_loaded
29
+ @loaded_state = @serializer.load(@record.state)
30
+ @state_loaded = true
31
+ @loaded_state
32
+ end
33
+
34
+ def state=(new_state)
35
+ @state_changed = true
36
+ @changed_state = new_state
37
+ end
38
+
39
+ def advance_metadata(events)
40
+ @advance_metadata.call(@record, events)
41
+ end
42
+
43
+ def save!
44
+ @record.state = @serializer.dump(@changed_state) \
45
+ if @state_changed
46
+ @record.save!
47
+ rescue ::ActiveRecord::StaleObjectError => e
48
+ # rescue just to add the convenient marker module to the exception
49
+ raise e if e.is_a?(Commons::ConcurrentWriteError)
50
+ raise StaleObjectError.new(e.record, e.attempted_action)
51
+ end
52
+ end
53
+ private_constant :ActiveRecordWrapper
54
+ end
55
+ end
@@ -0,0 +1,13 @@
1
+ require 'active_support/concern'
2
+ require 'active_model'
3
+
4
+ module Midnight
5
+ module Rails
6
+ module Command
7
+ extend ::ActiveSupport::Concern
8
+
9
+ include ::ActiveModel::Model
10
+ include ::ActiveModel::Attributes
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,28 @@
1
+ module Midnight
2
+ module Rails
3
+ # after_commit hook
4
+ # https://dev.to/evilmartians/rails-aftercommit-everywhere--4j9g
5
+ class CommitHooker
6
+ def initialize(&after_commit)
7
+ @after_commit = after_commit
8
+ freeze
9
+ end
10
+
11
+ # :nocov:
12
+ # rubocop:disable Style/MethodMissingSuper
13
+ def method_missing(method_name, *_args)
14
+ after_commit.call if method_name == :committed!
15
+ end
16
+ # rubocop:enable Style/MethodMissingSuper
17
+
18
+ def respond_to_missing?(_method_name, _include_private = false)
19
+ true
20
+ end
21
+ # :nocov:
22
+
23
+ private
24
+
25
+ attr_reader :after_commit
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,7 @@
1
+ module Midnight
2
+ module Rails
3
+ class DefaultEvent < ::ActiveRecord::Base
4
+ self.table_name = 'midnight__events'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,83 @@
1
+ require 'active_support/concern'
2
+ require 'active_support/core_ext/object/blank'
3
+ require 'openssl'
4
+
5
+ module Midnight
6
+ module Rails
7
+ # noinspection RubyDefParenthesesInspection
8
+ # noinspection RubyParameterNamingConvention
9
+ module DefaultMigration
10
+ extend ::ActiveSupport::Concern
11
+
12
+ def change_state_table(\
13
+ table_name: :midnight__states,
14
+ identification_index_name: nil,
15
+ concurrent_safe: true,
16
+ **_
17
+ )
18
+ # if you are sure your application don't generate aggregate_key
19
+ # that easy to collide (e.g. running number) you can turn
20
+ # the concurrent safety off
21
+ create_table table_name do |t|
22
+ t.binary :state
23
+ t.string :aggregate_key, null: false, index: {
24
+ name: identification_index_name.presence || randomized_index_name(
25
+ "{#{table_name}: :aggregate_key}"
26
+ ),
27
+ unique: concurrent_safe
28
+ }
29
+ t.integer :next_position, default: 1
30
+ t.integer :lock_version, default: 0
31
+ end
32
+ end
33
+
34
+ def change_event_table(\
35
+ table_name: :midnight__events,
36
+ state_index_name: nil,
37
+ occurred_at_index_name: nil,
38
+ position_index_name: nil,
39
+ concurrent_safe: false,
40
+ **_
41
+ )
42
+ # if you have an application with relatively small traffic
43
+ # and don't have that much events to store
44
+ # you can turn the concurrent safety on for the peace of mind
45
+ create_table table_name do |t|
46
+ t.binary :data, null: false
47
+ t.binary :metadata, null: false
48
+ t.string :event_type, null: false
49
+ t.integer :position, null: false
50
+ t.references :state, index: {
51
+ name: state_index_name.presence || randomized_index_name(\
52
+ "{#{table_name}: :state_id}"\
53
+ )
54
+ }
55
+ t.datetime :occurred_at, index: {
56
+ name: occurred_at_index_name.presence || randomized_index_name(\
57
+ "{#{table_name}: :occurred_at}"\
58
+ )
59
+ }, null: false
60
+ t.index [:state_id, :position], \
61
+ name: position_index_name.presence || randomized_index_name(\
62
+ "{#{table_name}:[:state_id,:position]}"\
63
+ ), \
64
+ unique: concurrent_safe
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def randomized_index_name(seed_data)
71
+ # the string can be hard coded
72
+ # but for transparency, lets leave the origin here
73
+ # instead of some magic constants
74
+ randomized_part = ::OpenSSL::HMAC.hexdigest(
75
+ 'SHA256',
76
+ 'Midnight::Rails',
77
+ seed_data
78
+ )
79
+ "index_rails_#{randomized_part[0, 10]}"
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,7 @@
1
+ module Midnight
2
+ module Rails
3
+ class DefaultState < ::ActiveRecord::Base
4
+ self.table_name = 'midnight__states'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,29 @@
1
+ require 'dry/schema'
2
+ require 'midnight/business_logic'
3
+
4
+ module Midnight
5
+ module Rails
6
+ class DryEvent < BusinessLogic::Event
7
+ class SchemaViolation < BusinessLogic::Event::SchemaViolation
8
+ end
9
+
10
+ class << self
11
+ private
12
+
13
+ def ensure_schema(data)
14
+ @data_schema ||= data_schema
15
+ result = @data_schema.call(data)
16
+ raise SchemaViolation unless result.success?
17
+ end
18
+
19
+ def data_schema
20
+ raise NotImplementedError
21
+ # example
22
+ # ::Dry::Schema.Params do
23
+ # required(:example_field).filled(:string)
24
+ # end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,10 @@
1
+ require 'active_record'
2
+ require 'midnight/business_logic'
3
+
4
+ module Midnight
5
+ module Rails
6
+ class StaleObjectError < ::ActiveRecord::StaleObjectError
7
+ include Commons::ConcurrentWriteError
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,56 @@
1
+ require 'active_record'
2
+ require 'midnight/utilities'
3
+
4
+ module Midnight
5
+ module Rails
6
+ class StateAdapter
7
+ def initialize(\
8
+ active_record: DefaultState,
9
+ serializer: Utilities::JsonSerializer,
10
+ metadata_adapter: lambda do |record|
11
+ {
12
+ next_position: record.next_position,
13
+ state_id: record.id,
14
+ aggregate_key: record.aggregate_key,
15
+ }
16
+ end,
17
+ advance_metadata: lambda do |record, events|
18
+ record.next_position += events.length
19
+ end
20
+ )
21
+ @model = active_record
22
+ @serializer = serializer
23
+ @metadata_adapter = metadata_adapter
24
+ @advance_metadata = advance_metadata
25
+ end
26
+
27
+ def load(key:, **_)
28
+ record = begin
29
+ @model.where(
30
+ aggregate_key: key
31
+ ).first_or_create!
32
+ rescue ::ActiveRecord::RecordNotUnique
33
+ # :nocov:
34
+ retry
35
+ # :nocov:
36
+ end
37
+ ActiveRecordWrapper.new(
38
+ record,
39
+ serializer: @serializer,
40
+ metadata_adapter: @metadata_adapter,
41
+ advance_metadata: @advance_metadata
42
+ )
43
+ end
44
+
45
+ def transaction(&block)
46
+ @model.transaction(&block)
47
+ end
48
+
49
+ def hook_commit(&block)
50
+ @model.connection.add_transaction_record(
51
+ CommitHooker.new(&block)
52
+ )
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,5 @@
1
+ module Midnight
2
+ module Rails
3
+ VERSION = '0.0.1'
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,155 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: midnight-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Sarun Rattanasiri
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-08-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activerecord
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activerecord-import
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: activemodel
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: dry-schema
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: midnight-business_logic
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: midnight-utilities
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: Midnight::Rails enrich your Rails, and non-Rails application, decouples
112
+ your business logic from frameworks, databases, and UI with the sane default. See
113
+ for yourself how far an application adopting the clean architecture can go.
114
+ email: midnight_w@gmx.tw
115
+ executables: []
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - lib/abc.js
120
+ - lib/midnight/rails.rb
121
+ - lib/midnight/rails/active_record_event_adapter.rb
122
+ - lib/midnight/rails/active_record_interactor.rb
123
+ - lib/midnight/rails/active_record_wrapper.rb
124
+ - lib/midnight/rails/command.rb
125
+ - lib/midnight/rails/commit_hooker.rb
126
+ - lib/midnight/rails/default_event.rb
127
+ - lib/midnight/rails/default_migration.rb
128
+ - lib/midnight/rails/default_state.rb
129
+ - lib/midnight/rails/dry_event.rb
130
+ - lib/midnight/rails/stale_object_error.rb
131
+ - lib/midnight/rails/state_adapter.rb
132
+ - lib/midnight/rails/version.rb
133
+ homepage: https://gitlab.com/slime-systems/midnight-enterprise-toolkit/tree/master/midnight-rails
134
+ licenses: []
135
+ metadata: {}
136
+ post_install_message:
137
+ rdoc_options: []
138
+ require_paths:
139
+ - lib
140
+ required_ruby_version: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ required_rubygems_version: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ requirements: []
151
+ rubygems_version: 3.0.3
152
+ signing_key:
153
+ specification_version: 4
154
+ summary: The default integration of Midnight::BusinessLogic with the sane choices.
155
+ test_files: []