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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8a474667ccaf5f102c3db4d12ac06f4eccf0e6c46c516736ba5ff6039e4c54d0
4
- data.tar.gz: 215c39cb7ecc5396ffda1ea074e826d9a4c9a3367b7bc3a13f9b1337501dc0b8
3
+ metadata.gz: 9281c4f53d4c4fba288f224c61ca7685a7deb8896b75cbff0198f516606b6171
4
+ data.tar.gz: 37767ffa7276832fcdf4b0ffb934b00f4d386e3b8d1a6a2974c8f9882c43a34c
5
5
  SHA512:
6
- metadata.gz: febca242b85002818369bfdfd8ed76a87f74b6799411a48362a7d0ee670b3227dd8f8afed11c187b1a03233eb4c4b40a537125e95f74fc21f988e8a2c483fd4d
7
- data.tar.gz: 95014407e067b0a9f2f6fd10d5e64b1df0085465f20f98a2079cb4fe88ceba6e7aaf8ec83ce38e0b047fb81e760eee6c3405340ffa0bda011a09d2147acee0f7
6
+ metadata.gz: '08dc2fa287cb4ff38cb2ebeeea1ccd84d6b0518a214481fee16163f38232b5631b7edb78538913d4f0c722db65108d7e04d914ce6bf70d96c1aa1b44ac5187d5'
7
+ data.tar.gz: c302f61b66be4060c2f1258d16f095cfdc46fabf9fbc9497d639e4b6e2095e4319d8073c93baedc3c8fcb8d4383e026e36dec878a1658885ab460549f946d9eb
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![Build Status](https://travis-ci.com/martintomas/reactive_observers.svg?branch=master)](https://travis-ci.com/martintomas/reactive_observers)
5
5
  [![codecov](https://codecov.io/gh/martintomas/reactive_observers/branch/master/graph/badge.svg)](https://codecov.io/gh/martintomas/reactive_observers)
6
6
 
7
- This gem can make observer from every possible class or object. 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.
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 observer is triggered.
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` - can be 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` - can be Symbol or Proc and defines action which is used to initialize observes class, for example: `notify: :load_all_dependent_objects`
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
- Class observers can become a bit tricky when initialization method is defined inside observer.
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 information. Fortunately, observing can be defined at object level which doesn't require initialization.
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 appropriate database triggers. __TODO:__ prepare jobs that generates appropriate triggers for defined tables.
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 topic table will listen on `topic_notices`. This default behaviour can be changed at configuration:
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 trigger data at any active record model:
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
- next if observer.only.present? && !observer.only.call(@observed_object)
18
-
19
- trigger_actions_for observer, Array.wrap(refine_observer_records_for(observer))
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 @observed_object if observer.refine.blank?
52
+ def refine_observer_records_for(observer, observed_object)
53
+ return observed_object if observer.refine.blank?
47
54
 
48
- observer.refine.call @observed_object
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ReactiveObservers
4
- VERSION = '0.0.1'.freeze
4
+ VERSION = '0.0.2'.freeze
5
5
  end
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.1
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