journaled 2.0.0.alpha1 → 2.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- 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>.
|