journaled 2.0.0.alpha1 → 2.0.0.rc1

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +95 -25
  3. data/app/models/concerns/journaled/changes.rb +41 -0
  4. data/app/models/journaled/actor_uri_provider.rb +22 -0
  5. data/app/models/journaled/change_definition.rb +17 -1
  6. data/app/models/journaled/change_writer.rb +3 -22
  7. data/app/models/journaled/event.rb +0 -4
  8. data/config/initializers/change_protection.rb +3 -0
  9. data/lib/journaled.rb +9 -1
  10. data/lib/journaled/relation_change_protection.rb +27 -0
  11. data/lib/journaled/rspec.rb +18 -0
  12. data/lib/journaled/version.rb +1 -1
  13. data/spec/dummy/README.rdoc +28 -0
  14. data/spec/dummy/Rakefile +6 -0
  15. data/spec/dummy/bin/bundle +3 -0
  16. data/spec/dummy/bin/rails +4 -0
  17. data/spec/dummy/bin/rake +4 -0
  18. data/spec/dummy/config.ru +4 -0
  19. data/spec/dummy/config/application.rb +26 -0
  20. data/spec/dummy/config/boot.rb +5 -0
  21. data/spec/dummy/config/database.yml +21 -0
  22. data/spec/dummy/config/environment.rb +5 -0
  23. data/spec/dummy/config/environments/development.rb +37 -0
  24. data/spec/dummy/config/environments/production.rb +78 -0
  25. data/spec/dummy/config/environments/test.rb +39 -0
  26. data/spec/dummy/config/initializers/assets.rb +8 -0
  27. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  28. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  29. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  30. data/spec/dummy/config/initializers/inflections.rb +16 -0
  31. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  32. data/spec/dummy/config/initializers/session_store.rb +3 -0
  33. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  34. data/spec/dummy/config/locales/en.yml +23 -0
  35. data/spec/dummy/config/routes.rb +56 -0
  36. data/spec/dummy/config/secrets.yml +22 -0
  37. data/spec/dummy/db/migrate/20180606205114_create_delayed_jobs.rb +18 -0
  38. data/spec/dummy/db/schema.rb +31 -0
  39. data/spec/dummy/log/development.log +34 -0
  40. data/spec/dummy/log/test.log +34540 -0
  41. data/spec/dummy/public/404.html +67 -0
  42. data/spec/dummy/public/422.html +67 -0
  43. data/spec/dummy/public/500.html +66 -0
  44. data/spec/dummy/public/favicon.ico +0 -0
  45. data/spec/lib/journaled_spec.rb +51 -0
  46. data/spec/models/concerns/journaled/actor_spec.rb +46 -0
  47. data/spec/models/concerns/journaled/changes_spec.rb +94 -0
  48. data/spec/models/database_change_protection_spec.rb +106 -0
  49. data/spec/models/journaled/actor_uri_provider_spec.rb +41 -0
  50. data/spec/models/journaled/change_writer_spec.rb +276 -0
  51. data/spec/models/journaled/delivery_spec.rb +156 -0
  52. data/spec/models/journaled/event_spec.rb +145 -0
  53. data/spec/models/journaled/json_schema_model/validator_spec.rb +133 -0
  54. data/spec/models/journaled/writer_spec.rb +129 -0
  55. data/spec/rails_helper.rb +20 -0
  56. data/spec/spec_helper.rb +22 -0
  57. data/spec/support/delayed_job_spec_helper.rb +11 -0
  58. data/spec/support/environment_spec_helper.rb +16 -0
  59. metadata +113 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4ee54a77497db5d43b3932d6015b18b6b2c4ae45
4
- data.tar.gz: 6fc6bed161faf09676c7578531eb563a47ee9e6b
3
+ metadata.gz: 205035e968787393ef42062f36bdb6068e48a962
4
+ data.tar.gz: bd6db1eec4f70ed2311e099d942014f9dd10c073
5
5
  SHA512:
6
- metadata.gz: 030301523134d5a36c15de4e7096917c9d948c00c1514c41c9e82ff0184f358f9c346786e9f1757d229c0050073f996b03ff2bac038a7a773a9f9c02bd56bd5a
7
- data.tar.gz: a6d3c21d82902e96f3486b3637365b20d0803a11ece98260121bc80b73555bf823b3eaf6731ba82daa8c83e6044f815793e4c5d73c996d8606eed7f0dec85f5a
6
+ metadata.gz: 4f482a501e506d57a38345a985485846f842a714c16a0e4af90b1a130751af5ec482759ed1a72aada99db5435adf5fbea3f137caa3e7e341f245ebbd4a5bb0d3
7
+ data.tar.gz: 46644b2d30fc64f0c52a810203940a8bffc6b8cd854635199582e356bbeb61c2749f87aa7046f8fa44d33011e384ec31983bd0be6537735d69d118777b84ec5f
data/README.md CHANGED
@@ -26,37 +26,44 @@ if you haven't already.
26
26
 
27
27
  2. To integrate Journaled into your application, simply include the gem in your
28
28
  app's Gemfile.
29
+
30
+ ```ruby
31
+ gem 'journaled'
29
32
  ```
30
- gem 'journaled', https_github: 'Betterment/journaled'
33
+
34
+ If you use rspec, add the following to your rails helper:
35
+
36
+ ```ruby
37
+ # spec/rails_helper.rb
38
+
39
+ # ... your other requires
40
+ require 'journaled/rspec'
31
41
  ```
32
- 3. You will also need to define the following environment variables to allow Journaled to publish events to your AWS Kinesis event stream:
33
42
 
34
- * `JOURNALED_STREAM_NAME`
43
+ 3. You will also need to define the following environment variables to allow Journaled to publish events to your AWS Kinesis event stream:
35
44
 
36
- Special case: if your `Journaled::Event` objects override the
37
- `#journaled_app_name` method to a non-nil value e.g. `my_app`, you will
38
- instead need to provide a corresponding
39
- `[upcased_app_name]_JOURNALED_STREAM_NAME` variable for each distinct
40
- value, e.g. `MY_APP_JOURNALED_STREAM_NAME`. You can provide a default value
41
- for all `Journaled::Event`s in an initializer like this:
45
+ * `JOURNALED_STREAM_NAME`
42
46
 
43
- ```ruby
44
- Journaled.default_app_name = 'my_app'
45
- ```
47
+ Special case: if your `Journaled::Event` objects override the
48
+ `#journaled_app_name` method to a non-nil value e.g. `my_app`, you will
49
+ instead need to provide a corresponding
50
+ `[upcased_app_name]_JOURNALED_STREAM_NAME` variable for each distinct
51
+ value, e.g. `MY_APP_JOURNALED_STREAM_NAME`. You can provide a default value
52
+ for all `Journaled::Event`s in an initializer like this:
46
53
 
47
- You may optionally define the following ENV vars to specify AWS
48
- credentials outside of the locations that the AWS SDK normally looks:
54
+ ```ruby
55
+ Journaled.default_app_name = 'my_app'
56
+ ```
49
57
 
50
- * `RUBY_AWS_ACCESS_KEY_ID`
51
- * `RUBY_AWS_SECRET_ACCESS_KEY`
58
+ You may optionally define the following ENV vars to specify AWS
59
+ credentials outside of the locations that the AWS SDK normally looks:
52
60
 
53
- You may also specify the region to target your AWS stream by setting
54
- `AWS_DEFAULT_REGION`. If you don't specify, Journaled will default to
55
- `us-east-1`.
61
+ * `RUBY_AWS_ACCESS_KEY_ID`
62
+ * `RUBY_AWS_SECRET_ACCESS_KEY`
56
63
 
57
- Journaled::Event provides a `commit_hash` method which you may journal
58
- if you like. If you choose to use it, you must provide a `GIT_COMMIT`
59
- environment variable.
64
+ You may also specify the region to target your AWS stream by setting
65
+ `AWS_DEFAULT_REGION`. If you don't specify, Journaled will default to
66
+ `us-east-1`.
60
67
 
61
68
  ## Usage
62
69
 
@@ -66,7 +73,7 @@ Out of the box, `Journaled` provides an event type and ActiveRecord
66
73
  mix-in for durably journaling changes to your model, implemented via
67
74
  ActiveRecord hooks. Use it like so:
68
75
 
69
- ```
76
+ ```ruby
70
77
  class User < ApplicationRecord
71
78
  include Journaled::Changes
72
79
 
@@ -76,7 +83,7 @@ end
76
83
 
77
84
  Add the following to your controller base class for attribution:
78
85
 
79
- ```
86
+ ```ruby
80
87
  class ApplicationController < ActionController::Base
81
88
  include Journaled::Actor
82
89
 
@@ -101,10 +108,30 @@ record is created or destroyed, an event will be sent to Kinesis with the follow
101
108
  your `journal_changes_to` declaration (e.g. `identity_change`)
102
109
  * `changes` - a serialized JSON object representing the latest values
103
110
  of any new or changed attributes from the specified set (e.g.
104
- `{"email":"mynewemail@example.com"}`)
111
+ `{"email":"mynewemail@example.com"}`). Upon destroy, all
112
+ specified attributes will be serialized as they were last stored.
105
113
  * `actor` - a string (usually a rails global_id) representing who
106
114
  performed the action.
107
115
 
116
+ Callback-bypassing database methods like `update_all`, `delete_all`,
117
+ `update_columns` and `delete` are intercepted and will require an
118
+ additional `force: true` argument if they would interfere with change
119
+ journaling. Note that the less-frequently-used methods `toggle`,
120
+ `increment*`, `decrement*`, and `update_counters` are not intercepted at
121
+ this time.
122
+
123
+ #### Testing
124
+
125
+ If you use RSpec (and have required `journaled/rspec` in your
126
+ `spec/rails_helper.rb`), you can regression-protect important journaling
127
+ config with the `journal_changes_to` matcher:
128
+
129
+ ```ruby
130
+ it "journals exactly these things or there will be heck to pay" do
131
+ expect(User).to journal_changes_to(:email, :first_name, :last_name, as: :identity_change)
132
+ end
133
+ ```
134
+
108
135
  ### Custom Journaling
109
136
 
110
137
  For every custom implementation of journaling in your application, define the JSON schema for the attributes in your event.
@@ -189,6 +216,49 @@ An event like the following will be journaled to kinesis:
189
216
  }
190
217
  ```
191
218
 
219
+ ### Helper methods for custom events
220
+
221
+ Journaled provides a couple helper methods that may be useful in your
222
+ custom events. You can add whichever you need your event types like
223
+ this:
224
+
225
+ ```ruby
226
+ # my_event.rb
227
+ class MyEvent
228
+ include Journaled::Event
229
+
230
+ journal_attributes :commit_hash, :actor_uri # ... etc, etc
231
+
232
+ def commit_hash
233
+ Journaled.commit_hash
234
+ end
235
+
236
+ def actor_uri
237
+ Journaled.actor_uri
238
+ end
239
+
240
+ # ... etc, etc
241
+ end
242
+ ```
243
+
244
+ #### `Journaled.commit_hash`
245
+
246
+ If you choose to use it, you must provide a `GIT_COMMIT` environment
247
+ variable. `Journaled.commit_hash` will fail if it is undefined.
248
+
249
+ #### `Journaled.actor_uri`
250
+
251
+ Returns one of the following in order of preference:
252
+
253
+ * The current controller-defined `journaled_actor`'s GlobalID, if
254
+ set
255
+ * A string of the form `gid://[app_name]/[os_username]` if performed on
256
+ the command line
257
+ * a string of the form `gid://[app_name]` as a fallback
258
+
259
+ In order for this to be most useful, you must configure your controller
260
+ as described in [Change Journaling](#change-journaling) above.
261
+
192
262
  ## Future improvements & issue tracking
193
263
  Suggestions for enhancements to this engine are currently being tracked via Github Issues. Please feel free to open an
194
264
  issue for a desired feature, as well as for any observed bugs.
@@ -3,7 +3,9 @@ module Journaled::Changes
3
3
 
4
4
  included do
5
5
  cattr_accessor :_journaled_change_definitions
6
+ cattr_accessor :journaled_attribute_names
6
7
  self._journaled_change_definitions = []
8
+ self.journaled_attribute_names = []
7
9
 
8
10
  after_create do
9
11
  self.class._journaled_change_definitions.each do |definition|
@@ -24,6 +26,32 @@ module Journaled::Changes
24
26
  end
25
27
  end
26
28
 
29
+ def delete(force: false)
30
+ if force || self.class.journaled_attribute_names.empty?
31
+ super()
32
+ else
33
+ raise(<<~ERROR)
34
+ #delete aborted by Journaled::Changes.
35
+
36
+ Invoke #delete(force: true) to override and skip journaling.
37
+ ERROR
38
+ end
39
+ end
40
+
41
+ def update_columns(attributes, force: false)
42
+ unless force || self.class.journaled_attribute_names.empty?
43
+ conflicting_journaled_attribute_names = self.class.journaled_attribute_names & attributes.keys.map(&:to_sym)
44
+ raise(<<~ERROR) if conflicting_journaled_attribute_names.present?
45
+ #update_columns aborted by Journaled::Changes due to journaled attributes:
46
+
47
+ #{conflicting_journaled_attribute_names.join(', ')}
48
+
49
+ Invoke #update_columns with additional arg `{ force: true }` to override and skip journaling.
50
+ ERROR
51
+ end
52
+ super(attributes)
53
+ end
54
+
27
55
  class_methods do
28
56
  def journal_changes_to(*attribute_names, as:) # rubocop:disable Naming/UncommunicativeMethodParamName
29
57
  if attribute_names.empty? || attribute_names.any? { |n| !n.is_a?(Symbol) }
@@ -33,6 +61,19 @@ module Journaled::Changes
33
61
  raise "as: must be a symbol" unless as.is_a?(Symbol)
34
62
 
35
63
  _journaled_change_definitions << Journaled::ChangeDefinition.new(attribute_names: attribute_names, logical_operation: as)
64
+ journaled_attribute_names.concat(attribute_names)
65
+ end
66
+
67
+ def delete(id_or_array, force: false)
68
+ if force || journaled_attribute_names.empty?
69
+ where(primary_key => id_or_array).delete_all(force: true)
70
+ else
71
+ raise(<<~ERROR)
72
+ #delete aborted by Journaled::Changes.
73
+
74
+ Invoke #delete(id_or_array, force: true) to override and skip journaling.
75
+ ERROR
76
+ end
36
77
  end
37
78
  end
38
79
  end
@@ -0,0 +1,22 @@
1
+ class Journaled::ActorUriProvider
2
+ include Singleton
3
+
4
+ def actor_uri
5
+ actor_global_id_uri || fallback_global_id_uri
6
+ end
7
+
8
+ private
9
+
10
+ def actor_global_id_uri
11
+ actor = RequestStore.store[:journaled_actor_proc]&.call
12
+ actor.to_global_id.to_s if actor
13
+ end
14
+
15
+ def fallback_global_id_uri
16
+ if defined?(::Rails::Console) || File.basename($PROGRAM_NAME) == "rake"
17
+ "gid://local/#{Etc.getlogin}"
18
+ else
19
+ "gid://#{Rails.application.config.global_id.app}"
20
+ end
21
+ end
22
+ end
@@ -2,7 +2,23 @@ class Journaled::ChangeDefinition
2
2
  attr_reader :attribute_names, :logical_operation
3
3
 
4
4
  def initialize(attribute_names:, logical_operation:)
5
- @attribute_names = attribute_names
5
+ @attribute_names = attribute_names.map(&:to_s)
6
6
  @logical_operation = logical_operation
7
+ @validated = false
8
+ end
9
+
10
+ def validated?
11
+ @validated
12
+ end
13
+
14
+ def validate!(model)
15
+ nonexistent_attribute_names = attribute_names - model.class.attribute_names
16
+ raise <<~ERROR if nonexistent_attribute_names.present?
17
+ Unable to persist #{model} because `journal_changes_to, as: #{logical_operation.inspect}`
18
+ includes nonexistant attributes:
19
+
20
+ #{nonexistent_attribute_names.join(', ')}
21
+ ERROR
22
+ @validated = true
7
23
  end
8
24
  end
@@ -1,14 +1,11 @@
1
1
  class Journaled::ChangeWriter
2
2
  attr_reader :model, :change_definition
3
- delegate :logical_operation, to: :change_definition
3
+ delegate :attribute_names, :logical_operation, to: :change_definition
4
4
 
5
5
  def initialize(model:, change_definition:)
6
6
  @model = model
7
7
  @change_definition = change_definition
8
- end
9
-
10
- def attribute_names
11
- @attribute_names ||= change_definition.attribute_names.map(&:to_s)
8
+ change_definition.validate!(model) unless change_definition.validated?
12
9
  end
13
10
 
14
11
  def create
@@ -48,27 +45,11 @@ class Journaled::ChangeWriter
48
45
  end
49
46
 
50
47
  def actor_uri
51
- actor_global_id_uri || fallback_global_id_uri
48
+ @actor_uri ||= Journaled.actor_uri
52
49
  end
53
50
 
54
51
  private
55
52
 
56
- def actor_global_id_uri
57
- actor.to_global_id.to_s if actor
58
- end
59
-
60
- def actor
61
- @actor ||= RequestStore.store[:journaled_actor_proc]&.call
62
- end
63
-
64
- def fallback_global_id_uri
65
- if defined?(::Rails::Console) || File.basename($PROGRAM_NAME) == "rake"
66
- "gid://local/#{Etc.getlogin}"
67
- else
68
- "gid://#{Rails.application.config.global_id.app}"
69
- end
70
- end
71
-
72
53
  def pluck_changed_values(change_hash, index:)
73
54
  change_hash.each_with_object({}) do |(k, v), result|
74
55
  result[k] = v[index]
@@ -19,10 +19,6 @@ module Journaled::Event
19
19
  @created_at ||= Time.zone.now
20
20
  end
21
21
 
22
- def commit_hash
23
- @commit_hash ||= ENV.fetch('GIT_COMMIT')
24
- end
25
-
26
22
  # Event metadata and configuration (not serialized)
27
23
 
28
24
  def journaled_schema_name
@@ -0,0 +1,3 @@
1
+ require 'journaled/relation_change_protection'
2
+
3
+ ActiveRecord::Relation.class_eval { prepend Journaled::RelationChangeProtection }
@@ -20,5 +20,13 @@ module Journaled
20
20
  @schema_providers ||= [Journaled::Engine, Rails]
21
21
  end
22
22
 
23
- module_function :development_or_test?, :enabled?, :schema_providers
23
+ def commit_hash
24
+ ENV.fetch('GIT_COMMIT')
25
+ end
26
+
27
+ def actor_uri
28
+ Journaled::ActorUriProvider.instance.actor_uri
29
+ end
30
+
31
+ module_function :development_or_test?, :enabled?, :schema_providers, :commit_hash, :actor_uri
24
32
  end
@@ -0,0 +1,27 @@
1
+ module Journaled::RelationChangeProtection
2
+ def update_all(updates, force: false)
3
+ unless force || !@klass.respond_to?(:journaled_attribute_names) || @klass.journaled_attribute_names.empty?
4
+ conflicting_journaled_attribute_names = @klass.journaled_attribute_names & updates.keys.map(&:to_sym)
5
+ raise(<<~ERROR) if conflicting_journaled_attribute_names.present?
6
+ #update_all aborted by Journaled::Changes due to journaled attributes:
7
+
8
+ #{conflicting_journaled_attribute_names.join(', ')}
9
+
10
+ Invoke #update_all with additional arg `{ force: true }` to override and skip journaling.
11
+ ERROR
12
+ end
13
+ super(updates)
14
+ end
15
+
16
+ def delete_all(force: false)
17
+ if force || !@klass.respond_to?(:journaled_attribute_names) || @klass.journaled_attribute_names.empty?
18
+ super()
19
+ else
20
+ raise(<<~ERROR)
21
+ #delete_all aborted by Journaled::Changes.
22
+
23
+ Invoke #delete_all(force: true) to override and skip journaling.
24
+ ERROR
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,18 @@
1
+ require 'rspec/expectations'
2
+
3
+ RSpec::Matchers.define :journal_changes_to do |*attribute_names, as:|
4
+ match do |model_class|
5
+ model_class._journaled_change_definitions.any? do |change_definition|
6
+ change_definition.logical_operation == as &&
7
+ attribute_names.map(&:to_s).sort == change_definition.attribute_names.sort
8
+ end
9
+ end
10
+
11
+ failure_message do |model_class|
12
+ "expected #{model_class} to journal changes to #{attribute_names.map(&:inspect).join(', ')} as #{as.inspect}"
13
+ end
14
+
15
+ failure_message_when_negated do |model_class|
16
+ "expected #{model_class} not to journal changes to #{attribute_names.map(&:inspect).join(', ')} as #{as.inspect}"
17
+ end
18
+ end
@@ -1,3 +1,3 @@
1
1
  module Journaled
2
- VERSION = "2.0.0.alpha1".freeze
2
+ VERSION = "2.0.0.rc1".freeze
3
3
  end
@@ -0,0 +1,28 @@
1
+ == README
2
+
3
+ This README would normally document whatever steps are necessary to get the
4
+ application up and running.
5
+
6
+ Things you may want to cover:
7
+
8
+ * Ruby version
9
+
10
+ * System dependencies
11
+
12
+ * Configuration
13
+
14
+ * Database creation
15
+
16
+ * Database initialization
17
+
18
+ * How to run the test suite
19
+
20
+ * Services (job queues, cache servers, search engines, etc.)
21
+
22
+ * Deployment instructions
23
+
24
+ * ...
25
+
26
+
27
+ Please feel free to use a different markup language if you do not plan to run
28
+ <tt>rake doc:app</tt>.