historiographer 4.0.0 → 4.1.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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +115 -39
  3. data/lib/historiographer/configuration.rb +36 -0
  4. data/lib/historiographer/history.rb +9 -2
  5. data/lib/historiographer/history_migration.rb +9 -6
  6. data/lib/historiographer/relation.rb +1 -1
  7. data/lib/historiographer/version.rb +3 -0
  8. data/lib/historiographer.rb +176 -11
  9. metadata +3 -30
  10. data/.document +0 -5
  11. data/.rspec +0 -1
  12. data/.ruby-version +0 -1
  13. data/.standalone_migrations +0 -6
  14. data/Gemfile +0 -34
  15. data/Gemfile.lock +0 -289
  16. data/Guardfile +0 -70
  17. data/Rakefile +0 -54
  18. data/VERSION +0 -1
  19. data/historiographer.gemspec +0 -106
  20. data/init.rb +0 -18
  21. data/spec/db/database.yml +0 -25
  22. data/spec/db/migrate/20161121212228_create_posts.rb +0 -19
  23. data/spec/db/migrate/20161121212229_create_post_histories.rb +0 -10
  24. data/spec/db/migrate/20161121212230_create_authors.rb +0 -13
  25. data/spec/db/migrate/20161121212231_create_author_histories.rb +0 -10
  26. data/spec/db/migrate/20161121212232_create_users.rb +0 -9
  27. data/spec/db/migrate/20171011194624_create_safe_posts.rb +0 -19
  28. data/spec/db/migrate/20171011194715_create_safe_post_histories.rb +0 -9
  29. data/spec/db/migrate/20191024142304_create_thing_with_compound_index.rb +0 -10
  30. data/spec/db/migrate/20191024142352_create_thing_with_compound_index_history.rb +0 -11
  31. data/spec/db/migrate/20191024203106_create_thing_without_history.rb +0 -7
  32. data/spec/db/migrate/20221018204220_create_silent_posts.rb +0 -21
  33. data/spec/db/migrate/20221018204255_create_silent_post_histories.rb +0 -9
  34. data/spec/db/schema.rb +0 -186
  35. data/spec/examples.txt +0 -32
  36. data/spec/factories/post.rb +0 -7
  37. data/spec/historiographer_spec.rb +0 -588
  38. data/spec/spec_helper.rb +0 -52
@@ -1,588 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
-
5
- class Post < ActiveRecord::Base
6
- include Historiographer
7
- acts_as_paranoid
8
- end
9
-
10
- class PostHistory < ActiveRecord::Base
11
- end
12
-
13
- class SafePost < ActiveRecord::Base
14
- include Historiographer::Safe
15
- acts_as_paranoid
16
- end
17
-
18
- class SafePostHistory < ActiveRecord::Base
19
- end
20
-
21
- class SilentPost < ActiveRecord::Base
22
- include Historiographer::Silent
23
- acts_as_paranoid
24
- end
25
-
26
- class SilentPostHistory < ActiveRecord::Base
27
- end
28
-
29
- class Author < ActiveRecord::Base
30
- include Historiographer
31
- end
32
-
33
- class AuthorHistory < ActiveRecord::Base
34
- end
35
-
36
- class User < ActiveRecord::Base
37
- end
38
-
39
- class ThingWithCompoundIndex < ActiveRecord::Base
40
- include Historiographer
41
- end
42
-
43
- class ThingWithCompoundIndexHistory < ActiveRecord::Base
44
- end
45
-
46
- class ThingWithoutHistory < ActiveRecord::Base
47
- end
48
-
49
- describe Historiographer do
50
- before(:all) do
51
- @now = Timecop.freeze
52
- end
53
-
54
- after(:all) do
55
- Timecop.return
56
- end
57
-
58
- let(:username) { 'Test User' }
59
-
60
- let(:user) do
61
- User.create(name: username)
62
- end
63
-
64
- let(:create_post) do
65
- Post.create(
66
- title: 'Post 1',
67
- body: 'Great post',
68
- author_id: 1,
69
- history_user_id: user.id
70
- )
71
- end
72
-
73
- let(:create_author) do
74
- Author.create(
75
- full_name: 'Breezy',
76
- history_user_id: user.id
77
- )
78
- end
79
-
80
- describe 'History counting' do
81
- it 'creates history on creation of primary model record' do
82
- expect do
83
- create_post
84
- end.to change {
85
- PostHistory.count
86
- }.by 1
87
- end
88
-
89
- it 'appends new history on update' do
90
- post = create_post
91
-
92
- expect do
93
- post.update(title: 'Better Title')
94
- end.to change {
95
- PostHistory.count
96
- }.by 1
97
- end
98
-
99
- it 'does not append new history if nothing has changed' do
100
- post = create_post
101
-
102
- expect do
103
- post.update(title: post.title)
104
- end.to_not change {
105
- PostHistory.count
106
- }
107
- end
108
- end
109
-
110
- describe 'History recording' do
111
- it 'records all fields from the parent' do
112
- post = create_post
113
- post_history = post.histories.first
114
-
115
- expect(post_history.title).to eq post.title
116
- expect(post_history.body).to eq post.body
117
- expect(post_history.author_id).to eq post.author_id
118
- expect(post_history.post_id).to eq post.id
119
- expect(post_history.history_started_at.to_s).to eq @now.in_time_zone(Historiographer::UTC).to_s
120
- expect(post_history.history_ended_at).to be_nil
121
- expect(post_history.history_user_id).to eq user.id
122
-
123
- post.update(title: 'Better title')
124
- post_histories = post.histories.reload.order('id asc')
125
- first_history = post_histories.first
126
- second_history = post_histories.second
127
-
128
- expect(first_history.history_ended_at.to_s).to eq @now.in_time_zone(Historiographer::UTC).to_s
129
- expect(second_history.history_ended_at).to be_nil
130
- end
131
-
132
- it 'cannot create without history_user_id' do
133
- post = Post.create(
134
- title: 'Post 1',
135
- body: 'Great post',
136
- author_id: 1
137
- )
138
- expect(post.errors.to_h).to eq(history_user_id: 'must be an integer')
139
-
140
- expect do
141
- post.send(:record_history)
142
- end.to raise_error(
143
- Historiographer::HistoryUserIdMissingError
144
- )
145
- end
146
-
147
- context 'When directly hitting the database via SQL' do
148
- context '#update_all' do
149
- it 'still updates histories' do
150
- FactoryBot.create_list(:post, 3, history_user_id: 1)
151
-
152
- posts = Post.all
153
- expect(posts.count).to eq 3
154
- expect(PostHistory.count).to eq 3
155
- expect(posts.map(&:histories).map(&:count)).to all (eq 1)
156
-
157
- posts.update_all(title: 'My New Post Title', history_user_id: 1)
158
-
159
- expect(PostHistory.count).to eq 6
160
- expect(PostHistory.current.count).to eq 3
161
- expect(posts.map(&:histories).map(&:count)).to all(eq 2)
162
- expect(posts.map(&:current_history).map(&:title)).to all (eq 'My New Post Title')
163
- expect(Post.all).to respond_to :has_histories?
164
-
165
- # It can update by sub-query
166
- Post.where(id: [posts.first.id, posts.last.id]).update_all(title: "Brett's Post", history_user_id: 1)
167
- posts = Post.all.reload.order(:id)
168
- expect(posts.first.histories.count).to eq 3
169
- expect(posts.second.histories.count).to eq 2
170
- expect(posts.third.histories.count).to eq 3
171
- expect(posts.first.title).to eq "Brett's Post"
172
- expect(posts.second.title).to eq 'My New Post Title'
173
- expect(posts.third.title).to eq "Brett's Post"
174
- expect(posts.first.current_history.title).to eq "Brett's Post"
175
- expect(posts.second.current_history.title).to eq 'My New Post Title'
176
- expect(posts.third.current_history.title).to eq "Brett's Post"
177
-
178
- # It does not update histories if nothing changed
179
- Post.all.update_all(title: "Brett's Post", history_user_id: 1)
180
- posts = Post.all.reload.order(:id)
181
- expect(posts.map(&:histories).map(&:count)).to all(eq 3)
182
-
183
- posts.update_all_without_history(title: 'Untracked')
184
- expect(posts.first.histories.count).to eq 3
185
- expect(posts.second.histories.count).to eq 3
186
- expect(posts.third.histories.count).to eq 3
187
-
188
- thing1 = ThingWithoutHistory.create(name: 'Thing 1')
189
- thing2 = ThingWithoutHistory.create(name: 'Thing 2')
190
-
191
- ThingWithoutHistory.all.update_all(name: 'Thing 3')
192
-
193
- expect(ThingWithoutHistory.all.map(&:name)).to all(eq 'Thing 3')
194
- expect(ThingWithoutHistory.all).to_not respond_to :has_histories?
195
- expect(ThingWithoutHistory.all).to_not respond_to :update_all_without_history
196
- expect(ThingWithoutHistory.all).to_not respond_to :delete_all_without_history
197
- end
198
-
199
- it 'respects safety' do
200
- FactoryBot.create_list(:post, 3, history_user_id: 1)
201
-
202
- posts = Post.all
203
- expect(posts.count).to eq 3
204
- expect(PostHistory.count).to eq 3
205
- expect(posts.map(&:histories).map(&:count)).to all (eq 1)
206
-
207
- expect do
208
- posts.update_all(title: 'My New Post Title')
209
- end.to raise_error
210
-
211
- posts.reload.map(&:title).each do |title|
212
- expect(title).to_not eq 'My New Post Title'
213
- end
214
-
215
- SafePost.create(
216
- title: 'Post 1',
217
- body: 'Great post',
218
- author_id: 1
219
- )
220
-
221
- safe_posts = SafePost.all
222
-
223
- expect do
224
- safe_posts.update_all(title: 'New One')
225
- end.to_not raise_error
226
-
227
- expect(safe_posts.map(&:title)).to all(eq 'New One')
228
- end
229
- end
230
-
231
- context '#delete_all' do
232
- it 'includes histories when not paranoid' do
233
- Timecop.freeze
234
- authors = 3.times.map do
235
- Author.create(full_name: 'Brett', history_user_id: 1)
236
- end
237
- Author.delete_all(history_user_id: 1)
238
- expect(AuthorHistory.count).to eq 3
239
- expect(AuthorHistory.current.count).to eq 0
240
- expect(AuthorHistory.where.not(history_ended_at: nil).count).to eq 3
241
- expect(Author.count).to eq 0
242
- Timecop.return
243
- end
244
-
245
- it 'includes histories when paranoid' do
246
- Timecop.freeze
247
- posts = FactoryBot.create_list(:post, 3, history_user_id: 1)
248
- Post.delete_all(history_user_id: 1)
249
- expect(PostHistory.count).to eq 6
250
- expect(PostHistory.current.count).to eq 3
251
- expect(PostHistory.current.map(&:deleted_at)).to all(eq Time.now)
252
- expect(PostHistory.current.map(&:history_user_id)).to all(eq 1)
253
- expect(PostHistory.where(deleted_at: nil).where.not(history_ended_at: nil).count).to eq 3
254
- expect(PostHistory.where(history_ended_at: nil).count).to eq 3
255
- expect(Post.count).to eq 0
256
- Timecop.return
257
- end
258
-
259
- it 'allows delete_all_without_history' do
260
- authors = 3.times.map do
261
- Author.create(full_name: 'Brett', history_user_id: 1)
262
- end
263
- Author.all.delete_all_without_history
264
- expect(AuthorHistory.current.count).to eq 3
265
- expect(Author.count).to eq 0
266
- end
267
- end
268
-
269
- context '#destroy_all' do
270
- it 'includes histories' do
271
- Timecop.freeze
272
- posts = FactoryBot.create_list(:post, 3, history_user_id: 1)
273
- Post.destroy_all(history_user_id: 1)
274
- expect(PostHistory.count).to eq 6
275
- expect(PostHistory.current.count).to eq 3
276
- expect(PostHistory.current.map(&:deleted_at)).to all(eq Time.now)
277
- expect(PostHistory.current.map(&:history_user_id)).to all(eq 1)
278
- expect(PostHistory.where(deleted_at: nil).where.not(history_ended_at: nil).count).to eq 3
279
- expect(PostHistory.where(history_ended_at: nil).count).to eq 3
280
- expect(Post.count).to eq 0
281
- Timecop.return
282
- end
283
-
284
- it 'destroys without histories' do
285
- Timecop.freeze
286
- posts = FactoryBot.create_list(:post, 3, history_user_id: 1)
287
- Post.all.destroy_all_without_history
288
- expect(PostHistory.count).to eq 3
289
- expect(PostHistory.current.count).to eq 3
290
- expect(Post.count).to eq 0
291
- Timecop.return
292
- end
293
- end
294
- end
295
-
296
- context 'When Safe mode' do
297
- it 'creates history without history_user_id' do
298
- expect(Rollbar).to receive(:error).with('history_user_id must be passed in order to save record with histories! If you are in a context with no history_user_id, explicitly call #save_without_history')
299
-
300
- post = SafePost.create(
301
- title: 'Post 1',
302
- body: 'Great post',
303
- author_id: 1
304
- )
305
- expect(post.errors.to_h.keys).to be_empty
306
- expect(post).to be_persisted
307
- expect(post.histories.count).to eq 1
308
- expect(post.histories.first.history_user_id).to be_nil
309
- end
310
-
311
- it 'creates history with history_user_id' do
312
- expect(Rollbar).to_not receive(:error)
313
-
314
- post = SafePost.create(
315
- title: 'Post 1',
316
- body: 'Great post',
317
- author_id: 1,
318
- history_user_id: user.id
319
- )
320
- expect(post.errors.to_h.keys).to be_empty
321
- expect(post).to be_persisted
322
- expect(post.histories.count).to eq 1
323
- expect(post.histories.first.history_user_id).to eq user.id
324
- end
325
-
326
- it 'skips history creation if desired' do
327
- post = SafePost.new(
328
- title: 'Post 1',
329
- body: 'Great post',
330
- author_id: 1
331
- )
332
-
333
- post.save_without_history
334
- expect(post).to be_persisted
335
- expect(post.histories.count).to eq 0
336
- end
337
- end
338
-
339
- context 'When Silent mode' do
340
- it 'creates history without history_user_id' do
341
- expect(Rollbar).to_not receive(:error)
342
-
343
- post = SilentPost.create(
344
- title: 'Post 1',
345
- body: 'Great post',
346
- author_id: 1
347
- )
348
- expect(post.errors.to_h.keys).to be_empty
349
- expect(post).to be_persisted
350
- expect(post.histories.count).to eq 1
351
- expect(post.histories.first.history_user_id).to be_nil
352
-
353
- post.update(title: 'New Title')
354
- post.reload
355
- expect(post.title).to eq 'New Title' # No error was raised
356
- end
357
-
358
- it 'creates history with history_user_id' do
359
- expect(Rollbar).to_not receive(:error)
360
-
361
- post = SilentPost.create(
362
- title: 'Post 1',
363
- body: 'Great post',
364
- author_id: 1,
365
- history_user_id: user.id
366
- )
367
- expect(post.errors.to_h.keys).to be_empty
368
- expect(post).to be_persisted
369
- expect(post.histories.count).to eq 1
370
- expect(post.histories.first.history_user_id).to eq user.id
371
- end
372
-
373
- it 'skips history creation if desired' do
374
- post = SilentPost.new(
375
- title: 'Post 1',
376
- body: 'Great post',
377
- author_id: 1
378
- )
379
-
380
- post.save_without_history
381
- expect(post).to be_persisted
382
- expect(post.histories.count).to eq 0
383
- end
384
- end
385
- it 'can override without history_user_id' do
386
- expect do
387
- post = Post.new(
388
- title: 'Post 1',
389
- body: 'Great post',
390
- author_id: 1
391
- )
392
-
393
- post.save_without_history
394
- end.to_not raise_error
395
- end
396
-
397
- it 'can override without history_user_id' do
398
- expect do
399
- post = Post.new(
400
- title: 'Post 1',
401
- body: 'Great post',
402
- author_id: 1
403
- )
404
-
405
- post.save_without_history!
406
- end.to_not raise_error
407
- end
408
-
409
- it 'does not record histories when main model fails to save' do
410
- class Post
411
- after_save :raise_error, prepend: true
412
-
413
- def raise_error
414
- raise 'Oh no, db issue!'
415
- end
416
- end
417
-
418
- expect { create_post }.to raise_error
419
- expect(Post.count).to be 0
420
- expect(PostHistory.count).to be 0
421
-
422
- Post.skip_callback(:save, :after, :raise_error)
423
- end
424
- end
425
-
426
- describe 'Scopes' do
427
- it 'finds current histories' do
428
- post1 = create_post
429
- post1.update(title: 'Better title')
430
-
431
- post2 = create_post
432
- post2.update(title: 'Better title')
433
-
434
- expect(PostHistory.current.pluck(:title)).to all eq 'Better title'
435
- expect(post1.current_history.title).to eq 'Better title'
436
- end
437
- end
438
-
439
- describe 'Associations' do
440
- it 'names associated records' do
441
- post1 = create_post
442
- expect(post1.histories.first).to be_a(PostHistory)
443
-
444
- expect(post1.histories.first.post).to be(post1)
445
-
446
- author1 = create_author
447
- expect(author1.histories.first).to be_a(AuthorHistory)
448
-
449
- expect(author1.histories.first.author).to be(author1)
450
- end
451
- end
452
-
453
- describe 'Histories' do
454
- it 'does not allow direct updates of histories' do
455
- post1 = create_post
456
- hist1 = post1.histories.first
457
-
458
- expect(hist1.update(title: 'A different title')).to be false
459
- expect(hist1.reload.title).to eq post1.title
460
-
461
- expect(hist1.update!(title: 'A different title')).to be false
462
- expect(hist1.reload.title).to eq post1.title
463
-
464
- hist1.title = 'A different title'
465
- expect(hist1.save).to be false
466
- expect(hist1.reload.title).to eq post1.title
467
-
468
- hist1.title = 'A different title'
469
- expect(hist1.save!).to be false
470
- expect(hist1.reload.title).to eq post1.title
471
- end
472
-
473
- it 'does not allow destroys of histories' do
474
- post1 = create_post
475
- hist1 = post1.histories.first
476
- original_history_count = post1.histories.count
477
-
478
- expect(hist1.destroy).to be false
479
- expect(hist1.destroy!).to be false
480
-
481
- expect(post1.histories.count).to be original_history_count
482
- end
483
- end
484
-
485
- describe 'Deletion' do
486
- it 'records deleted_at and history_user_id on primary and history if you use acts_as_paranoid' do
487
- post = Post.create(
488
- title: 'Post 1',
489
- body: 'Great post',
490
- author_id: 1,
491
- history_user_id: user.id
492
- )
493
-
494
- expect do
495
- post.destroy(history_user_id: 2)
496
- end.to change {
497
- PostHistory.count
498
- }.by 1
499
-
500
- expect(Post.unscoped.where.not(deleted_at: nil).count).to eq 1
501
- expect(Post.unscoped.where(deleted_at: nil).count).to eq 0
502
- expect(PostHistory.where.not(deleted_at: nil).count).to eq 1
503
- expect(PostHistory.last.history_user_id).to eq 2
504
- end
505
-
506
- it 'works with Historiographer::Safe' do
507
- post = SafePost.create(title: 'HELLO', body: 'YO', author_id: 1)
508
-
509
- expect do
510
- post.destroy
511
- end.to_not raise_error
512
-
513
- expect(SafePost.count).to eq 0
514
- expect(post.deleted_at).to_not be_nil
515
- expect(SafePostHistory.count).to eq 2
516
- expect(SafePostHistory.current.last.deleted_at).to eq post.deleted_at
517
-
518
- post2 = SafePost.create(title: 'HELLO', body: 'YO', author_id: 1)
519
-
520
- expect do
521
- post2.destroy!
522
- end.to_not raise_error
523
-
524
- expect(SafePost.count).to eq 0
525
- expect(post2.deleted_at).to_not be_nil
526
- expect(SafePostHistory.count).to eq 4
527
- expect(SafePostHistory.current.where(safe_post_id: post2.id).last.deleted_at).to eq post2.deleted_at
528
- end
529
- end
530
-
531
- describe 'Scopes' do
532
- it 'finds current' do
533
- post = create_post
534
- post.update(title: 'New Title')
535
- post.update(title: 'New Title 2')
536
-
537
- expect(PostHistory.current.count).to be 1
538
- end
539
- end
540
-
541
- describe 'User associations' do
542
- it 'links to user' do
543
- post = create_post
544
- author = create_author
545
-
546
- expect(post.current_history.user.name).to eq username
547
- expect(author.current_history.user.name).to eq username
548
- end
549
- end
550
-
551
- describe 'Migrations with compound indexes' do
552
- it 'supports renaming compound indexes and migrating them to history tables' do
553
- indices_sql = "
554
- SELECT
555
- DISTINCT(
556
- ARRAY_TO_STRING(ARRAY(
557
- SELECT pg_get_indexdef(idx.indexrelid, k + 1, true)
558
- FROM generate_subscripts(idx.indkey, 1) as k
559
- ORDER BY k
560
- ), ',')
561
- ) as indkey_names
562
- FROM pg_class t,
563
- pg_class i,
564
- pg_index idx,
565
- pg_attribute a,
566
- pg_am am
567
- WHERE t.oid = idx.indrelid
568
- AND i.oid = idx.indexrelid
569
- AND a.attrelid = t.oid
570
- AND a.attnum = ANY(idx.indkey)
571
- AND t.relkind = 'r'
572
- AND t.relname = ?;
573
- "
574
-
575
- indices_query_array = [indices_sql, :thing_with_compound_index_histories]
576
- indices_sanitized_query = ThingWithCompoundIndexHistory.send(:sanitize_sql_array, indices_query_array)
577
-
578
- indexes = ThingWithCompoundIndexHistory.connection.execute(indices_sanitized_query).to_a.map(&:values).flatten.map { |i| i.split(',') }
579
-
580
- expect(indexes).to include(['history_started_at'])
581
- expect(indexes).to include(['history_ended_at'])
582
- expect(indexes).to include(['history_user_id'])
583
- expect(indexes).to include(['id'])
584
- expect(indexes).to include(%w[key value])
585
- expect(indexes).to include(['thing_with_compound_index_id'])
586
- end
587
- end
588
- end
data/spec/spec_helper.rb DELETED
@@ -1,52 +0,0 @@
1
- ENV["HISTORIOGRAPHY_ENV"] = "test"
2
-
3
- require_relative "../init.rb"
4
- require "ostruct"
5
- require "factory_bot"
6
-
7
- FactoryBot.definition_file_paths = %w{./factories ./spec/factories}
8
- FactoryBot.find_definitions
9
-
10
- RSpec.configure do |config|
11
- config.expect_with :rspec do |expectations|
12
- expectations.include_chain_clauses_in_custom_matcher_descriptions = true
13
- end
14
-
15
- config.mock_with :rspec do |mocks|
16
- mocks.verify_partial_doubles = true
17
- end
18
-
19
- config.filter_run :focus
20
- config.run_all_when_everything_filtered = true
21
-
22
- config.example_status_persistence_file_path = "spec/examples.txt"
23
-
24
- if config.files_to_run.one?
25
- config.default_formatter = 'doc'
26
- end
27
-
28
- config.profile_examples = 10
29
-
30
- config.order = :random
31
-
32
- Kernel.srand config.seed
33
-
34
- config.before(:suite) do
35
- DatabaseCleaner.strategy = :transaction
36
- DatabaseCleaner.clean_with(:truncation)
37
- end
38
-
39
- config.around(:each) do |example|
40
- DatabaseCleaner.cleaning do
41
- example.run
42
- end
43
- end
44
-
45
- config.before(:each, :logsql) do
46
- ActiveRecord::Base.logger = Logger.new(STDOUT)
47
- end
48
-
49
- config.after(:each, :logsql) do
50
- ActiveRecord::Base.logger = nil
51
- end
52
- end