active_outbox 0.0.1 → 0.0.3

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: 114be65b9ebb769c48aebe0079432a121fb2f71b45ae912668eec8b0416a1761
4
- data.tar.gz: c0d9b1e730354d515c7ac9096369cce22774cc5b3ad5d9c77a0d5ace01d39e39
3
+ metadata.gz: f366197388646246ec6d819c6877f846fd732a3f0df79673c23e7d5883057d78
4
+ data.tar.gz: 72a2af511d588e32622444068d1e26cc57114debd08cade8b88399cf3ad297e8
5
5
  SHA512:
6
- metadata.gz: 94d42500e7272e0cefee12db79054308013edf4afdb6133a1f9a49f011e68596bbc005d4e2419d383dfb7f451558d1cd8bf63ae382a7a54d214c5847b0f6ff18
7
- data.tar.gz: e8b82acd7157417084c076f307da90a9138a6f424c0fae86f93190613dd47e409dde33407ab0397fc2b8464ccbfef67cb699147d52df7e1d263846dd143c2550
6
+ metadata.gz: c3da7a34231c3161e4f65291aa78b304d94213d06cdefa7f0e5cfa0318772db56906ee27391610be9bcd18e640db4481c00eee40b9341f183b5877063c3d1120
7
+ data.tar.gz: b6d2e214d3dcaef57c7f774a934a814dca2c4d8a355d79721a0cfcc3d8f377bec36f7f1dd289d36a06fbcc6a695a09c6f61027fa6177d0e39419abda0c688492
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
@@ -1,9 +1,28 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveOutbox
4
+ class << self
5
+ attr_accessor :configuration
6
+
7
+ def configuration
8
+ @configuration ||= ActiveOutbox::Configuration.new
9
+ end
10
+
11
+ def reset
12
+ @configuration = ActiveOutbox::Configuration.new
13
+ end
14
+
15
+ def configure
16
+ yield(configuration)
17
+ end
18
+ end
19
+
2
20
  class Configuration
3
- attr_accessor :adapter
21
+ attr_accessor :adapter, :outbox_mapping
4
22
 
5
23
  def initialize
6
- @adatper = :sqlite
24
+ @adapter = :sqlite
25
+ @outbox_mapping = {}
7
26
  end
8
27
  end
9
28
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveOutbox
4
+ class OutboxConfigurationError < StandardError; end
5
+
6
+ class OutboxClassNotFoundError < OutboxConfigurationError
7
+ def message
8
+ <<~MESSAGE
9
+ Missing Outbox class definition. Configure mapping in `config/initializers/active_outbox.rb`:
10
+
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
19
+ end
20
+ end
21
+ end
@@ -1,17 +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
- argument :domains, type: :array, default: [], banner: "domain domain"
9
11
  class_option :root_components_path, type: :string, default: Rails.root
10
12
 
11
13
  def create_migration_files
12
14
  migration_path = "#{options['root_components_path']}/db/migrate"
13
15
  migration_template(
14
- "migration.rb",
16
+ 'migration.rb',
15
17
  "#{migration_path}/outbox_create_#{table_name}.rb",
16
18
  migration_version: migration_version
17
19
  )
@@ -24,16 +26,4 @@ class OutboxGenerator < ActiveRecord::Generators::Base
24
26
  def table_name
25
27
  "#{name}_outboxes"
26
28
  end
27
-
28
- def uuid_type
29
- postgres? ? 'uuid' : 'string'
30
- end
31
-
32
- def json_type
33
- postgres? ? 'jsonb' : 'string'
34
- end
35
-
36
- def postgres?
37
- ActiveRecord::Base.connection.adapter_name.downcase == 'postgresql'
38
- end
39
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|
@@ -4,8 +4,7 @@ module ActiveOutbox
4
4
  module Outboxable
5
5
  extend ActiveSupport::Concern
6
6
 
7
- included do |base|
8
- base.extend(ClassMethods)
7
+ included do
9
8
  *namespace, klass = name.underscore.upcase.split('/')
10
9
  namespace = namespace.reverse.join('.')
11
10
 
@@ -25,37 +24,23 @@ module ActiveOutbox
25
24
  end
26
25
 
27
26
  def save(**options, &block)
28
- if options[:outbox_event].present?
29
- @outbox_event = options[:outbox_event].underscore.upcase
30
- end
31
-
27
+ assign_outbox_event(options)
32
28
  super(**options, &block)
33
29
  end
34
30
 
35
31
  def save!(**options, &block)
36
- if options[:outbox_event].present?
37
- @outbox_event = options[:outbox_event].underscore.upcase
38
- end
39
-
32
+ assign_outbox_event(options)
40
33
  super(**options, &block)
41
34
  end
42
35
 
43
- module ClassMethods
44
- def parent_module_name
45
- module_parent == Object ? '' : module_parent.name
46
- end
36
+ private
47
37
 
48
- def outbox_model
49
- @outbox_model ||= ActiveOutbox::Base.subclasses.find do |klass|
50
- klass.name.include?(parent_module_name)
51
- end
52
- end
38
+ def assign_outbox_event(options)
39
+ @outbox_event = options[:outbox_event].underscore.upcase if options[:outbox_event].present?
53
40
  end
54
41
 
55
- private
56
-
57
42
  def create_outbox!(action, event_name)
58
- outbox = self.class.outbox_model.new(
43
+ outbox = outbox_model.new(
59
44
  aggregate: self.class.name,
60
45
  aggregate_identifier: try(:identifier) || id,
61
46
  event: @outbox_event || event_name,
@@ -64,41 +49,59 @@ module ActiveOutbox
64
49
  )
65
50
  @outbox_event = nil
66
51
 
67
- if outbox.invalid?
68
- outbox.errors.each do |error|
69
- self.errors.import(error, attribute: "outbox.#{error.attribute}")
70
- 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)
71
62
  end
72
63
 
73
- outbox.save!
64
+ module_parent.const_get('OUTBOX_MODEL')
74
65
  end
75
66
 
76
- def formatted_payload(action)
77
- payload = payload(action)
78
- case ActiveRecord::Base.connection.adapter_name.downcase
79
- when 'postgresql'
80
- payload
81
- else
82
- 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.configuration.outbox_mapping[namespace&.underscore]
75
+ end
76
+
77
+ def default_outbox_mapping
78
+ ActiveOutbox.configuration.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}")
83
84
  end
84
85
  end
85
86
 
86
- def payload(action)
87
- 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)
88
93
  case action
89
94
  when :create
90
- payload[:after] = as_json
95
+ { before: nil, after: as_json }
91
96
  when :update
92
- # previous_changes => { 'name' => ['bob', 'robert'] }
93
97
  changes = previous_changes.transform_values(&:first)
94
- payload[:before] = as_json.merge(changes)
95
- payload[:after] = as_json
98
+ { before: as_json.merge(changes), after: as_json }
96
99
  when :destroy
97
- payload[:before] = as_json
100
+ { before: as_json, after: nil }
98
101
  else
99
- 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)
100
104
  end
101
- payload
102
105
  end
103
106
  end
104
107
  end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveOutbox
4
+ class Railtie < Rails::Railtie
5
+ end
6
+ end
data/lib/active_outbox.rb CHANGED
@@ -1,26 +1,8 @@
1
- require 'active_outbox/base'
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_outbox/adapter_helper'
2
4
  require 'active_outbox/configuration'
5
+ require 'active_outbox/errors'
6
+ require 'active_outbox/generators/active_outbox_generator'
3
7
  require 'active_outbox/outboxable'
4
- require 'active_outbox/generators/outbox_generator'
5
-
6
- module ActiveOutbox
7
- class << self
8
- attr_accessor :configuration
9
-
10
- def configuration
11
- @configuration ||= Configuration.new
12
- end
13
-
14
- def reset
15
- @configuration = Configuration.new
16
- end
17
-
18
- def configure
19
- yield(configuration)
20
- end
21
- end
22
-
23
- def configuration
24
- @configuration ||= Configuration.new
25
- end
26
- end
8
+ require 'active_outbox/railtie' if defined?(Rails::Railtie)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_outbox
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.3
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-06-28 00:00:00.000000000 Z
11
+ date: 2023-10-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -16,113 +16,15 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '7.0'
19
+ version: '6.1'
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
69
- - !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
113
- requirement: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - "~>"
116
- - !ruby/object:Gem::Version
117
- version: 11.1.3
118
- type: :development
119
- prerelease: false
120
- version_requirements: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - "~>"
123
- - !ruby/object:Gem::Version
124
- version: 11.1.3
125
- description: A Transactional Outbox implementation for ActiveRecord
26
+ version: '6.1'
27
+ description:
126
28
  email: guillermoaguirre1@gmail.com
127
29
  executables:
128
30
  - outbox
@@ -132,16 +34,18 @@ files:
132
34
  - README.md
133
35
  - bin/outbox
134
36
  - lib/active_outbox.rb
135
- - lib/active_outbox/base.rb
37
+ - lib/active_outbox/adapter_helper.rb
136
38
  - lib/active_outbox/configuration.rb
137
- - lib/active_outbox/generators/outbox_generator.rb
39
+ - lib/active_outbox/errors.rb
40
+ - lib/active_outbox/generators/active_outbox_generator.rb
138
41
  - lib/active_outbox/generators/templates/migration.rb
139
42
  - lib/active_outbox/outboxable.rb
43
+ - lib/active_outbox/railtie.rb
140
44
  homepage: https://rubygems.org/gems/active_outbox
141
45
  licenses:
142
46
  - MIT
143
47
  metadata: {}
144
- post_install_message:
48
+ post_install_message:
145
49
  rdoc_options: []
146
50
  require_paths:
147
51
  - lib
@@ -149,15 +53,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
149
53
  requirements:
150
54
  - - ">="
151
55
  - !ruby/object:Gem::Version
152
- version: '0'
56
+ version: '3.0'
153
57
  required_rubygems_version: !ruby/object:Gem::Requirement
154
58
  requirements:
155
59
  - - ">="
156
60
  - !ruby/object:Gem::Version
157
61
  version: '0'
158
62
  requirements: []
159
- rubygems_version: 3.1.6
160
- signing_key:
63
+ rubygems_version: 3.4.10
64
+ signing_key:
161
65
  specification_version: 4
162
- summary: ActiveOutbox
66
+ summary: A Transactional Outbox implementation for ActiveRecord
163
67
  test_files: []
@@ -1,7 +0,0 @@
1
- require 'active_record'
2
-
3
- module ActiveOutbox
4
- class Base < ActiveRecord::Base
5
- self.abstract_class = true
6
- end
7
- end