time-travel 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: 7066602cf148bc17416b4ce73cba9f5f0272d0e3fc65d51165c28e61b6810ace
4
+ data.tar.gz: 47a9320d5458174874d576b62b8f9e2edd9ffdce4aee51ff39273c6a0397cae2
5
+ SHA512:
6
+ metadata.gz: acc6c7acd035e5a242bdcc11bb4851b89488496945556e719f292ebc98cb3e79e4aa9b49ce8a60dc779e2362b9ac0d44a9e23f228ee00828313f595525b96bae
7
+ data.tar.gz: 7ec088cc98671dd0964e33e8a7034c278b1ce3d1230620b51e90a93632c6115e4cb02a66cfd58331d2a497b436ccce18248449e09150d3db01bf892773eb0955
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2018
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,293 @@
1
+ # Time Travel
2
+
3
+ The time travel gem adds in-table version control to your data. It lets you see, correct and update records at any point in time, but preserves the entire history of corrections and updates to your data so you can drill-down and find out exactly what happened when something goes wrong.
4
+
5
+ # How It Works
6
+
7
+ Lets say that we're a new bank and we're planning to maintain our customers' cash balances in a table called `balance`.
8
+
9
+ We first add time travel fields to the model with
10
+ ```
11
+ > bundle exec rake generate time_travel balance
12
+ > bundle exec rake db:migrate
13
+ ```
14
+ include the Time Travel helper in the `balance` model
15
+
16
+ ```
17
+ class Balance < ActiveRecord::Base
18
+ include TimeTravel::TimelineHelper
19
+
20
+ ...
21
+ ```
22
+ and define the `timeline_fields` method to return the fields that uniquely identify each timeline in our model(in our case, the cash account id)
23
+ ```
24
+ ...
25
+ def self.timeline_fields
26
+ :cash_account_id
27
+ end
28
+ ```
29
+
30
+ Then we start off our operations
31
+
32
+ ## Day 1 - 6th Septermber - New Account
33
+
34
+ The operations team informs us that a new customer created our bank's first account and deposited $500 5 days ago
35
+
36
+ We record this info with
37
+
38
+ ```
39
+ > timeline=balance.timeline(cash_account_id: 1)
40
+ > timeline.create(amount: 500, effective_from: Time.now - 5.days)
41
+ ```
42
+
43
+ After a few minutes, they ping us again and tells us that the customer also submitted an addition $200 two days ago,
44
+ so we record that as well with
45
+
46
+ ```
47
+ > timeline.update(amount: 700, effective_from: Time.now - 2.days)
48
+ ```
49
+
50
+ ## Day 2 - 7th September - Corrections
51
+
52
+ An operations guy walks in hurriedly and tells us that they are extremely sorry but the amounts deposited were recorded wrong, it was $600 and $300 and not $500 and $200
53
+
54
+ We cross-check with the team and record the updates
55
+ ```
56
+ > timeline.update(amount: 600, effective_from: Time.now - 6 days)
57
+ > timeline.update(amount: 900, effective_from: Time.now - 3.days)
58
+ ```
59
+
60
+ ## Day 3 - 8th September - Reconcilliation
61
+
62
+ On day 3, the customer walks in and tells us that something is wrong with our systems and that the balances were different yesterday and day-before even though he didn't deposit or withdraw any money
63
+
64
+ So our support team starts with checking the current balance first
65
+
66
+ To decipher what happened, the team looks at two time ranges in each record. The effective time range(`effective_from` and `effective_till`) tells them what period the data was recorded for, while the valid time range(`valid_from` and `valid_till`) tells them when the data was recorded. An infinite end date(1-1-3000) tells them that the record is currently effective or currently valid depending on which time range it shows up on.
67
+
68
+ ```
69
+ > timeline.at(Time.now)
70
+ {
71
+ "id"=>6,
72
+ "cash_account_id"=>1,
73
+ "amount"=>900,
74
+ "reference_id"=>nil,
75
+ "effective_from"=>"2021-09-04T18:30:00.000Z",
76
+ "effective_till"=>"3000-01-01T00:00:00.000Z",
77
+ "valid_from"=>"2021-09-07T18:30:00.000Z",
78
+ "valid_till"=>"3000-01-01T00:00:00.000Z"
79
+ }
80
+ ```
81
+ The above record tells them that the customer's balance changed to $900 on the 4th of September and is currently effective, and that the amount was recorded on the 7th and is currently valid.
82
+
83
+ They check if the customer expects the balance to be $900 and he confirms this
84
+
85
+ Great, atleast the current balance in order, so they start digging in deeper to check what the balance was 2 days ago
86
+
87
+ ```
88
+ > timeline.at(Time.now - 2.days, as_of: Time.now - 2.days)
89
+ {
90
+ "id"=>3,
91
+ "cash_account_id"=>1,
92
+ "amount"=>700,
93
+ "reference_id"=>nil,
94
+ "effective_from"=>"2021-09-04T18:30:00.000Z",
95
+ "effective_till"=>"3000-01-01T00:00:00.000Z",
96
+ "valid_from"=>"2021-09-06T18:30:00.000Z",
97
+ "valid_till"=>"2021-09-07T18:30:00.000Z"
98
+ }
99
+ ```
100
+ They realize from this record is that the balance was indeed different two days ago, and it reflected $700. Additionally the valid time range tells them that there was a correction made to the balance a day later since the valid time range ends on the 7th.
101
+
102
+ They inform the customer that two days ago, he might have seen a balance of $700, and he confirms this.
103
+
104
+ They then check further and compare balance data recorded two days ago to find out what happened
105
+
106
+ ```
107
+ > timeline.as_of(Time.now - 2.days)
108
+ [
109
+ {
110
+ "id"=>2,
111
+ "cash_account_id"=>1,
112
+ "amount"=>500,
113
+ "reference_id"=>nil,
114
+ "effective_from"=>"2021-09-01T18:30:00.000Z",
115
+ "effective_till"=>"2021-09-04T18:30:00.000Z",
116
+ "valid_from"=>"2021-09-06T18:30:00.000Z",
117
+ "valid_till"=>"2021-09-07T18:30:00.000Z"
118
+ },
119
+ {
120
+ "id"=>3,
121
+ "cash_account_id"=>1,
122
+ "amount"=>700,
123
+ "reference_id"=>nil,
124
+ "effective_from"=>"2021-09-04T18:30:00.000Z",
125
+ "effective_till"=>"3000-01-01T00:00:00.000Z",
126
+ "valid_from"=>"2021-09-06T18:30:00.000Z",
127
+ "valid_till"=>"2021-09-07T18:30:00.000Z"
128
+ }
129
+ ]
130
+
131
+ > timeline.as_of(Time.now)
132
+ [
133
+ {
134
+ "id"=>5,
135
+ "cash_account_id"=>1,
136
+ "amount"=>600,
137
+ "reference_id"=>nil,
138
+ "effective_from"=>"2021-09-01T18:30:00.000Z",
139
+ "effective_till"=>"2021-09-04T18:30:00.000Z",
140
+ "valid_from"=>"2021-09-07T18:30:00.000Z",
141
+ "valid_till"=>"3000-01-01T00:00:00.000Z"
142
+ },
143
+ {
144
+ "id"=>6,
145
+ "cash_account_id"=>1,
146
+ "amount"=>900,
147
+ "reference_id"=>nil,
148
+ "effective_from"=>"2021-09-04T18:30:00.000Z",
149
+ "effective_till"=>"3000-01-01T00:00:00.000Z",
150
+ "valid_from"=>"2021-09-07T18:30:00.000Z",
151
+ "valid_till"=>"3000-01-01T00:00:00.000Z"
152
+ }
153
+ ]
154
+ ```
155
+
156
+ From the time ranges, they understand that the balance dates were correct and not altered, but the amounts were corrected a day after the amounts were initially recorded.
157
+
158
+ They inform the customer about exactly what happened with his accounts in the last two days.
159
+
160
+ The customer, wanting to ensure that everything is right, asks them when the additional $300 was recorded. and they inform the customer that the date of the second deposit is 4 days go, but the correct amount was updated yesterday.
161
+
162
+ The customer feels satisfied that all the changes were tracked accurately, thanks us and leaves
163
+
164
+ ## Installation
165
+
166
+ To install the gem, reference this git repository in your `Gemfile`
167
+
168
+ git "https://<your-personal-access-token>:x-oauth-basic@github.com/planarinv/elder-wand.git" do
169
+ gem 'time_travel'
170
+ end
171
+
172
+ Then run:
173
+
174
+ bundle install
175
+
176
+ ## Usage
177
+
178
+ ### Creating a new model that tracks history
179
+
180
+ To create a new model which will track history, use the `time_travel` generator to create a scaffold
181
+
182
+ bundle exec rake generate time_travel <NewModel> <fields>
183
+
184
+ Then, include `TimeTravel::TimelineHelper` in your ActiveRecord Model, and define which fields identify a unique timeline in your model for which changes and corrections need to be tracked
185
+
186
+ In the example below. a `CashBalance` model has a `:cash_account_id` field which uniquely identifies the account for which the cash balance needs to be tracked.
187
+
188
+ class CashBalance < ActiveRecord::Base
189
+ include TimeTravel::TimelineHelper
190
+
191
+ def self.timeline_fields
192
+ :cash_account_id
193
+ end
194
+ end
195
+
196
+ ### Adding history tracking to an existing model
197
+
198
+ #### _Adding fields for history tracking_
199
+
200
+ To add history tracking to an existing model, you can use the `time_travel` generator, specifying the name of a model that already exists.
201
+
202
+ bundle exec rake generate time_travel <ExistingModel>
203
+
204
+ #### _Migrating existing data_
205
+
206
+ To migrate existing data, you'll need to populate the effective and valid time ranges in each record in your model with a custom script of your own. An easy way to do this for a table that has a single date field is to order the records by the field and chain the dates from subsequent records to create the effective time range. The valid time range can be set to the current date onwards if you don't care about history prior to the migration.
207
+
208
+ ### Manipulating data in a Time Travel model
209
+
210
+ To apply changes to a timeline, first create a timeline object
211
+
212
+ timeline=balance.timeline(cash_account_id: 1)
213
+
214
+ Then use the `create`, `update` or `terminate` methods to modify the timeline.
215
+
216
+ In case you're not sure if you need to create a new timeline or update it, you can always call `create_or_update` to do the dirty work for you.
217
+
218
+ You can pass in `effective_from` and `effective_till` dates to indicate the period during which you want to create or update records. This is especially useful if you want to correct an older record.
219
+
220
+ The `valid_from` and `valid_till` fields are managed by the gem, based on when records are added or corrected and cannot be modified explicitly on the timeline.
221
+
222
+ Here are some examples of operations:
223
+
224
+ # create account with balance of Rs. 500, effective from now onwards
225
+ timeline.create({amount: 500})
226
+ # create account with balance Rs. 500, effective from 1st of August
227
+ timeline.create({amount: 500}, effective_from: Date.parse("01/09/2018").beginning_of_day))
228
+ # update account with balance Rs. 1000, effective from now onwards
229
+ timeline.update({amount: 1000})
230
+ # update account with balance Rs. 1500, effective from 20th of August
231
+ timeline.update({amount: 1500}, effective_from: Date.parse("20/09/2018").beginning_of_day)
232
+ # correct account balance to Rs. 2000 between 5th and 22nd of August
233
+ timeline.update({amount: 2000},
234
+ effective_from: Date.parse("05/09/2018").begining_of_day,
235
+ effective_till: Date.parse("22/09/2018").begining_of_day)
236
+ # close account now
237
+ timeline.terminate()
238
+ # close account, effective from 30th August timeline.terminate(effective_from: Date.parse("30/09/2018"))
239
+
240
+ Updates can be applied in bulk by supplying attributes in an array and using the `bulk_update` method
241
+
242
+ ### Accessing the timelines
243
+
244
+ To access the records in the timelines, use the `at` and `as_of` methods.
245
+
246
+ The `at` method returns a single record at a point in the timelines.
247
+
248
+ ```
249
+ # retrieve a currently valid record, effective 2 days ago
250
+ timeline.at(Time.now - 2 days)
251
+ # retrieve record which was valid and effective 2 days ago
252
+ timeline.at(Time.now - 2.days, as_of: Time.now - 2 days)
253
+ ```
254
+
255
+ The `as_of` method returns the entire history of records which were valid on a given date
256
+
257
+ ```
258
+ # the currently valid set of records
259
+ timeline.as_of(Time.now)
260
+ # the set of records valid two days ago
261
+ timeline.as_of(Time.now - 2.days)
262
+ ```
263
+
264
+ ### Updating data directly
265
+
266
+ Data on any record can be directly updated by using ActiveRecord methods on your model.
267
+
268
+ You might want to do this for fields which you don't want to track on the timelines.
269
+
270
+ Avoid updating the time ranges directly though, unless you really know what you're doing.
271
+
272
+ We allow updates to the time ranges since you might need to migrate an existing model to add `time_travel` functionality.
273
+
274
+ ### SQL and Native modes
275
+
276
+ By default time_travel applies updates using native ruby logic.
277
+
278
+ For performance, the sql mode is also available with support for Postgres.
279
+
280
+ To switch modes, create an initializer as follows
281
+
282
+ TimeTravel.configure do
283
+ update_mode="sql"
284
+ end
285
+
286
+ and install the postgres plsql function with
287
+
288
+ rake time_travel:create_postgres_function
289
+
290
+ ## Behind the Scenes
291
+
292
+ The time travel gem uses bi-temporal modelling to track changes and corrections. There's a lot of material online that covers it in-case you want to dig deeper into what makes the gem work.
293
+
data/Rakefile ADDED
@@ -0,0 +1,29 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'TimeTravel'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ require 'bundler/gem_tasks'
18
+
19
+ require 'rake/testtask'
20
+
21
+ Rake::TestTask.new(:test) do |t|
22
+ t.libs << 'test'
23
+ t.pattern = 'test/**/*_test.rb'
24
+ t.verbose = false
25
+ end
26
+
27
+ task default: :test
28
+
29
+ import "./lib/tasks/create_postgres_function.rake"
@@ -0,0 +1,9 @@
1
+ Description:
2
+ Adds historical update and correction tracking to models
3
+
4
+ Example:
5
+ rails generate time_travel Model
6
+
7
+ This will create:
8
+ a model with fields for tracking history
9
+
@@ -0,0 +1,17 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ change_table :<%= table_name %> do |t|
4
+ t.column :effective_from, :datetime
5
+ t.column :effective_till, :datetime
6
+ t.column :valid_from, :datetime
7
+ t.column :valid_till, :datetime
8
+
9
+ add_index :<%= table_name %>, :effective_from
10
+ add_index :<%= table_name %>, :effective_till
11
+ add_index :<%= table_name %>, :valid_from
12
+ add_index :<%= table_name %>, :valid_till
13
+
14
+ # TODO: Migrate existing data and add a not null constraint for effective_from and valid_till
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :<%= table_name %> do |t|
4
+ <% attributes.each do |attribute| -%>
5
+ t.<%= attribute.type %> :<%= attribute.name %>
6
+ <% end -%>
7
+ t.column :effective_from, :datetime, null: false
8
+ t.column :effective_till, :datetime
9
+ t.column :valid_from, :datetime, null: false
10
+ t.column :valid_till, :datetime
11
+
12
+ end
13
+
14
+ add_index :<%= table_name %>, :effective_from
15
+ add_index :<%= table_name %>, :effective_till
16
+ add_index :<%= table_name %>, :valid_from
17
+ add_index :<%= table_name %>, :valid_till
18
+ end
19
+ end
@@ -0,0 +1,49 @@
1
+ require "rails/generators/active_record"
2
+
3
+ class TimeTravelGenerator < ActiveRecord::Generators::Base
4
+ desc "Create a migration to add history tracking fields to your model. "+
5
+ "The only argument this generator takes is the model on which the history tracking needs to be applied"
6
+ argument :attributes, type: :array, default: [], banner: "field:type field:type"
7
+
8
+ def self.source_root
9
+ @source_root ||= File.expand_path('../templates', __FILE__)
10
+ end
11
+
12
+ def generate_migration
13
+ if (behavior == :invoke && model_exists?)
14
+ migration_template("time_travel_migration_existing.rb.erb",
15
+ "db/migrate/#{migration_file_name}",
16
+ migration_version: migration_version)
17
+ else
18
+ migration_template("time_travel_migration_new.rb.erb",
19
+ "db/migrate/#{migration_file_name}",
20
+ migration_version: migration_version)
21
+ end
22
+ end
23
+
24
+ def model_exists?
25
+ File.exist?(File.join(destination_root, model_path))
26
+ end
27
+
28
+ def model_path
29
+ @model_path ||= File.join("app", "models", "#{file_path}.rb")
30
+ end
31
+
32
+ def migration_name
33
+ "add_time_travel_to_#{name.underscore.pluralize}"
34
+ end
35
+
36
+ def migration_file_name
37
+ "#{migration_name}.rb"
38
+ end
39
+
40
+ def migration_class_name
41
+ migration_name.camelize
42
+ end
43
+
44
+ def migration_version
45
+ if Rails.version.start_with? "5"
46
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
47
+ end
48
+ end
49
+ end