after_commit_changes 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []