snapshotable 0.0.1

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
+ SHA1:
3
+ metadata.gz: c6e8b118f2e16f01a786078f2f595dc4758d68a9
4
+ data.tar.gz: ce248ffcc575d4958f117be3558551a311a82c3a
5
+ SHA512:
6
+ metadata.gz: 4734984d20167bc564e2314d4333947fb6b33e3639584d94c0c976b4714dbebf33ae4154f4da044941fd73414054a5ad931b277090611bd2333d4530a7f51d8e
7
+ data.tar.gz: a4fbbbb53540e83ccd1a19451286f00194ca69dbc669cb8e57994d3d5dae582538d8366c155ce4b127d79dcc1a0a21e089131578798f0b090eaffee5da5cb3cd
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Manuel Puyol
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,145 @@
1
+
2
+
3
+ # Snapshotable
4
+
5
+ This gem is intended to work as a model history, saving important information about it (and its relations) over time.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'snapshotable'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install snapshotable
22
+
23
+ ## Setup
24
+
25
+ ### Creating the model and migration
26
+ We provide a helpful generator to create migrations and models easily. Simply run:
27
+ ```
28
+ rails generate snapshotable:create <model_to_be_snapshoted>
29
+ ```
30
+
31
+ Example:
32
+ ```
33
+ rails generate snapshotable:create user
34
+ ```
35
+
36
+ This would create a `create_user_snapshot` migration
37
+ ```
38
+ class CreateContractSnapshots < ActiveRecord::Migration
39
+ def change
40
+ create_table :user_snapshots do |t|
41
+ # model to be snapshoted
42
+ t.references :user, index: true, null: false, foreign_key: true
43
+
44
+ # snapshoted_attributes
45
+ t.jsonb :attributes, null: false
46
+ t.timestamps null: false
47
+ end
48
+ end
49
+ end
50
+ ```
51
+
52
+ and a `UserSnapshot` model
53
+ ```
54
+ class UserSnapshot < ApplicationRecord
55
+ belongs_to :user
56
+
57
+ validates :user, presence: true
58
+ validates :attributes, presence: true
59
+ end
60
+ ```
61
+
62
+ ### Saving relation information
63
+
64
+ It's possible to add `--has_one` or `--has_many` options to the generator, which creates the needed attributes
65
+
66
+ Example:
67
+ ```
68
+ rails generate snapshotable:create user --has_one profile photo --has_many groups friends
69
+ ```
70
+
71
+ This would create a `create_user_snapshot` migration
72
+ ```
73
+ class CreateContractSnapshots < ActiveRecord::Migration
74
+ def change
75
+ create_table :user_snapshots do |t|
76
+ # model to be snapshoted
77
+ t.references :user, index: true, null: false, foreign_key: true
78
+
79
+ # snapshoted_attributes
80
+ t.jsonb :attributes, null: false
81
+
82
+ t.jsonb :profile_attributes, null: false
83
+ t.jsonb :photo_attributes, null: false
84
+
85
+ t.jsonb :groups_attributes, null: false, array: true, default: []
86
+ t.jsonb :friends_attributes, null: false, array: true, default: []
87
+
88
+ t.timestamps null: false
89
+ end
90
+ end
91
+ end
92
+ ```
93
+
94
+ In this case, the model won't change, but you could modify it manually if you wish.
95
+
96
+ ### Setting the base model
97
+
98
+ After creating the Snapshot model, add a `has_many` relation to the base model
99
+ ```
100
+ has_many user_snapshots, dependent: :destroy
101
+ ```
102
+ Then, set which attributes should be saved on the Snapshot using `snapshot` on the model
103
+ ```
104
+ snapshot :id, :name, :age, profile: [:description], photo: [:url], groups: [:name], friends: [:name, :age]
105
+ ```
106
+
107
+ ### Creating a Snapshot
108
+
109
+ Run `take_snapshot!` to save a new snapshot from a model instance. This will only save a new snapshot if any of the saved fields have changed.
110
+
111
+ In the example above a `UserSnapshot` would be created like this:
112
+ ```
113
+ {
114
+ user_id: user.id,
115
+ attributes: {
116
+ id: user.id,
117
+ name: user.name,
118
+ age: user.age
119
+ },
120
+ profile_attributes: {
121
+ description: user.profile.description
122
+ },
123
+ photo_attributes: {
124
+ url: user.photo.url
125
+ },
126
+ groups_attributes: [{
127
+ name: user.groups.first.name
128
+ },
129
+ {
130
+ name: user.groups.second.name
131
+ }],
132
+ friends_attributes: [{
133
+ name: user.friends.first.name,
134
+ age: user.friends.first.age
135
+ },
136
+ {
137
+ name: user.friends.second.name,
138
+ age: user.friends.second.age
139
+ }]
140
+ }
141
+ ```
142
+
143
+ ## License
144
+
145
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,59 @@
1
+ require "rails/generators"
2
+ require "rails/generators/migration"
3
+ require "active_record"
4
+ require "rails/generators/active_record"
5
+
6
+ module Snapshotable
7
+ module Generators
8
+ class CreateGenerator < Rails::Generators::Base
9
+ include Rails::Generators::Migration
10
+
11
+ source_root File.expand_path("templates", __dir__)
12
+
13
+ argument :snapshotable_model, type: :string
14
+ class_option :has_one, type: :array, default: [], desc: 'has_one relations to add to the snapshot'
15
+ class_option :has_many, type: :array, default: [], desc: 'has_many relations to add to the snapshot'
16
+
17
+ def generate_migration_and_model
18
+ migration_template "migration.rb", "db/migrate/create_#{snapshotable_model}_snapshots.rb", migration_version: migration_version
19
+ template "model.rb", "app/models/#{model_underscored}_snapshot.rb"
20
+ end
21
+
22
+ # Implement the required interface for Rails::Generators::Migration.
23
+ def self.next_migration_number(dirname) #:nodoc:
24
+ next_migration_number = current_migration_number(dirname) + 1
25
+ if ActiveRecord::Base.timestamped_migrations
26
+ [Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % next_migration_number].max
27
+ else
28
+ "%.3d" % next_migration_number
29
+ end
30
+ end
31
+
32
+ def migration_version
33
+ if ActiveRecord::VERSION::MAJOR >= 5
34
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
35
+ end
36
+ end
37
+
38
+ def active_record_class
39
+ ActiveRecord::VERSION::MAJOR >= 5 ? 'ApplicationRecord' : 'ActiveRecord::Base'
40
+ end
41
+
42
+ def has_one
43
+ options['has_one']
44
+ end
45
+
46
+ def has_many
47
+ options['has_many']
48
+ end
49
+
50
+ def model_underscored
51
+ snapshotable_model.underscore
52
+ end
53
+
54
+ def model_camelcased
55
+ snapshotable_model.camelcase
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,17 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :<%= model_underscored %>_snapshots do |t|
4
+ # model to be snapshoted
5
+ t.references :<%= model_underscored %>, index: true, null: false, foreign_key: true
6
+
7
+ # snapshoted_attributes
8
+ t.jsonb :attributes, null: false
9
+ <% has_one.each do |relation| %>
10
+ t.jsonb :<%= relation.underscore %>_attributes, null: false<% end %>
11
+ <% has_many.each do |relation| %>
12
+ t.jsonb :<%= relation.underscore %>_attributes, null: false, array: true, default: []<% end %>
13
+
14
+ t.timestamps null: false
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,6 @@
1
+ class <%= model_camelcased %>Snapshot < <%= active_record_class %>
2
+ belongs_to :<%= model_underscored %>
3
+
4
+ validates :<%= model_underscored %>, presence: true
5
+ validates :attributes, presence: true
6
+ end
@@ -0,0 +1,53 @@
1
+ module Snapshotable
2
+ class SnapshotCreator
3
+ def initialize(record)
4
+ @record = record
5
+ end
6
+
7
+ def call
8
+ snapshot_attrs
9
+ end
10
+
11
+ private
12
+
13
+ attr_reader :record
14
+
15
+ def snapshot_attrs
16
+ snapshot = {}
17
+
18
+ record.custom_snapshot_attributes.each do |key, attribute|
19
+ snapshot[key] = record.send(attribute)
20
+ end
21
+
22
+ snapshot[:attributes] = extract_attributes(record_snapshot_attrs, record) if record_snapshot_attrs.any?
23
+
24
+ deep_snapshot_attrs&.each do |association_name, attributes|
25
+ association = record.send(association_name)
26
+
27
+ snapshot["#{association_name}_attributes"] = if association.class.name == 'ActiveRecord::Associations::CollectionProxy'
28
+ association.map { |model| extract_attributes(attributes, model) }
29
+ else
30
+ extract_attributes(attributes, association)
31
+ end
32
+ end
33
+
34
+ snapshot
35
+ end
36
+
37
+ def extract_attributes(attributes, model)
38
+ return {} if model.blank?
39
+
40
+ attributes.inject({}) do |collected_attrs, attr|
41
+ collected_attrs.merge(attr => model.send(attr))
42
+ end
43
+ end
44
+
45
+ def record_snapshot_attrs
46
+ @record_snapshot_attrs ||= record.attributes_to_save_on_snapshot.select { |attr| attr.is_a? Symbol }
47
+ end
48
+
49
+ def deep_snapshot_attrs
50
+ @deep_snapshot_attrs ||= record.attributes_to_save_on_snapshot.select { |attr| attr.is_a? Hash }.first
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,3 @@
1
+ module Snapshotable
2
+ VERSION = '0.0.1'.freeze
3
+ end
@@ -0,0 +1,62 @@
1
+ require 'services/snapshot_creator'
2
+
3
+ module Snapshotable
4
+ def self.included(base)
5
+ base.class_eval do
6
+ extend ClassMethods
7
+ class_attribute :attributes_to_save_on_snapshot, instance_writer: false
8
+ class_attribute :attributes_to_ignore_on_diff, instance_writer: false
9
+ class_attribute :custom_snapshot_attributes, instance_writer: false
10
+
11
+ self.attributes_to_save_on_snapshot = []
12
+ self.attributes_to_ignore_on_diff = %w[id created_at updated_at]
13
+ self.custom_snapshot_attributes = {}
14
+
15
+ unless instance_methods.include?(:take_snapshot!)
16
+ def take_snapshot!
17
+ snapshot = SnapshotCreator.new(self).call
18
+ snapshot_model = Object.const_get("#{self.class.name}Snapshot")
19
+
20
+ snapshot_model.create!(snapshot) if should_create_new_snapshot?(snapshot)
21
+ end
22
+ end
23
+
24
+ unless instance_methods.include?(:last_snapshot)
25
+ def last_snapshot
26
+ send("#{self.class.name.downcase}_snapshots").last
27
+ end
28
+ end
29
+
30
+ unless instance_methods.include?(:should_create_cache?)
31
+ def should_create_new_snapshot?(snapshot)
32
+ return true if last_snapshot.nil?
33
+
34
+ snapshot_to_compare = last_snapshot
35
+ .attributes
36
+ .except(*attributes_to_ignore_on_diff)
37
+ .with_indifferent_access
38
+
39
+ HashDiff.diff(snapshot_to_compare, snapshot.with_indifferent_access).any?
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ module ClassMethods
46
+ def snapshot(*attributes)
47
+ self.attributes_to_save_on_snapshot = attributes
48
+ end
49
+
50
+ def snapshot_ignore_diff(*attributes)
51
+ self.attributes_to_ignore_on_diff = attributes.map(&:to_s)
52
+ end
53
+
54
+ def custom_snapshot(*attributes)
55
+ self.custom_snapshot_attributes = attributes.first
56
+ end
57
+ end
58
+ end
59
+
60
+ ActiveSupport.on_load :active_record do
61
+ include Snapshotable
62
+ end
File without changes
metadata ADDED
@@ -0,0 +1,175 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: snapshotable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - João Batista Marinho
8
+ - Manuel Puyol
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2018-08-10 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '4.1'
21
+ - - "<"
22
+ - !ruby/object:Gem::Version
23
+ version: '6'
24
+ type: :runtime
25
+ prerelease: false
26
+ version_requirements: !ruby/object:Gem::Requirement
27
+ requirements:
28
+ - - ">="
29
+ - !ruby/object:Gem::Version
30
+ version: '4.1'
31
+ - - "<"
32
+ - !ruby/object:Gem::Version
33
+ version: '6'
34
+ - !ruby/object:Gem::Dependency
35
+ name: activerecord
36
+ requirement: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '4.1'
41
+ - - "<"
42
+ - !ruby/object:Gem::Version
43
+ version: '6'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: '4.1'
51
+ - - "<"
52
+ - !ruby/object:Gem::Version
53
+ version: '6'
54
+ - !ruby/object:Gem::Dependency
55
+ name: hashdiff
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0.3'
61
+ - - "<"
62
+ - !ruby/object:Gem::Version
63
+ version: '1'
64
+ type: :runtime
65
+ prerelease: false
66
+ version_requirements: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0.3'
71
+ - - "<"
72
+ - !ruby/object:Gem::Version
73
+ version: '1'
74
+ - !ruby/object:Gem::Dependency
75
+ name: bundler
76
+ requirement: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: '1.16'
81
+ type: :development
82
+ prerelease: false
83
+ version_requirements: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: '1.16'
88
+ - !ruby/object:Gem::Dependency
89
+ name: pry-rails
90
+ requirement: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0.3'
95
+ - - "<"
96
+ - !ruby/object:Gem::Version
97
+ version: '1'
98
+ type: :development
99
+ prerelease: false
100
+ version_requirements: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0.3'
105
+ - - "<"
106
+ - !ruby/object:Gem::Version
107
+ version: '1'
108
+ - !ruby/object:Gem::Dependency
109
+ name: rake
110
+ requirement: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - "~>"
113
+ - !ruby/object:Gem::Version
114
+ version: '10.0'
115
+ type: :development
116
+ prerelease: false
117
+ version_requirements: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - "~>"
120
+ - !ruby/object:Gem::Version
121
+ version: '10.0'
122
+ - !ruby/object:Gem::Dependency
123
+ name: rspec
124
+ requirement: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - "~>"
127
+ - !ruby/object:Gem::Version
128
+ version: '3.0'
129
+ type: :development
130
+ prerelease: false
131
+ version_requirements: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - "~>"
134
+ - !ruby/object:Gem::Version
135
+ version: '3.0'
136
+ description: Caches a model in a time period
137
+ email: engineering@qulture.rocks
138
+ executables: []
139
+ extensions: []
140
+ extra_rdoc_files: []
141
+ files:
142
+ - LICENSE.txt
143
+ - README.md
144
+ - lib/generators/snapshotable/create_generator.rb
145
+ - lib/generators/snapshotable/templates/migration.rb
146
+ - lib/generators/snapshotable/templates/model.rb
147
+ - lib/services/snapshot_creator.rb
148
+ - lib/snapshotable.rb
149
+ - lib/snapshotable/version.rb
150
+ - lib/tasks/cacheable_models.rake
151
+ homepage: https://github.com/QultureRocks/snapshotable
152
+ licenses:
153
+ - MIT
154
+ metadata: {}
155
+ post_install_message:
156
+ rdoc_options: []
157
+ require_paths:
158
+ - lib
159
+ required_ruby_version: !ruby/object:Gem::Requirement
160
+ requirements:
161
+ - - ">="
162
+ - !ruby/object:Gem::Version
163
+ version: 2.1.7
164
+ required_rubygems_version: !ruby/object:Gem::Requirement
165
+ requirements:
166
+ - - ">="
167
+ - !ruby/object:Gem::Version
168
+ version: '0'
169
+ requirements: []
170
+ rubyforge_project:
171
+ rubygems_version: 2.6.14
172
+ signing_key:
173
+ specification_version: 4
174
+ summary: Caches a model in a time period.
175
+ test_files: []