after_commit_changes 1.0.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 69c7f5e46ca54be22ea0fecc939d4a092681e5183f3f80ef7850a7b4588aa702
4
+ data.tar.gz: 03c2c226e947acab43f004a8b98bfe6f0b27f3959a1abde8b421715861324442
5
+ SHA512:
6
+ metadata.gz: c478de53032f858476b399ec9aff94aa46b14f8ccb341a29332f7cbfcd037a6fb7b3d85244172042f88c012550c7c876ee0f664c8c6af875eeb57044720ef40b
7
+ data.tar.gz: 99e87f8493b2fe465ef4d14866691e4fb6ffffefbd8e8dd5e193ae262961e51a82546a2e0d43480e8bbb0cc383dd52356963489824f8ed467ac235255843fe04
data/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## 1.0.0
8
+
9
+ ### Added
10
+ - Initial release
data/MIT_LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2023 Brian Durand
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # AfterCommitChanges
2
+
3
+ [![Continuous Integration](https://github.com/bdurand/after_commit_changes/actions/workflows/continuous_integration.yml/badge.svg)](https://github.com/bdurand/after_commit_changes/actions/workflows/continuous_integration.yml)
4
+ [![Regression Test](https://github.com/bdurand/after_commit_changes/actions/workflows/regression_test.yml/badge.svg)](https://github.com/bdurand/after_commit_changes/actions/workflows/regression_test.yml)
5
+ [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
6
+ [![Gem Version](https://badge.fury.io/rb/after_commit_changes.svg)](https://badge.fury.io/rb/after_commit_changes)
7
+
8
+ This gem addresses an [issue in ActiveRecord](https://github.com/rails/rails/pull/50011) with the `saved_changes` value when a record is updated multiple times in a single database transaction.
9
+
10
+ After a record is saved, you can check the set of changes with the `saved_changes` method using the [ActiveModel::Dirty API](https://api.rubyonrails.org/classes/ActiveModel/Dirty.html). However, when a record is saved multiple times, the list of saved changes is reset with each save operation. This can be an issue inside of an `after_commit` or `before_commit` callback since those callbacks are only called once for the transaction and will only get the last set of changes.
11
+
12
+ This can be a problem if you have a callback that checks for changes to specific fields. Consider this model where we want to run an asychronous job when a user changes their email address:
13
+
14
+ ```ruby
15
+ class User < ApplicationRecord
16
+ after_commit :notify_email_changes, if: :email_changed?
17
+
18
+ def notify_email_changes
19
+ NotifyEmailChangesJob.perform_later(id)
20
+ end
21
+ end
22
+ ```
23
+
24
+ This breaks down if a record is saved twice in a single transaction.
25
+
26
+ ```ruby
27
+ user.transaction do
28
+ user.update!(email: params[:email])
29
+ if user.last_visited_at < 1.day.ago
30
+ user.update!(last_visited_at: Time.now)
31
+ end
32
+ end
33
+ ```
34
+
35
+ In the case where we update the `last_visited_at` field, the `email_changed?` method will return false since the email address was not changed in the last save operation and `notify_email_changes` method will not be called.
36
+
37
+ This gem addresses this issue by merging all saved changes together before calling the `after_commit` or `before_commit` callbacks so that `saved_changes` will return the complete list of changes for the transaction.
38
+
39
+ ## Usage
40
+
41
+ To use the gem, you simply need to mix it into your models. You can include it in all models by including it in your `ApplicationRecord` class:
42
+
43
+ ```ruby
44
+ class ApplicationRecord < ActiveRecord::Base
45
+ include AfterCommitChanges
46
+ end
47
+ ```
48
+
49
+ ## Installation
50
+
51
+ Add this line to your application's Gemfile:
52
+
53
+ ```ruby
54
+ gem "after_commit_changes"
55
+ ```
56
+
57
+ Then execute:
58
+ ```bash
59
+ $ bundle
60
+ ```
61
+
62
+ Or install it yourself as:
63
+ ```bash
64
+ $ gem install gem "after_commit_changes"
65
+ ```
66
+
67
+ ## Contributing
68
+
69
+ Open a pull request on GitHub.
70
+
71
+ Please use the [standardrb](https://github.com/testdouble/standard) syntax and lint your code with `standardrb --fix` before submitting.
72
+
73
+ ## License
74
+
75
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,36 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = "after_commit_changes"
3
+ spec.version = File.read(File.join(__dir__, "VERSION")).strip
4
+ spec.authors = ["Brian Durand"]
5
+ spec.email = ["bbdurand@gmail.com"]
6
+
7
+ spec.summary = "Aggregate all changes made to an ActiveRecord model inside a transaction into a single set of changes."
8
+ spec.homepage = "https://github.com/bdurand/after_commit_changes"
9
+ spec.license = "MIT"
10
+
11
+ # Specify which files should be added to the gem when it is released.
12
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
13
+ ignore_files = %w[
14
+ .
15
+ Appraisals
16
+ Gemfile
17
+ Gemfile.lock
18
+ Rakefile
19
+ config.ru
20
+ assets/
21
+ bin/
22
+ gemfiles/
23
+ spec/
24
+ ]
25
+ spec.files = Dir.chdir(__dir__) do
26
+ `git ls-files -z`.split("\x0").reject { |f| ignore_files.any? { |path| f.start_with?(path) } }
27
+ end
28
+
29
+ spec.require_paths = ["lib"]
30
+
31
+ spec.add_dependency "activerecord", ">= 6.0"
32
+
33
+ spec.add_development_dependency "bundler"
34
+
35
+ spec.required_ruby_version = ">= 2.5"
36
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AfterCommitChanges
4
+ VERSION = File.read(File.expand_path("../VERSION", __dir__)).strip
5
+
6
+ def self.included(base)
7
+ base.before_commit do
8
+ rollup_mutations_for_transaction!
9
+ end
10
+
11
+ base.after_save do
12
+ @after_commit_saved_changes ||= []
13
+ @after_commit_saved_changes << saved_changes
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def rollup_mutations_for_transaction!
20
+ return unless @after_commit_saved_changes && @after_commit_saved_changes.size > 1
21
+
22
+ attributes = @_start_transaction_state[:attributes].deep_dup
23
+ mutations = ActiveModel::AttributeMutationTracker.new(attributes)
24
+
25
+ @after_commit_saved_changes[1, @after_commit_saved_changes.length].each do |changes|
26
+ changes.each do |attr_name, value_change|
27
+ attribute = attributes[attr_name]
28
+ last_value = value_change.last
29
+ last_value = last_value.to_h if last_value.is_a?(ActiveSupport::HashWithIndifferentAccess)
30
+ attributes[attr_name] = ActiveModel::Attribute.from_user(attr_name, last_value, attribute.type, attribute)
31
+ mutations.force_change(attr_name) unless mutations.changed?(attr_name)
32
+ end
33
+ end
34
+
35
+ @after_commit_saved_changes = nil
36
+ @mutations_before_last_save = mutations
37
+ end
38
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: after_commit_changes
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Brian Durand
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-11-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '6.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '6.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description:
42
+ email:
43
+ - bbdurand@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - CHANGELOG.md
49
+ - MIT_LICENSE.txt
50
+ - README.md
51
+ - VERSION
52
+ - after_commit_changes.gemspec
53
+ - lib/after_commit_changes.rb
54
+ homepage: https://github.com/bdurand/after_commit_changes
55
+ licenses:
56
+ - MIT
57
+ metadata: {}
58
+ post_install_message:
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '2.5'
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ requirements: []
73
+ rubygems_version: 3.4.20
74
+ signing_key:
75
+ specification_version: 4
76
+ summary: Aggregate all changes made to an ActiveRecord model inside a transaction
77
+ into a single set of changes.
78
+ test_files: []