changed 0.0.1 → 1.2.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 +4 -4
- data/LICENSE +20 -0
- data/README.md +93 -10
- data/Rakefile +9 -3
- data/app/models/changed/application_record.rb +5 -0
- data/app/models/changed/association.rb +12 -0
- data/app/models/changed/audit.rb +109 -0
- data/db/migrate/20180213015838_create_changed_audits.rb +13 -0
- data/db/migrate/20180213015849_create_changed_associations.rb +14 -0
- data/lib/changed.rb +85 -2
- data/lib/changed/auditable.rb +51 -0
- data/lib/changed/builder.rb +117 -0
- data/lib/changed/config.rb +5 -0
- data/lib/changed/engine.rb +10 -0
- data/lib/changed/version.rb +1 -1
- metadata +162 -33
- data/.gitignore +0 -11
- data/.rspec +0 -3
- data/.travis.yml +0 -5
- data/Gemfile +0 -6
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/changed.gemspec +0 -24
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 53782f67f91f94fb6371707c6898b36e531ca972ce8d9fe404089b9983d91740
|
4
|
+
data.tar.gz: d7d9968239f4e0a1c857abf6fd1aeb8938630d04a814927f7213de8cdceb2282
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 06473d2413c6f76544c63eced68807389cae2ce1dbea1024fc5b9e8a2c7856088ed0b90074b8d8a528b08a70be24c1ba34cc652397e07fcb37c317787026f331
|
7
|
+
data.tar.gz: e3d4a686dc92c98adcd378f66948645aca010e68555ff282e3d9891c6a91cc8065983b3e23462980fbd1af2af40823f06d33337ea71ccc1591c0a75bbe9449f6
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2018 Clutter
|
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
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# Changed
|
2
2
|
|
3
|
-
|
3
|
+
[](https://badge.fury.io/rb/changed)
|
4
4
|
|
5
|
-
|
5
|
+
A gem for tracking what **changed** when.
|
6
6
|
|
7
7
|
## Installation
|
8
8
|
|
@@ -14,22 +14,105 @@ gem 'changed'
|
|
14
14
|
|
15
15
|
And then execute:
|
16
16
|
|
17
|
-
|
17
|
+
```bash
|
18
|
+
$ bundle
|
19
|
+
```
|
18
20
|
|
19
21
|
Or install it yourself as:
|
20
22
|
|
21
|
-
|
23
|
+
```bash
|
24
|
+
$ gem install changed
|
25
|
+
```
|
26
|
+
|
27
|
+
After installing the gem run the following to setup:
|
28
|
+
|
29
|
+
```bash
|
30
|
+
rails changed:install:migrations
|
31
|
+
rails db:migrate
|
32
|
+
```
|
22
33
|
|
23
34
|
## Usage
|
24
35
|
|
25
|
-
|
36
|
+
This gem is designed to integrate with active record objects:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
class Employee
|
40
|
+
include Changed::Auditable
|
41
|
+
belongs_to :company
|
42
|
+
|
43
|
+
audited :name, :email, :eid, :company, transformations: { eid: 'Employee ID' }
|
44
|
+
end
|
45
|
+
```
|
46
|
+
|
47
|
+
To ensure the proper 'changer' is tracked, add the following code to your application controller:
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
before_action :configure_audit_changer
|
51
|
+
|
52
|
+
protected
|
53
|
+
|
54
|
+
def configure_audit_changer
|
55
|
+
Changed.changer = User.current
|
56
|
+
end
|
57
|
+
```
|
26
58
|
|
27
|
-
|
59
|
+
To execute code with a different timestamp or changer, use the following:
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
employee = Employee.find_by(name: "...")
|
63
|
+
Changed.perform(changer: User.current, timestamp: Time.now) do
|
64
|
+
employee.name = "..."
|
65
|
+
employee.save!
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
### Fields
|
70
|
+
|
71
|
+
Fields (i.e. name, email, phone, etc) are tracked inside the `changeset` key of a generated audit. They can be queried using:
|
72
|
+
|
73
|
+
```sql
|
74
|
+
SELECT
|
75
|
+
"audits"."timestamp",
|
76
|
+
"audits"."changeset"->'name'->>0 AS "was",
|
77
|
+
"audits"."changeset"->'name'->>1 AS "now",
|
78
|
+
"changers"."name" AS "changer"
|
79
|
+
FROM "audits"
|
80
|
+
JOIN "users" AS "changers" ON "audits"."changer_id" = "changers"."id" AND "audits"."changer_type" = 'User'
|
81
|
+
WHERE "audits"."changeset"->>'name' IS NOT NULL
|
82
|
+
```
|
83
|
+
|
84
|
+
### Associations
|
85
|
+
|
86
|
+
Associations (i.e. user, favourites, etc) are tracked by the `associations` table. They can be queried using:
|
87
|
+
|
88
|
+
```sql
|
89
|
+
SELECT
|
90
|
+
"audits"."timestamp",
|
91
|
+
"changers"."name" AS "changer",
|
92
|
+
CASE "associations"."kind"
|
93
|
+
WHEN '0' THEN 'ADD'
|
94
|
+
WHEN '1' THEN 'REMOVE'
|
95
|
+
END AS "kind",
|
96
|
+
"users"."name" AS "user"
|
97
|
+
FROM "audits"
|
98
|
+
JOIN "associations" ON "associations"."audit_id" = "audits"."id"
|
99
|
+
JOIN "users" ON "associations"."associated_id" = "users"."id" AND "associations"."associated_type" = 'User'
|
100
|
+
JOIN "users" AS "changers" ON "audits"."changer_id" = "changers"."id" AND "audits"."changer_type" = 'User'
|
101
|
+
WHERE "associations"."name" = 'user'
|
102
|
+
```
|
103
|
+
|
104
|
+
## Configuration
|
105
|
+
|
106
|
+
Specifying `default_changer_proc` gives a changer if one cannot be inferred otherwise:
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
Changed.config.default_changer_proc = ->{ User.system }
|
110
|
+
```
|
28
111
|
|
29
|
-
|
112
|
+
## Status
|
30
113
|
|
31
|
-
|
114
|
+
[](https://circleci.com/gh/clutter/changed)
|
32
115
|
|
33
|
-
##
|
116
|
+
## License
|
34
117
|
|
35
|
-
|
118
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
CHANGED
@@ -1,6 +1,12 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'bundler/gem_tasks'
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
|
5
|
+
APP_RAKEFILE = File.expand_path('../spec/dummy/Rakefile', __FILE__)
|
6
|
+
|
7
|
+
load 'rails/tasks/engine.rake'
|
8
|
+
load 'rails/tasks/statistics.rake'
|
3
9
|
|
4
10
|
RSpec::Core::RakeTask.new(:spec)
|
5
11
|
|
6
|
-
task :
|
12
|
+
task default: :spec
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Changed
|
2
|
+
class Association < ApplicationRecord
|
3
|
+
belongs_to :audit, class_name: 'Changed::Audit'
|
4
|
+
belongs_to :associated, polymorphic: true
|
5
|
+
|
6
|
+
enum kind: %i[add remove]
|
7
|
+
|
8
|
+
validates :name, presence: true
|
9
|
+
|
10
|
+
scope :ordered, -> { order(id: :desc) }
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module Changed
|
2
|
+
class Audit < ApplicationRecord
|
3
|
+
module Event
|
4
|
+
CREATE = 'create'.freeze
|
5
|
+
UPDATE = 'update'.freeze
|
6
|
+
end
|
7
|
+
|
8
|
+
EVENTS = [
|
9
|
+
Event::CREATE,
|
10
|
+
Event::UPDATE,
|
11
|
+
].freeze
|
12
|
+
|
13
|
+
belongs_to :changer, polymorphic: true, required: false
|
14
|
+
belongs_to :audited, polymorphic: true
|
15
|
+
has_many :associations, dependent: :destroy, class_name: 'Changed::Association'
|
16
|
+
|
17
|
+
scope :optimized, -> { preload(:changer, :audited, associations: :associated) }
|
18
|
+
scope :ordered, -> { order(id: :desc) }
|
19
|
+
scope :creates, -> { where(event: Event::CREATE) }
|
20
|
+
scope :updates, -> { where(event: Event::UPDATE) }
|
21
|
+
|
22
|
+
validates :event, inclusion: { in: EVENTS }
|
23
|
+
validates :audited, presence: true
|
24
|
+
|
25
|
+
scope :for, ->(audited) { where(audited: audited).ordered }
|
26
|
+
|
27
|
+
after_initialize -> { self.timestamp ||= (Changed.timestamp || Time.now) }
|
28
|
+
|
29
|
+
def fields
|
30
|
+
changeset.map do |name, value|
|
31
|
+
was, now = value
|
32
|
+
Field.new(was, now, transform(name))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def relationships
|
37
|
+
memo = {}
|
38
|
+
associations.each do |association|
|
39
|
+
memo[association.name] ||= Set.new
|
40
|
+
memo[association.name] << association
|
41
|
+
end
|
42
|
+
memo.map do |name, associations|
|
43
|
+
Relationship.new(associations, transform(name))
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def track(event, fields)
|
48
|
+
self.changer = Changed.changer
|
49
|
+
|
50
|
+
fields.each do |attribute|
|
51
|
+
attribute = String(attribute)
|
52
|
+
if audited.saved_change_to_attribute?(attribute)
|
53
|
+
changeset[attribute] = audited.saved_change_to_attribute(attribute)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
self.event = event
|
58
|
+
end
|
59
|
+
|
60
|
+
def anything?
|
61
|
+
changeset.any? || associations.any?
|
62
|
+
end
|
63
|
+
|
64
|
+
def changed?(key)
|
65
|
+
fields.map(&:name).include?(key)
|
66
|
+
end
|
67
|
+
|
68
|
+
# The 'change' provided needs to be a block, lambda, or proc that executes
|
69
|
+
# the changes. The other provided block is yielded pre and post the change.
|
70
|
+
def track_attribute_change(attribute, change)
|
71
|
+
attribute_was = yield
|
72
|
+
change.call
|
73
|
+
attribute_now = yield
|
74
|
+
|
75
|
+
return if attribute_was == attribute_now
|
76
|
+
|
77
|
+
changeset[String(attribute)] = [
|
78
|
+
attribute_was,
|
79
|
+
attribute_now,
|
80
|
+
]
|
81
|
+
end
|
82
|
+
|
83
|
+
def track_association_change(name, change)
|
84
|
+
association_was = yield
|
85
|
+
change.call
|
86
|
+
association_now = yield
|
87
|
+
return if association_was == association_now
|
88
|
+
|
89
|
+
(association_was - association_now).each do |associated|
|
90
|
+
associations.build(name: name, associated: associated, kind: :remove)
|
91
|
+
end
|
92
|
+
(association_now - association_was).each do |associated|
|
93
|
+
associations.build(name: name, associated: associated, kind: :add)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def transform(name)
|
100
|
+
(transformations[name] if transformations) || name
|
101
|
+
end
|
102
|
+
|
103
|
+
def transformations
|
104
|
+
@transformations = audited.class.auditable[:transformations] unless defined?(@transformations)
|
105
|
+
@transformations
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class CreateChangedAudits < ActiveRecord::Migration[6.0]
|
2
|
+
def change
|
3
|
+
create_table :changed_audits do |t|
|
4
|
+
t.references :changer, polymorphic: true, null: true, index: true
|
5
|
+
t.references :audited, polymorphic: true, null: false, index: true
|
6
|
+
t.jsonb :changeset, default: {}, null: false
|
7
|
+
t.string :event, null: false
|
8
|
+
t.datetime :timestamp, null: false
|
9
|
+
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class CreateChangedAssociations < ActiveRecord::Migration[6.0]
|
2
|
+
def change
|
3
|
+
create_table :changed_associations do |t|
|
4
|
+
t.references :audit, null: false, index: true
|
5
|
+
t.references :associated, null: false, polymorphic: true, index: true
|
6
|
+
t.string :name, null: false
|
7
|
+
t.integer :kind, null: false
|
8
|
+
|
9
|
+
t.timestamps
|
10
|
+
end
|
11
|
+
|
12
|
+
add_foreign_key :changed_associations, :changed_audits, column: :audit_id
|
13
|
+
end
|
14
|
+
end
|
data/lib/changed.rb
CHANGED
@@ -1,5 +1,88 @@
|
|
1
|
-
require
|
1
|
+
require 'request_store'
|
2
|
+
|
3
|
+
require 'changed/auditable'
|
4
|
+
require 'changed/builder'
|
5
|
+
require 'changed/config'
|
6
|
+
require 'changed/engine'
|
2
7
|
|
3
8
|
module Changed
|
4
|
-
|
9
|
+
Field = Struct.new(:was, :now, :name)
|
10
|
+
Relationship = Struct.new(:associations, :name)
|
11
|
+
|
12
|
+
# Access the library configuration.
|
13
|
+
#
|
14
|
+
# ==== Examples
|
15
|
+
#
|
16
|
+
# Changed.config.default_changer_proc = ->{ User.system }
|
17
|
+
def self.config
|
18
|
+
@config ||= Config.new
|
19
|
+
end
|
20
|
+
|
21
|
+
# Access the timestamp (this value is set as the timestamp within an audit and defaults to now).
|
22
|
+
def self.timestamp
|
23
|
+
options[:timestamp] || Time.now
|
24
|
+
end
|
25
|
+
|
26
|
+
# Customize the timestamp (uses a request store to only change lifeycle event).
|
27
|
+
#
|
28
|
+
# ==== Attributes
|
29
|
+
#
|
30
|
+
# * +timestamp+ - A timestamp to use.
|
31
|
+
#
|
32
|
+
# ==== Examples
|
33
|
+
#
|
34
|
+
# Changed.timestamp = 2.hours.ago
|
35
|
+
def self.timestamp=(timestamp)
|
36
|
+
options[:timestamp] = timestamp
|
37
|
+
end
|
38
|
+
|
39
|
+
# Access the changer (this value is set as the changer within an audit and defaults to config).
|
40
|
+
def self.changer
|
41
|
+
options[:changer] || config.default_changer_proc&.call
|
42
|
+
end
|
43
|
+
|
44
|
+
# Customize the changer (uses a request store to only change lifeycle event).
|
45
|
+
#
|
46
|
+
# ==== Attributes
|
47
|
+
#
|
48
|
+
# * +changer+ - A changer to use.
|
49
|
+
#
|
50
|
+
# ==== Examples
|
51
|
+
#
|
52
|
+
# Changed.changer = User.current
|
53
|
+
def self.changer=(changer)
|
54
|
+
options[:changer] = changer
|
55
|
+
end
|
56
|
+
|
57
|
+
# Perform a block with custom override options.
|
58
|
+
#
|
59
|
+
# ==== Attributes
|
60
|
+
#
|
61
|
+
# * +options+ - Values for the changer and / or timestamp.
|
62
|
+
# * +block+ - Some code to run with the new options.
|
63
|
+
#
|
64
|
+
# ==== Examples
|
65
|
+
#
|
66
|
+
# Changed.perform(changer: User.system, timestamp: 2.hours.ago) do
|
67
|
+
# widget.name = "Sprocket"
|
68
|
+
# widget.save!
|
69
|
+
# end
|
70
|
+
def self.perform(options = {}, &block)
|
71
|
+
backup = self.options
|
72
|
+
self.options = options
|
73
|
+
block.call
|
74
|
+
ensure
|
75
|
+
self.options = backup
|
76
|
+
end
|
77
|
+
|
78
|
+
OPTIONS_REQUEST_STORE_KEY = :changed_options
|
79
|
+
private_constant :OPTIONS_REQUEST_STORE_KEY
|
80
|
+
|
81
|
+
def self.options=(options)
|
82
|
+
RequestStore.store[OPTIONS_REQUEST_STORE_KEY] = options
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.options
|
86
|
+
RequestStore.store[OPTIONS_REQUEST_STORE_KEY] ||= {}
|
87
|
+
end
|
5
88
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module Changed
|
4
|
+
module Auditable
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
# ==== Overview
|
8
|
+
#
|
9
|
+
# A helper that caches an audit between operations. Once an audit is persisted this method handles the generation
|
10
|
+
# of a new audit, thus ensuring that each transaction is audited separately.
|
11
|
+
def audit
|
12
|
+
@audit = Audit.new(audited: self) if @audit.nil? || @audit.persisted?
|
13
|
+
@audit
|
14
|
+
end
|
15
|
+
|
16
|
+
included do
|
17
|
+
has_many :audits, -> { ordered }, as: :audited, class_name: 'Changed::Audit', dependent: :destroy
|
18
|
+
|
19
|
+
# ==== Overview
|
20
|
+
#
|
21
|
+
# A helper for setting up options.
|
22
|
+
#
|
23
|
+
def self.auditable
|
24
|
+
@auditable ||= {}
|
25
|
+
end
|
26
|
+
|
27
|
+
# ==== Overview
|
28
|
+
#
|
29
|
+
# A concern for setting up auditable for a model. An audited model can track the changes to attributes
|
30
|
+
# (ints, bools, strings, dates, times) or associations (`has_many`, `belongs_to`, `has_and_belongs_to_many`).
|
31
|
+
#
|
32
|
+
# The `audit` call needs to be placed after all `has_many`, `belongs_to`, `has_and_belongs_to_many` declarations
|
33
|
+
# in order for the association reflection to work. Multiple inclusions of `audit` are not supported.
|
34
|
+
#
|
35
|
+
# ==== Options
|
36
|
+
#
|
37
|
+
# * +:keys:+ - An array of symbols for the attributes or associations that are tracked with each audit.
|
38
|
+
# * +:transformations:+ - A hash of of attribute name mappings (i.e. 'number' to '#' or 'user' to 'rep').
|
39
|
+
#
|
40
|
+
# ==== Usage
|
41
|
+
#
|
42
|
+
# audit(:number, :scheduled, :region, :address, :items, transformations: { number: "#", user: "rep" })
|
43
|
+
#
|
44
|
+
def self.audited(*keys, transformations: nil)
|
45
|
+
auditable[:transformations] = ActiveSupport::HashWithIndifferentAccess.new(transformations) if transformations
|
46
|
+
Builder.build(self, *keys)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module Changed
|
2
|
+
class Builder
|
3
|
+
class ArgumentError < ::ArgumentError
|
4
|
+
end
|
5
|
+
|
6
|
+
ARGUMENT_ERROR_EMPTY_KEYS_MESSAGE = 'audited requires specifying a splat of keys'.freeze
|
7
|
+
|
8
|
+
def self.build(*args)
|
9
|
+
new(*args).build
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(klass, *keys)
|
13
|
+
raise ArgumentError, ARGUMENT_ERROR_EMPTY_KEYS_MESSAGE if keys.empty?
|
14
|
+
|
15
|
+
@klass = klass
|
16
|
+
@keys = keys
|
17
|
+
end
|
18
|
+
|
19
|
+
def build
|
20
|
+
define_callbacks_for_associations
|
21
|
+
define_after_create_callback
|
22
|
+
define_after_update_callback
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def define_callbacks_for_associations
|
28
|
+
@keys.each do |key|
|
29
|
+
association = @klass.reflect_on_association(key)
|
30
|
+
case association
|
31
|
+
when ActiveRecord::Reflection::HasManyReflection,
|
32
|
+
ActiveRecord::Reflection::HasAndBelongsToManyReflection,
|
33
|
+
ActiveRecord::Reflection::ThroughReflection
|
34
|
+
define_callbacks_for_has_many(association)
|
35
|
+
when ActiveRecord::Reflection::BelongsToReflection
|
36
|
+
define_callbacks_for_belongs_to(association)
|
37
|
+
when ActiveRecord::Reflection::HasOneReflection
|
38
|
+
define_callbacks_for_has_one(association)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def after_create_or_update_for_belongs_to_callback(key, foreign_key, foreign_type, class_name)
|
44
|
+
proc do |resource|
|
45
|
+
was_associated_id, now_associated_id = resource.saved_change_to_attribute(foreign_key)
|
46
|
+
was_associated_type, now_associated_type = resource.saved_change_to_attribute(foreign_type)
|
47
|
+
associated_type = resource[foreign_type] || class_name
|
48
|
+
|
49
|
+
if was_associated_id
|
50
|
+
resource.audit.associations.build(
|
51
|
+
name: key,
|
52
|
+
kind: :remove, associated_id: was_associated_id, associated_type: was_associated_type || associated_type
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
if now_associated_id
|
57
|
+
resource.audit.associations.build(
|
58
|
+
name: key, kind: :add,
|
59
|
+
associated_id: now_associated_id, associated_type: now_associated_type || associated_type
|
60
|
+
)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def define_callbacks_for_belongs_to(association)
|
66
|
+
callback = after_create_or_update_for_belongs_to_callback(
|
67
|
+
association.name,
|
68
|
+
association.foreign_key,
|
69
|
+
association.foreign_type,
|
70
|
+
association.class_name
|
71
|
+
)
|
72
|
+
|
73
|
+
@klass.after_update callback
|
74
|
+
@klass.after_create callback
|
75
|
+
end
|
76
|
+
|
77
|
+
def before_add_for_has_many_callback(name)
|
78
|
+
proc do |_method, resource, associated|
|
79
|
+
resource.audit.associations.build(name: name, associated: associated, kind: :add)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def before_remove_for_has_many_callback(name)
|
84
|
+
proc do |_method, resource, associated|
|
85
|
+
resource.audit.associations.build(name: name, associated: associated, kind: :remove)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def define_callbacks_for_has_many(association)
|
90
|
+
name = association.name
|
91
|
+
@klass.send(:"before_add_for_#{association.name}") << before_add_for_has_many_callback(name)
|
92
|
+
@klass.send(:"before_remove_for_#{association.name}") << before_remove_for_has_many_callback(name)
|
93
|
+
end
|
94
|
+
|
95
|
+
def define_callbacks_for_has_one(association)
|
96
|
+
raise ArgumentError, "unsupported reflection '#{association.name}'"
|
97
|
+
end
|
98
|
+
|
99
|
+
def define_after_create_callback
|
100
|
+
keys = @keys
|
101
|
+
@klass.after_create do |resource|
|
102
|
+
audit = resource.audit
|
103
|
+
audit.track(Audit::Event::CREATE, keys)
|
104
|
+
audit.save! if audit.anything?
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def define_after_update_callback
|
109
|
+
keys = @keys
|
110
|
+
@klass.after_update do |resource|
|
111
|
+
audit = resource.audit
|
112
|
+
audit.track(Audit::Event::UPDATE, keys)
|
113
|
+
audit.save! if audit.anything?
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
data/lib/changed/version.rb
CHANGED
metadata
CHANGED
@@ -1,77 +1,207 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: changed
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kevin Sylvestre
|
8
8
|
autorequire:
|
9
|
-
bindir:
|
9
|
+
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-10-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
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: request_store
|
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: brakeman
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
13
55
|
- !ruby/object:Gem::Dependency
|
14
56
|
name: bundler
|
15
57
|
requirement: !ruby/object:Gem::Requirement
|
16
58
|
requirements:
|
17
|
-
- - "
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
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: byebug
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
18
74
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
75
|
+
version: '0'
|
20
76
|
type: :development
|
21
77
|
prerelease: false
|
22
78
|
version_requirements: !ruby/object:Gem::Requirement
|
23
79
|
requirements:
|
24
|
-
- - "
|
80
|
+
- - ">="
|
25
81
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
82
|
+
version: '0'
|
27
83
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
84
|
+
name: factory_bot_rails
|
29
85
|
requirement: !ruby/object:Gem::Requirement
|
30
86
|
requirements:
|
31
|
-
- - "
|
87
|
+
- - ">="
|
32
88
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
89
|
+
version: '0'
|
34
90
|
type: :development
|
35
91
|
prerelease: false
|
36
92
|
version_requirements: !ruby/object:Gem::Requirement
|
37
93
|
requirements:
|
38
|
-
- - "
|
94
|
+
- - ">="
|
39
95
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
96
|
+
version: '0'
|
41
97
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
98
|
+
name: pg
|
43
99
|
requirement: !ruby/object:Gem::Requirement
|
44
100
|
requirements:
|
45
|
-
- - "
|
101
|
+
- - ">="
|
46
102
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
103
|
+
version: '0'
|
48
104
|
type: :development
|
49
105
|
prerelease: false
|
50
106
|
version_requirements: !ruby/object:Gem::Requirement
|
51
107
|
requirements:
|
52
|
-
- - "
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rspec_junit_formatter
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
53
116
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
55
|
-
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rspec-rails
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: rubocop
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: shoulda-matchers
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: simplecov
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - ">="
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
181
|
+
description: "⏱"
|
56
182
|
email:
|
57
|
-
- kevin@
|
183
|
+
- kevin@clutter.com
|
58
184
|
executables: []
|
59
185
|
extensions: []
|
60
186
|
extra_rdoc_files: []
|
61
187
|
files:
|
62
|
-
-
|
63
|
-
- ".rspec"
|
64
|
-
- ".travis.yml"
|
65
|
-
- Gemfile
|
188
|
+
- LICENSE
|
66
189
|
- README.md
|
67
190
|
- Rakefile
|
68
|
-
-
|
69
|
-
-
|
70
|
-
- changed.
|
191
|
+
- app/models/changed/application_record.rb
|
192
|
+
- app/models/changed/association.rb
|
193
|
+
- app/models/changed/audit.rb
|
194
|
+
- db/migrate/20180213015838_create_changed_audits.rb
|
195
|
+
- db/migrate/20180213015849_create_changed_associations.rb
|
71
196
|
- lib/changed.rb
|
197
|
+
- lib/changed/auditable.rb
|
198
|
+
- lib/changed/builder.rb
|
199
|
+
- lib/changed/config.rb
|
200
|
+
- lib/changed/engine.rb
|
72
201
|
- lib/changed/version.rb
|
73
|
-
homepage:
|
74
|
-
licenses:
|
202
|
+
homepage: https://github.com/clutter/changed
|
203
|
+
licenses:
|
204
|
+
- MIT
|
75
205
|
metadata: {}
|
76
206
|
post_install_message:
|
77
207
|
rdoc_options: []
|
@@ -79,18 +209,17 @@ require_paths:
|
|
79
209
|
- lib
|
80
210
|
required_ruby_version: !ruby/object:Gem::Requirement
|
81
211
|
requirements:
|
82
|
-
- - "
|
212
|
+
- - ">"
|
83
213
|
- !ruby/object:Gem::Version
|
84
|
-
version:
|
214
|
+
version: 2.6.0
|
85
215
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
216
|
requirements:
|
87
217
|
- - ">="
|
88
218
|
- !ruby/object:Gem::Version
|
89
219
|
version: '0'
|
90
220
|
requirements: []
|
91
|
-
|
92
|
-
rubygems_version: 2.7.6
|
221
|
+
rubygems_version: 3.0.3
|
93
222
|
signing_key:
|
94
223
|
specification_version: 4
|
95
|
-
summary:
|
224
|
+
summary: Provides insights into what changed.
|
96
225
|
test_files: []
|
data/.gitignore
DELETED
data/.rspec
DELETED
data/.travis.yml
DELETED
data/Gemfile
DELETED
data/bin/console
DELETED
@@ -1,14 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
require "bundler/setup"
|
4
|
-
require "changed"
|
5
|
-
|
6
|
-
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
-
# with your gem easier. You can also use a different console, if you like.
|
8
|
-
|
9
|
-
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
-
# require "pry"
|
11
|
-
# Pry.start
|
12
|
-
|
13
|
-
require "irb"
|
14
|
-
IRB.start(__FILE__)
|
data/bin/setup
DELETED
data/changed.gemspec
DELETED
@@ -1,24 +0,0 @@
|
|
1
|
-
|
2
|
-
lib = File.expand_path("../lib", __FILE__)
|
3
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require "changed/version"
|
5
|
-
|
6
|
-
Gem::Specification.new do |spec|
|
7
|
-
spec.name = "changed"
|
8
|
-
spec.version = Changed::VERSION
|
9
|
-
spec.authors = ["Kevin Sylvestre"]
|
10
|
-
spec.email = ["kevin@ksylvest.com"]
|
11
|
-
|
12
|
-
spec.summary = %q{A simple gem for change tracking.}
|
13
|
-
|
14
|
-
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
15
|
-
f.match(%r{^(test|spec|features)/})
|
16
|
-
end
|
17
|
-
spec.bindir = "exe"
|
18
|
-
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
-
spec.require_paths = ["lib"]
|
20
|
-
|
21
|
-
spec.add_development_dependency "bundler", "~> 1.16"
|
22
|
-
spec.add_development_dependency "rake", "~> 10.0"
|
23
|
-
spec.add_development_dependency "rspec", "~> 3.0"
|
24
|
-
end
|