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.
- checksums.yaml +4 -4
- data/README.md +95 -25
- data/app/models/concerns/journaled/changes.rb +41 -0
- data/app/models/journaled/actor_uri_provider.rb +22 -0
- data/app/models/journaled/change_definition.rb +17 -1
- data/app/models/journaled/change_writer.rb +3 -22
- data/app/models/journaled/event.rb +0 -4
- data/config/initializers/change_protection.rb +3 -0
- data/lib/journaled.rb +9 -1
- data/lib/journaled/relation_change_protection.rb +27 -0
- data/lib/journaled/rspec.rb +18 -0
- data/lib/journaled/version.rb +1 -1
- data/spec/dummy/README.rdoc +28 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +26 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +21 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +37 -0
- data/spec/dummy/config/environments/production.rb +78 -0
- data/spec/dummy/config/environments/test.rb +39 -0
- data/spec/dummy/config/initializers/assets.rb +8 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/routes.rb +56 -0
- data/spec/dummy/config/secrets.yml +22 -0
- data/spec/dummy/db/migrate/20180606205114_create_delayed_jobs.rb +18 -0
- data/spec/dummy/db/schema.rb +31 -0
- data/spec/dummy/log/development.log +34 -0
- data/spec/dummy/log/test.log +34540 -0
- data/spec/dummy/public/404.html +67 -0
- data/spec/dummy/public/422.html +67 -0
- data/spec/dummy/public/500.html +66 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/lib/journaled_spec.rb +51 -0
- data/spec/models/concerns/journaled/actor_spec.rb +46 -0
- data/spec/models/concerns/journaled/changes_spec.rb +94 -0
- data/spec/models/database_change_protection_spec.rb +106 -0
- data/spec/models/journaled/actor_uri_provider_spec.rb +41 -0
- data/spec/models/journaled/change_writer_spec.rb +276 -0
- data/spec/models/journaled/delivery_spec.rb +156 -0
- data/spec/models/journaled/event_spec.rb +145 -0
- data/spec/models/journaled/json_schema_model/validator_spec.rb +133 -0
- data/spec/models/journaled/writer_spec.rb +129 -0
- data/spec/rails_helper.rb +20 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/support/delayed_job_spec_helper.rb +11 -0
- data/spec/support/environment_spec_helper.rb +16 -0
- metadata +113 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 205035e968787393ef42062f36bdb6068e48a962
|
4
|
+
data.tar.gz: bd6db1eec4f70ed2311e099d942014f9dd10c073
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
44
|
-
|
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
|
-
|
48
|
-
|
54
|
+
```ruby
|
55
|
+
Journaled.default_app_name = 'my_app'
|
56
|
+
```
|
49
57
|
|
50
|
-
|
51
|
-
|
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
|
-
|
54
|
-
`
|
55
|
-
`us-east-1`.
|
61
|
+
* `RUBY_AWS_ACCESS_KEY_ID`
|
62
|
+
* `RUBY_AWS_SECRET_ACCESS_KEY`
|
56
63
|
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
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
|
-
|
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]
|
data/lib/journaled.rb
CHANGED
@@ -20,5 +20,13 @@ module Journaled
|
|
20
20
|
@schema_providers ||= [Journaled::Engine, Rails]
|
21
21
|
end
|
22
22
|
|
23
|
-
|
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
|
data/lib/journaled/version.rb
CHANGED
@@ -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>.
|