reactive_observers 0.0.1 → 0.0.2
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 +13 -9
- data/lib/generators/reactive_observers/USAGE +8 -0
- data/lib/generators/reactive_observers/postgresql_listeners_generator.rb +22 -0
- data/lib/generators/reactive_observers/templates/postgresql_listeners_migration.rb.tt +62 -0
- data/lib/reactive_observers/observable/notification.rb +13 -6
- data/lib/reactive_observers/observer/container.rb +2 -0
- data/lib/reactive_observers/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9281c4f53d4c4fba288f224c61ca7685a7deb8896b75cbff0198f516606b6171
|
4
|
+
data.tar.gz: 37767ffa7276832fcdf4b0ffb934b00f4d386e3b8d1a6a2974c8f9882c43a34c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '08dc2fa287cb4ff38cb2ebeeea1ccd84d6b0518a214481fee16163f38232b5631b7edb78538913d4f0c722db65108d7e04d914ce6bf70d96c1aa1b44ac5187d5'
|
7
|
+
data.tar.gz: c302f61b66be4060c2f1258d16f095cfdc46fabf9fbc9497d639e4b6e2095e4319d8073c93baedc3c8fcb8d4383e026e36dec878a1658885ab460549f946d9eb
|
data/README.md
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
[](https://travis-ci.com/martintomas/reactive_observers)
|
5
5
|
[](https://codecov.io/gh/martintomas/reactive_observers)
|
6
6
|
|
7
|
-
This gem can
|
7
|
+
This gem can transform every possible class or object to observer. Observer relation can be defined at Class level and processed dynamically when appropriate record changes. Observable module is using build in Active Record hooks or database triggers which can be turned on for specified tables in multiple App environment.
|
8
8
|
|
9
9
|
```ruby
|
10
10
|
class Topic < ActiveRecord::Base; end
|
@@ -40,7 +40,7 @@ Or install it yourself as:
|
|
40
40
|
|
41
41
|
## Usage
|
42
42
|
|
43
|
-
Observing relation can be initialized between object/class pairs. Observer can be any class or object. Observable has to be always Active Record class or object. It is recommended to define class observers as often as possible, because It enables to observe all active records independently and initialize all required data after
|
43
|
+
Observing relation can be initialized between object/class pairs. Observer can be any class or object. Observable entity has to be always Active Record class or object. It is recommended to define class observers as often as possible, because It enables to observe all active records independently and initialize all required data after observing action is triggered. It is also quite easy to maintain Observer classes when whole logic is encapsulated at one place.
|
44
44
|
|
45
45
|
Every class can be transformed to observer just by including following module:
|
46
46
|
|
@@ -56,8 +56,8 @@ Observe method accepts several different arguments which are:
|
|
56
56
|
* `fields` - observer is notified only when specified active record attributes are changed, for example: `fields: [:first_name, :last_name]`
|
57
57
|
* `only` - accepts Proc and can do additional complex filtering, for example: `only: ->(active_record) { active_record.type == 'ObservableType' }`
|
58
58
|
* active options
|
59
|
-
* `trigger` -
|
60
|
-
* `notify` -
|
59
|
+
* `trigger` - accepts symbol or Proc and defines action which is called on observer, for example: `trigger: :recompute_statistics`. Default value, which can be changed through configuration, is `:changed`.
|
60
|
+
* `notify` - accepts be Symbol or Proc and defines action which is used to initialize observer class, for example: `notify: :load_all_dependent_objects`
|
61
61
|
* `refine` - accepts Proc and defines operation which is done with active record object before observer is called, for example: `refine: ->(active_record) { active_record.topics }`
|
62
62
|
* additional options
|
63
63
|
* `context` - observer can be registered with context information which is provided back from observed object when notification happens. Example of such option can be for example: `context: :topic`
|
@@ -96,7 +96,7 @@ class TopicsStatisticService
|
|
96
96
|
end
|
97
97
|
```
|
98
98
|
|
99
|
-
|
99
|
+
It can be a bit tricky when classes with complex initialization process are transformed to observer - in such case, `notify` parameter is required which specifies how observer class is initialized
|
100
100
|
|
101
101
|
```ruby
|
102
102
|
class TopicStatisticService
|
@@ -113,7 +113,7 @@ class TopicStatisticService
|
|
113
113
|
end
|
114
114
|
```
|
115
115
|
|
116
|
-
Initialize method of class can quickly get out of the hand and It can be impossible to initialize it only with observed object
|
116
|
+
Initialize method of class can quickly get out of the hand and It can be impossible to initialize it only with observed object data. Fortunately, observing can be defined at object level which doesn't require initialization at all.
|
117
117
|
|
118
118
|
```ruby
|
119
119
|
class ComplexStatisticsService
|
@@ -183,13 +183,17 @@ To enable database observers, following configuration needs to be put into initi
|
|
183
183
|
config.observed_tables = [:topics] # names of tables which should be observed
|
184
184
|
end
|
185
185
|
|
186
|
-
It is also required to create
|
186
|
+
It is also required to create database triggers for observed tables which can be done with following command:
|
187
|
+
|
188
|
+
rails g reactive_observers:postgresql_listeners topics comments
|
189
|
+
|
190
|
+
This example will generate migration for PostgreSQL database and add triggers to topics and comments tables.
|
187
191
|
|
188
|
-
Gem listens on `TG_TABLE_NAME_notices` which for example means that observer for
|
192
|
+
Gem listens on `TG_TABLE_NAME_notices` which for example means that observer for topics table will listen on `topic_notices`. This default behaviour can be changed at configuration:
|
189
193
|
|
190
194
|
$ ReactiveObservers.configure { |config| config.listening_job_name = "%{table_name}_notices" }
|
191
195
|
|
192
|
-
Data obtained through trigger are forwarded to observers, but can be also used for different purposes (not just observing). It is possible to register any method that can process
|
196
|
+
Data obtained through trigger are forwarded to observers, but can be also used for different purposes (not just observing). It is possible to register any method that can process triggered data for any active record model:
|
193
197
|
|
194
198
|
```ruby
|
195
199
|
class Topic < ActiveRecord::Base
|
@@ -0,0 +1,8 @@
|
|
1
|
+
Description:
|
2
|
+
This generator creates migration with listeners for appropriate tables and database type
|
3
|
+
|
4
|
+
Example:
|
5
|
+
rails g reactive_observers:postgresql_listeners topics comments
|
6
|
+
|
7
|
+
This will create:
|
8
|
+
db/migrate/#{Time.now.strftime "%Y%m%d%H%M%S"}_create_postgresql_listeners.rb
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators/active_record"
|
4
|
+
|
5
|
+
module ReactiveObservers
|
6
|
+
class PostgresqlListenersGenerator < Rails::Generators::Base
|
7
|
+
include ActiveRecord::Generators::Migration
|
8
|
+
source_root File.expand_path('templates', __dir__)
|
9
|
+
|
10
|
+
argument :tables, type: :array, default: []
|
11
|
+
|
12
|
+
desc 'This generator creates migration with database listeners for postgresql database'
|
13
|
+
|
14
|
+
def copy_install_file
|
15
|
+
migration_template 'postgresql_listeners_migration.rb', File.join(db_migrate_path, "create_postgresql_listeners.rb")
|
16
|
+
end
|
17
|
+
|
18
|
+
def table_listeners
|
19
|
+
format ReactiveObservers.configuration.listening_job_name, table_name: ''
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
2
|
+
def change
|
3
|
+
execute <<-SQL
|
4
|
+
|
5
|
+
CREATE OR REPLACE FUNCTION jsonb_diff ( arg1 jsonb, arg2 jsonb ) RETURNS jsonb AS $$
|
6
|
+
SELECT
|
7
|
+
COALESCE(json_object_agg(key, value), '{}')::jsonb
|
8
|
+
FROM
|
9
|
+
jsonb_each(arg1)
|
10
|
+
WHERE
|
11
|
+
(arg1 -> key) <> (arg2 -> key)
|
12
|
+
OR (arg2 -> key) IS NULL
|
13
|
+
$$ LANGUAGE SQL;
|
14
|
+
|
15
|
+
CREATE OR REPLACE FUNCTION notice_insert() RETURNS trigger AS $$
|
16
|
+
DECLARE
|
17
|
+
channel_name varchar DEFAULT (TG_TABLE_NAME || '<%= table_listeners %>');
|
18
|
+
BEGIN
|
19
|
+
PERFORM pg_notify(channel_name, json_build_object('action', TG_OP, 'model', NEW)::text);
|
20
|
+
|
21
|
+
RETURN NEW;
|
22
|
+
END;
|
23
|
+
$$ LANGUAGE plpgsql;
|
24
|
+
|
25
|
+
CREATE OR REPLACE FUNCTION notice_update() RETURNS trigger AS $$
|
26
|
+
DECLARE
|
27
|
+
channel_name varchar DEFAULT (TG_TABLE_NAME || '<%= table_listeners %>');
|
28
|
+
js_new jsonb := row_to_json(NEW)::jsonb;
|
29
|
+
js_old jsonb := row_to_json(OLD)::jsonb;
|
30
|
+
BEGIN
|
31
|
+
PERFORM pg_notify(channel_name, json_build_object('action', TG_OP, 'model', NEW, 'old_model', OLD, 'diff', jsonb_diff(js_new, js_old))::text);
|
32
|
+
|
33
|
+
RETURN NEW;
|
34
|
+
END;
|
35
|
+
$$ LANGUAGE plpgsql;
|
36
|
+
|
37
|
+
CREATE OR REPLACE FUNCTION notice_delete() RETURNS trigger as $$
|
38
|
+
DECLARE
|
39
|
+
channel_name varchar DEFAULT (TG_TABLE_NAME || '<%= table_listeners %>');
|
40
|
+
BEGIN
|
41
|
+
PERFORM pg_notify(channel_name, json_build_object('action', TG_OP, 'model', OLD)::text);
|
42
|
+
|
43
|
+
RETURN OLD;
|
44
|
+
END;
|
45
|
+
$$ LANGUAGE plpgsql;
|
46
|
+
|
47
|
+
<% tables.each do |table_name| %>
|
48
|
+
CREATE TRIGGER notice_on_insert
|
49
|
+
AFTER INSERT ON public.<%= table_name %> FOR EACH ROW
|
50
|
+
EXECUTE PROCEDURE notice_insert();
|
51
|
+
|
52
|
+
CREATE TRIGGER notice_on_update
|
53
|
+
AFTER UPDATE ON public.<%= table_name %> FOR EACH ROW
|
54
|
+
EXECUTE PROCEDURE notice_update();
|
55
|
+
|
56
|
+
CREATE TRIGGER notice_on_delete
|
57
|
+
AFTER DELETE ON public.<%= table_name %> FOR EACH ROW
|
58
|
+
EXECUTE PROCEDURE notice_delete();
|
59
|
+
<% end %>
|
60
|
+
SQL
|
61
|
+
end
|
62
|
+
end
|
@@ -14,9 +14,10 @@ module ReactiveObservers
|
|
14
14
|
|
15
15
|
def perform
|
16
16
|
filter_observers.each do |observer|
|
17
|
-
|
18
|
-
|
19
|
-
|
17
|
+
process observer, @observed_object
|
18
|
+
if @action == :update && observer.trigger_with_previous_values
|
19
|
+
process observer, @observed_object.clone.assign_attributes(@options[:diff])
|
20
|
+
end
|
20
21
|
end
|
21
22
|
end
|
22
23
|
|
@@ -26,6 +27,12 @@ module ReactiveObservers
|
|
26
27
|
@filtered_observers ||= Filtering.new(@observed_object.id, @observers, @action, @options).perform
|
27
28
|
end
|
28
29
|
|
30
|
+
def process(observer, observed_object)
|
31
|
+
return if observer.only.present? && !observer.only.call(observed_object)
|
32
|
+
|
33
|
+
trigger_actions_for observer, Array.wrap(refine_observer_records_for(observer, observed_object))
|
34
|
+
end
|
35
|
+
|
29
36
|
def trigger_actions_for(observer, records)
|
30
37
|
records.each do |record|
|
31
38
|
Array.wrap(observer_objects_for(observer, record)).each do |observer_object|
|
@@ -42,10 +49,10 @@ module ReactiveObservers
|
|
42
49
|
trigger.call record, observer.to_h
|
43
50
|
end
|
44
51
|
|
45
|
-
def refine_observer_records_for(observer)
|
46
|
-
return
|
52
|
+
def refine_observer_records_for(observer, observed_object)
|
53
|
+
return observed_object if observer.refine.blank?
|
47
54
|
|
48
|
-
observer.refine.call
|
55
|
+
observer.refine.call observed_object
|
49
56
|
end
|
50
57
|
|
51
58
|
def observer_objects_for(observer, record)
|
@@ -10,6 +10,7 @@ module ReactiveObservers
|
|
10
10
|
attr_accessor :observer, :observed
|
11
11
|
attr_accessor :trigger, :notify, :refine, :context
|
12
12
|
attr_accessor :fields, :on, :only, :constrain
|
13
|
+
attr_accessor :trigger_with_previous_values
|
13
14
|
|
14
15
|
def initialize(observer, observed, options)
|
15
16
|
@observer = observer
|
@@ -23,6 +24,7 @@ module ReactiveObservers
|
|
23
24
|
@context = options[:context]
|
24
25
|
ReactiveObservers::Observer::ContainerValidator.new(self).run_validations!
|
25
26
|
@constrain = load_observer_constrains
|
27
|
+
@trigger_with_previous_values = options[:trigger_with_previous_values] || false
|
26
28
|
end
|
27
29
|
|
28
30
|
def compare
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: reactive_observers
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- martintomas
|
@@ -86,6 +86,9 @@ files:
|
|
86
86
|
- Rakefile
|
87
87
|
- bin/console
|
88
88
|
- bin/setup
|
89
|
+
- lib/generators/reactive_observers/USAGE
|
90
|
+
- lib/generators/reactive_observers/postgresql_listeners_generator.rb
|
91
|
+
- lib/generators/reactive_observers/templates/postgresql_listeners_migration.rb.tt
|
89
92
|
- lib/reactive_observers.rb
|
90
93
|
- lib/reactive_observers/base.rb
|
91
94
|
- lib/reactive_observers/configuration.rb
|