snapshotable 0.0.1

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
+ 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: []