active_outbox 0.0.2 → 0.0.4

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: aa92d48552facbc79e9ec9ae9f58a599d533267a490f7199c5b4e87b9f62f83b
4
- data.tar.gz: a04f7dbe86d0b7e53296079be077db720c50baa48f2b53237cb18423fb33209d
3
+ metadata.gz: 1b6c2d51498f61533b935cb0d210cbed4c6159eeee4ae9d142e08564c43a7362
4
+ data.tar.gz: f12ce116296edaf0100e39e927b3a264fac24cc8c66f778a49a7b5ebe2b008fe
5
5
  SHA512:
6
- metadata.gz: acfbc64c4ad058a44d56c170fd194d300841cdfd8bdcaa950b9d63b90ae23f392f3bfdf7c1240e0564d7312680362057360114e9e98e9aaafec842639dd0ab98
7
- data.tar.gz: 6449a7b730797fb16e55fca130e0ee7d4813bda5d06c34c4093f23bb6e20d77e5bca07cdcc5ed212d30c42617b6035ffcb31c2fc0033b90ebb0653dd6fe6bf25
6
+ metadata.gz: 5b0583688cda8d0a641e3ff22624d6ae11976bd664a35ff393bd6fa5b1afe4206acda7bb1f9e6cd66b6c0cd6bde77d5ab57920c5ce07be40425f652571ea9fc1
7
+ data.tar.gz: 252a0cea22365fdb05527ce7fbe9dd59c63e8560cb6d655d07b81e26f4b95af25b424e8d0f28fe5ae9611731a30c980d44c11145450f8501479b96a82ae3c225
data/README.md CHANGED
@@ -1,2 +1,100 @@
1
1
  # Active Outbox
2
2
  A Transactional Outbox implementation for ActiveRecord
3
+
4
+ ![transactional outbox pattern](./docs/images/transactional_outbox.png)
5
+
6
+ This gem aims to implement the event persistance side of the pattern, focusing only on providing a seamless way to store Outbox records whenever a change occurs on a given model (#1 in the diagram).
7
+ We do not provide an event publisher, nor a consumer as a part of this gem since the idea is to keep it as light weight as possible.
8
+
9
+ ## Motivation
10
+ If you find yourself repeatedly defining a transaction block every time you need to persist an event, it might be a sign that something needs improvement. We believe that adopting a pattern should enhance your workflow, not hinder it. Creating, updating or destroying a record should remain a familiar and smooth process.
11
+
12
+ Our primary objective is to ensure a seamless experience without imposing our own opinions or previous experiences. That's why this gem exclusively focuses on persisting records. We leave the other aspects of the pattern entirely open for your customization. You can emit these events using Sidekiq jobs, or explore more sophisticated solutions like Kafka Connect.
13
+
14
+ ## Why active_outbox?
15
+ - Seamless integration with ActiveRecord
16
+ - CRUD events out of the box
17
+ - Ability to set custom events
18
+ - Test helpers to easily check Outbox records are being created correctly
19
+ - Customizable
20
+
21
+ ## Installation
22
+
23
+ Add this line to your application's Gemfile:
24
+
25
+ ```ruby
26
+ gem 'active_outbox'
27
+ ```
28
+
29
+ And then execute:
30
+ ```bash
31
+ bundle install
32
+ ```
33
+ Or install it yourself as:
34
+ ```bash
35
+ gem install active_outbox
36
+ ```
37
+
38
+ ## Usage
39
+ ### Setup
40
+ Create an `Outbox` table using the provided generator and corresponding model.
41
+ ```bash
42
+ rails g active_outbox outbox
43
+ ```
44
+ After running the migration, create an initializer under `config/initializers/active_outbox.rb` and setup the default outbox class to the new `Outbox` model you just created.
45
+ ```ruby
46
+ # frozen_string_literal: true
47
+
48
+ Rails.application.reloader.to_prepare do
49
+ ActiveOutbox.configure do |config|
50
+ config.outbox_mapping = {
51
+ 'default' => 'Outbox'
52
+ }
53
+ end
54
+ end
55
+ ```
56
+
57
+ To allow models to store Outbox records on changes, you will have to include the `Outboxable` concern.
58
+ ```ruby
59
+ # app/models/user.rb
60
+
61
+ class User < ApplicationRecord
62
+ include ActiveOutbox::Outboxable
63
+ end
64
+ ```
65
+ ### Base Events
66
+ Using the User model as an example, the default event names provided are:
67
+ - USER_CREATED
68
+ - USER_UPDATED
69
+ - USER_DESTROYED
70
+
71
+ ### Custom Events
72
+ If you want to persist a custom event other than the provided base events, you can do so.
73
+ ```ruby
74
+ user.save(outbox_event: 'YOUR_CUSTOM_EVENT')
75
+ ```
76
+ ## Advanced Usage
77
+ If more granularity is desired multiple `Outbox` classes can be configured. After creating the needed `Outbox` classes for each module you can specify multiple mappings in the initializer.
78
+ ```ruby
79
+ # frozen_string_literal: true
80
+
81
+ Rails.application.reloader.to_prepare do
82
+ ActiveOutbox.configure do |config|
83
+ config.outbox_mapping = {
84
+ 'Member' => 'Member::Outbox',
85
+ 'UserAccess' => 'UserAccess::Outbox'
86
+ }
87
+ end
88
+ end
89
+ ```
90
+ ## Contributing
91
+
92
+ Bug reports and pull requests are welcome on GitHub at https://github.com/rootstrap/active_outbox. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/rootstrap/active_outbox/blob/main/CODE_OF_CONDUCT.md).
93
+
94
+ ## License
95
+
96
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/license/mit/).
97
+
98
+ ## Code of Conduct
99
+
100
+ Everyone interacting in the ActiveOutbox project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/rootstrap/active_outbox/blob/main/CODE_OF_CONDUCT.md).
data/bin/outbox CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'rails'
4
5
  require 'active_outbox'
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveOutbox
4
+ module AdapterHelper
5
+ def self.uuid_type
6
+ postgres? ? 'uuid' : 'string'
7
+ end
8
+
9
+ def self.json_type
10
+ postgres? ? 'jsonb' : 'string'
11
+ end
12
+
13
+ def self.postgres?
14
+ ActiveRecord::Base.connection.adapter_name.downcase == 'postgresql'
15
+ end
16
+ end
17
+ end
@@ -2,21 +2,20 @@
2
2
 
3
3
  module ActiveOutbox
4
4
  class OutboxConfigurationError < StandardError; end
5
+
5
6
  class OutboxClassNotFoundError < OutboxConfigurationError
6
7
  def message
7
8
  <<~MESSAGE
8
- Missing Outbox class definition in module. Use `rails generate outbox <outbox model name>`.
9
- Define default class in `config/initializers/active_outbox.rb`:
9
+ Missing Outbox class definition. Configure mapping in `config/initializers/active_outbox.rb`:
10
10
 
11
- Rails.application.reloader.to_prepare do
12
- ActiveOutbox.configure do |config|
13
- config.outbox_mapping = {
14
- 'Default' => <outbox model name>,
15
- 'Meetings' => 'Meetings::Outbox'
16
- }
17
- end
18
- end
19
- MESSAGE
11
+ Rails.application.reloader.to_prepare do
12
+ ActiveOutbox.configure do |config|
13
+ config.outbox_mapping = {
14
+ 'default' => <outbox model name>
15
+ }
16
+ end
17
+ end
18
+ MESSAGE
20
19
  end
21
20
  end
22
21
  end
@@ -1,16 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rails'
2
4
  require 'rails/generators'
3
5
  require 'rails/generators/active_record'
4
6
 
5
- class OutboxGenerator < ActiveRecord::Generators::Base
6
- source_root File.expand_path("../templates", __FILE__)
7
+ class ActiveOutboxGenerator < ActiveRecord::Generators::Base
8
+ include ActiveOutbox::AdapterHelper
9
+ source_root File.expand_path('templates', __dir__)
7
10
 
8
11
  class_option :root_components_path, type: :string, default: Rails.root
9
12
 
10
13
  def create_migration_files
11
14
  migration_path = "#{options['root_components_path']}/db/migrate"
12
15
  migration_template(
13
- "migration.rb",
16
+ 'migration.rb',
14
17
  "#{migration_path}/outbox_create_#{table_name}.rb",
15
18
  migration_version: migration_version
16
19
  )
@@ -23,16 +26,4 @@ class OutboxGenerator < ActiveRecord::Generators::Base
23
26
  def table_name
24
27
  "#{name}_outboxes"
25
28
  end
26
-
27
- def uuid_type
28
- postgres? ? 'uuid' : 'string'
29
- end
30
-
31
- def json_type
32
- postgres? ? 'jsonb' : 'string'
33
- end
34
-
35
- def postgres?
36
- ActiveRecord::Base.connection.adapter_name.downcase == 'postgresql'
37
- end
38
29
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class OutboxCreate<%= table_name.camelize.singularize %> < ActiveRecord::Migration<%= migration_version %>
2
4
  def change
3
5
  create_table :<%= table_name %> do |t|
@@ -24,36 +24,23 @@ module ActiveOutbox
24
24
  end
25
25
 
26
26
  def save(**options, &block)
27
- if options[:outbox_event].present?
28
- @outbox_event = options[:outbox_event].underscore.upcase
29
- end
30
-
27
+ assign_outbox_event(options)
31
28
  super(**options, &block)
32
29
  end
33
30
 
34
31
  def save!(**options, &block)
35
- if options[:outbox_event].present?
36
- @outbox_event = options[:outbox_event].underscore.upcase
37
- end
38
-
32
+ assign_outbox_event(options)
39
33
  super(**options, &block)
40
34
  end
41
35
 
42
36
  private
43
37
 
38
+ def assign_outbox_event(options)
39
+ @outbox_event = options[:outbox_event].underscore.upcase if options[:outbox_event].present?
40
+ end
41
+
44
42
  def create_outbox!(action, event_name)
45
- unless self.class.module_parent.const_defined?('OUTBOX_MODEL')
46
- *namespace, klass = self.class.name.underscore.upcase.split('/')
47
- namespace = namespace.reverse.join('.')
48
- outbox_model_name = ActiveOutbox.configuration.outbox_mapping[self.class.module_parent.name.underscore] ||
49
- ActiveOutbox.configuration.outbox_mapping['default']
50
- raise OutboxClassNotFoundError if outbox_model_name.nil?
51
-
52
- outbox_model = outbox_model_name.safe_constantize
53
- self.class.module_parent.const_set('OUTBOX_MODEL', outbox_model)
54
- end
55
-
56
- outbox = self.class.module_parent.const_get('OUTBOX_MODEL').new(
43
+ outbox = outbox_model.new(
57
44
  aggregate: self.class.name,
58
45
  aggregate_identifier: try(:identifier) || id,
59
46
  event: @outbox_event || event_name,
@@ -62,41 +49,59 @@ module ActiveOutbox
62
49
  )
63
50
  @outbox_event = nil
64
51
 
65
- if outbox.invalid?
66
- outbox.errors.each do |error|
67
- self.errors.import(error, attribute: "outbox.#{error.attribute}")
68
- end
52
+ handle_outbox_errors(outbox) if outbox.invalid?
53
+ outbox.save!
54
+ end
55
+
56
+ def outbox_model
57
+ module_parent = self.class.module_parent
58
+
59
+ unless module_parent.const_defined?('OUTBOX_MODEL')
60
+ outbox_model = outbox_model_name!.safe_constantize
61
+ module_parent.const_set('OUTBOX_MODEL', outbox_model)
69
62
  end
70
63
 
71
- outbox.save!
64
+ module_parent.const_get('OUTBOX_MODEL')
72
65
  end
73
66
 
74
- def formatted_payload(action)
75
- payload = payload(action)
76
- case ActiveRecord::Base.connection.adapter_name.downcase
77
- when 'postgresql'
78
- payload
79
- else
80
- payload.to_json
67
+ def outbox_model_name!
68
+ namespace_outbox_mapping || default_outbox_mapping || raise(OutboxClassNotFoundError)
69
+ end
70
+
71
+ def namespace_outbox_mapping
72
+ namespace = self.class.name.split('/').first
73
+
74
+ ActiveOutbox.config.outbox_mapping[namespace&.underscore]
75
+ end
76
+
77
+ def default_outbox_mapping
78
+ ActiveOutbox.config.outbox_mapping['default']
79
+ end
80
+
81
+ def handle_outbox_errors(outbox)
82
+ outbox.errors.each do |error|
83
+ errors.import(error, attribute: "outbox.#{error.attribute}")
81
84
  end
82
85
  end
83
86
 
84
- def payload(action)
85
- payload = { before: nil, after: nil }
87
+ def formatted_payload(action)
88
+ payload = construct_payload(action)
89
+ AdapterHelper.postgres? ? payload : payload.to_json
90
+ end
91
+
92
+ def construct_payload(action)
86
93
  case action
87
94
  when :create
88
- payload[:after] = as_json
95
+ { before: nil, after: as_json }
89
96
  when :update
90
- # previous_changes => { 'name' => ['bob', 'robert'] }
91
97
  changes = previous_changes.transform_values(&:first)
92
- payload[:before] = as_json.merge(changes)
93
- payload[:after] = as_json
98
+ { before: as_json.merge(changes), after: as_json }
94
99
  when :destroy
95
- payload[:before] = as_json
100
+ { before: as_json, after: nil }
96
101
  else
97
- raise ActiveRecord::RecordNotSaved.new("Failed to create Outbox payload for #{self.class.name}: #{identifier}", self)
102
+ raise ActiveRecord::RecordNotSaved.new("Failed to create Outbox payload for #{self.class.name}: #{identifier}",
103
+ self)
98
104
  end
99
- payload
100
105
  end
101
106
  end
102
107
  end
@@ -1,4 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveOutbox
2
4
  class Railtie < Rails::Railtie
3
5
  end
4
- end
6
+ end
data/lib/active_outbox.rb CHANGED
@@ -1,5 +1,15 @@
1
- require 'active_outbox/configuration'
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_outbox/adapter_helper'
2
4
  require 'active_outbox/errors'
5
+ require 'active_outbox/generators/active_outbox_generator'
3
6
  require 'active_outbox/outboxable'
4
- require 'active_outbox/generators/outbox_generator'
5
7
  require 'active_outbox/railtie' if defined?(Rails::Railtie)
8
+ require 'dry-configurable'
9
+
10
+ module ActiveOutbox
11
+ extend Dry::Configurable
12
+
13
+ setting :adapter, default: :sqlite
14
+ setting :outbox_mapping, default: {}
15
+ end
metadata CHANGED
@@ -1,128 +1,44 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_outbox
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Guillermo Aguirre
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-07-05 00:00:00.000000000 Z
11
+ date: 2023-10-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: rails
14
+ name: dry-configurable
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '7.0'
19
+ version: '1.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '7.0'
27
- - !ruby/object:Gem::Dependency
28
- name: pry-rails
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: 0.3.6
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: 0.3.6
41
- - !ruby/object:Gem::Dependency
42
- name: reek
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: 6.0.6
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: 6.0.6
55
- - !ruby/object:Gem::Dependency
56
- name: rspec-rails
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: 3.8.0
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: 3.8.0
26
+ version: '1.0'
69
27
  - !ruby/object:Gem::Dependency
70
- name: rubocop
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - "~>"
74
- - !ruby/object:Gem::Version
75
- version: 1.22.0
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - "~>"
81
- - !ruby/object:Gem::Version
82
- version: 1.22.0
83
- - !ruby/object:Gem::Dependency
84
- name: simplecov
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - "~>"
88
- - !ruby/object:Gem::Version
89
- version: 0.17.1
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - "~>"
95
- - !ruby/object:Gem::Version
96
- version: 0.17.1
97
- - !ruby/object:Gem::Dependency
98
- name: sqlite3
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - '='
102
- - !ruby/object:Gem::Version
103
- version: 1.4.2
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - '='
109
- - !ruby/object:Gem::Version
110
- version: 1.4.2
111
- - !ruby/object:Gem::Dependency
112
- name: byebug
28
+ name: rails
113
29
  requirement: !ruby/object:Gem::Requirement
114
30
  requirements:
115
31
  - - "~>"
116
32
  - !ruby/object:Gem::Version
117
- version: 11.1.3
118
- type: :development
33
+ version: '6.1'
34
+ type: :runtime
119
35
  prerelease: false
120
36
  version_requirements: !ruby/object:Gem::Requirement
121
37
  requirements:
122
38
  - - "~>"
123
39
  - !ruby/object:Gem::Version
124
- version: 11.1.3
125
- description: A Transactional Outbox implementation for ActiveRecord
40
+ version: '6.1'
41
+ description:
126
42
  email: guillermoaguirre1@gmail.com
127
43
  executables:
128
44
  - outbox
@@ -132,9 +48,9 @@ files:
132
48
  - README.md
133
49
  - bin/outbox
134
50
  - lib/active_outbox.rb
135
- - lib/active_outbox/configuration.rb
51
+ - lib/active_outbox/adapter_helper.rb
136
52
  - lib/active_outbox/errors.rb
137
- - lib/active_outbox/generators/outbox_generator.rb
53
+ - lib/active_outbox/generators/active_outbox_generator.rb
138
54
  - lib/active_outbox/generators/templates/migration.rb
139
55
  - lib/active_outbox/outboxable.rb
140
56
  - lib/active_outbox/railtie.rb
@@ -142,7 +58,7 @@ homepage: https://rubygems.org/gems/active_outbox
142
58
  licenses:
143
59
  - MIT
144
60
  metadata: {}
145
- post_install_message:
61
+ post_install_message:
146
62
  rdoc_options: []
147
63
  require_paths:
148
64
  - lib
@@ -150,15 +66,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
150
66
  requirements:
151
67
  - - ">="
152
68
  - !ruby/object:Gem::Version
153
- version: '0'
69
+ version: '3.0'
154
70
  required_rubygems_version: !ruby/object:Gem::Requirement
155
71
  requirements:
156
72
  - - ">="
157
73
  - !ruby/object:Gem::Version
158
74
  version: '0'
159
75
  requirements: []
160
- rubygems_version: 3.1.6
161
- signing_key:
76
+ rubygems_version: 3.4.10
77
+ signing_key:
162
78
  specification_version: 4
163
- summary: ActiveOutbox
79
+ summary: A Transactional Outbox implementation for ActiveRecord
164
80
  test_files: []
@@ -1,26 +0,0 @@
1
- module ActiveOutbox
2
- class << self
3
- attr_accessor :configuration
4
-
5
- def configuration
6
- @configuration ||= ActiveOutbox::Configuration.new
7
- end
8
-
9
- def reset
10
- @configuration = ActiveOutbox::Configuration.new
11
- end
12
-
13
- def configure
14
- yield(configuration)
15
- end
16
- end
17
-
18
- class Configuration
19
- attr_accessor :adapter, :outbox_mapping
20
-
21
- def initialize
22
- @adapter = :sqlite
23
- @outbox_mapping = {}
24
- end
25
- end
26
- end