jason-rails 0.5.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/README.md +14 -5
  4. data/app/controllers/jason/api/pusher_controller.rb +15 -0
  5. data/app/controllers/jason/api_controller.rb +44 -2
  6. data/client/lib/JasonProvider.js +1 -1
  7. data/client/lib/createJasonReducers.js +7 -0
  8. data/client/lib/createPayloadHandler.d.ts +6 -3
  9. data/client/lib/createPayloadHandler.js +8 -4
  10. data/client/lib/createTransportAdapter.d.ts +5 -0
  11. data/client/lib/createTransportAdapter.js +20 -0
  12. data/client/lib/pruneIdsMiddleware.js +9 -11
  13. data/client/lib/transportAdapters/actionCableAdapter.d.ts +5 -0
  14. data/client/lib/transportAdapters/actionCableAdapter.js +35 -0
  15. data/client/lib/transportAdapters/pusherAdapter.d.ts +5 -0
  16. data/client/lib/transportAdapters/pusherAdapter.js +68 -0
  17. data/client/lib/useJason.js +14 -34
  18. data/client/lib/useJason.test.js +8 -2
  19. data/client/package.json +2 -1
  20. data/client/src/JasonProvider.tsx +1 -1
  21. data/client/src/createJasonReducers.ts +7 -0
  22. data/client/src/createPayloadHandler.ts +9 -4
  23. data/client/src/createTransportAdapter.ts +13 -0
  24. data/client/src/pruneIdsMiddleware.ts +11 -11
  25. data/client/src/restClient.ts +1 -0
  26. data/client/src/transportAdapters/actionCableAdapter.ts +38 -0
  27. data/client/src/transportAdapters/pusherAdapter.ts +72 -0
  28. data/client/src/useJason.test.ts +8 -2
  29. data/client/src/useJason.ts +15 -36
  30. data/client/yarn.lock +12 -0
  31. data/config/routes.rb +5 -1
  32. data/lib/jason.rb +29 -8
  33. data/lib/jason/broadcaster.rb +19 -0
  34. data/lib/jason/channel.rb +6 -2
  35. data/lib/jason/graph_helper.rb +165 -0
  36. data/lib/jason/includes_helper.rb +108 -0
  37. data/lib/jason/lua_generator.rb +23 -1
  38. data/lib/jason/publisher.rb +16 -16
  39. data/lib/jason/subscription.rb +208 -183
  40. data/lib/jason/version.rb +1 -1
  41. metadata +15 -2
data/lib/jason.rb CHANGED
@@ -7,22 +7,43 @@ require 'jason/api_model'
7
7
  require 'jason/channel'
8
8
  require 'jason/publisher'
9
9
  require 'jason/subscription'
10
+ require 'jason/broadcaster'
10
11
  require 'jason/engine'
11
12
  require 'jason/lua_generator'
13
+ require 'jason/includes_helper'
14
+ require 'jason/graph_helper'
12
15
 
13
16
  module Jason
14
17
  class Error < StandardError; end
15
18
 
16
- $redis_jason = ::ConnectionPool::Wrapper.new(size: 5, timeout: 3) { ::Redis.new(url: ENV['REDIS_URL']) }
19
+ self.mattr_accessor :schema
20
+ self.mattr_accessor :transport_service
21
+ self.mattr_accessor :redis
22
+ self.mattr_accessor :pusher
23
+ self.mattr_accessor :pusher_key
24
+ self.mattr_accessor :pusher_region
25
+ self.mattr_accessor :pusher_channel_prefix
26
+ self.mattr_accessor :authorization_service
17
27
 
28
+ self.schema = {}
29
+ self.transport_service = :action_cable
30
+ self.pusher_region = 'eu'
31
+ self.pusher_channel_prefix = 'jason'
18
32
 
19
- self.mattr_accessor :schema
20
- self.schema = {}
21
- # add default values of more config vars here
33
+ # add default values of more config vars here
22
34
 
23
- # this function maps the vars from your app into your engine
24
- def self.setup(&block)
25
- yield self
26
- end
35
+ # this function maps the vars from your app into your engine
36
+ def self.setup(&block)
37
+ yield self
38
+ end
27
39
 
40
+ $redis_jason = self.redis || ::ConnectionPool::Wrapper.new(size: 5, timeout: 3) { ::Redis.new(url: ENV['REDIS_URL']) }
41
+
42
+ if ![:action_cable, :pusher].include?(self.transport_service)
43
+ raise "Unknown transport service '#{self.transport_service}' specified"
44
+ end
45
+
46
+ if self.transport_service == :pusher && self.pusher.blank?
47
+ raise "Pusher specified as transport service but no Pusher client provided. Please configure with config.pusher = Pusher::Client.new(...)"
48
+ end
28
49
  end
@@ -0,0 +1,19 @@
1
+ class Jason::Broadcaster
2
+ attr_reader :channel
3
+
4
+ def initialize(channel)
5
+ @channel = channel
6
+ end
7
+
8
+ def pusher_channel_name
9
+ "private-#{Jason.pusher_channel_prefix}-#{channel}"
10
+ end
11
+
12
+ def broadcast(message)
13
+ if Jason.transport_service == :action_cable
14
+ ActionCable.server.broadcast(channel, message)
15
+ elsif Jason.transport_service == :pusher
16
+ Jason.pusher.trigger(pusher_channel_name, 'changed', message)
17
+ end
18
+ end
19
+ end
data/lib/jason/channel.rb CHANGED
@@ -32,6 +32,8 @@ class Jason::Channel < ActionCable::Channel::Base
32
32
 
33
33
  def create_subscription(model, conditions, includes)
34
34
  subscription = Jason::Subscription.upsert_by_config(model, conditions: conditions || {}, includes: includes || nil)
35
+
36
+ return if !subscription.user_can_access?(current_user)
35
37
  stream_from subscription.channel
36
38
 
37
39
  subscriptions.push(subscription)
@@ -52,10 +54,12 @@ class Jason::Channel < ActionCable::Channel::Base
52
54
 
53
55
  def get_payload(config, force_refresh = false)
54
56
  subscription = Jason::Subscription.upsert_by_config(config['model'], conditions: config['conditions'], includes: config['includes'])
57
+
58
+ return if !subscription.user_can_access?(current_user)
55
59
  if force_refresh
56
- subscription.set_ids(enforce: true)
60
+ subscription.set_ids_for_sub_models
57
61
  end
58
- subscription.get.each do |payload|
62
+ subscription.get.each do |model_name, payload|
59
63
  transmit(payload) if payload.present?
60
64
  end
61
65
  end
@@ -0,0 +1,165 @@
1
+ class Jason::GraphHelper
2
+ attr_reader :id, :includes_helper
3
+
4
+ def initialize(id, includes_helper)
5
+ @id = id
6
+ @includes_helper = includes_helper
7
+ end
8
+
9
+ def add_edge(parent_model, parent_id, child_model, child_id)
10
+ edge = "#{parent_model}:#{parent_id}/#{child_model}:#{child_id}"
11
+ $redis_jason.sadd("jason:subscriptions:#{id}:graph", edge)
12
+ end
13
+
14
+ def remove_edge(parent_model, parent_id, child_model, child_id)
15
+ edge = "#{parent_model}:#{parent_id}/#{child_model}:#{child_id}"
16
+ $redis_jason.srem("jason:subscriptions:#{id}:graph", edge)
17
+ end
18
+
19
+ def add_edges(all_models, instance_ids)
20
+ edges = build_edges(all_models, instance_ids)
21
+ $redis_jason.sadd("jason:subscriptions:#{id}:graph", edges)
22
+ end
23
+
24
+ def remove_edges(all_models, instance_ids)
25
+ edges = build_edges(all_models, instance_ids)
26
+ $redis_jason.srem("jason:subscriptions:#{id}:graph", edges)
27
+ end
28
+
29
+ def apply_remove_node(node)
30
+ edges = $redis_jason.smembers("jason:subscriptions:#{id}:graph")
31
+ edges = find_edges_with_node(edges, node)
32
+ diff_edges_from_graph(remove_edges: edges)
33
+ end
34
+
35
+ # Add and remove edges, return graph before and after
36
+ def apply_update(add: nil, remove: nil)
37
+ add_edges = []
38
+ remove_edges = []
39
+
40
+ if add.present?
41
+ add.each do |edge_set|
42
+ add_edges += build_edges(edge_set[:model_names], edge_set[:instance_ids])
43
+ end
44
+ end
45
+
46
+ if remove.present?
47
+ remove.each do |edge_set|
48
+ remove_edges += build_edges(edge_set[:model_names], edge_set[:instance_ids], include_root: false)
49
+ end
50
+ end
51
+ diff_edges_from_graph(add_edges: add_edges, remove_edges: remove_edges)
52
+ end
53
+
54
+ def diff_edges_from_graph(add_edges: [], remove_edges: [])
55
+ old_edges, new_edges = Jason::LuaGenerator.new.update_set_with_diff("jason:subscriptions:#{id}:graph", add_edges.flatten, remove_edges.flatten)
56
+
57
+ old_graph = build_graph_from_edges(old_edges)
58
+ new_graph = build_graph_from_edges(new_edges)
59
+
60
+ old_nodes = (old_graph.values + old_graph.keys).flatten.uniq - ['root']
61
+ new_nodes = (new_graph.values + new_graph.keys).flatten.uniq - ['root']
62
+ orphan_nodes = find_orphans_in_graph(new_graph)
63
+
64
+ added_nodes = new_nodes - old_nodes - orphan_nodes
65
+ removed_nodes = old_nodes - new_nodes + orphan_nodes
66
+
67
+ orphaned_edges = orphan_nodes.map do |node|
68
+ find_edges_with_node(new_edges, node)
69
+ end.flatten
70
+
71
+ if orphaned_edges.present?
72
+ $redis_jason.srem("jason:subscriptions:#{id}:graph", orphaned_edges)
73
+ end
74
+
75
+ ids_to_add = {}
76
+ ids_to_remove = {}
77
+
78
+ added_nodes.each do |node|
79
+ model_name, instance_id = node.split(':')
80
+ ids_to_add[model_name] ||= []
81
+ ids_to_add[model_name].push(instance_id)
82
+ end
83
+
84
+ removed_nodes.each do |node|
85
+ model_name, instance_id = node.split(':')
86
+ ids_to_remove[model_name] ||= []
87
+ ids_to_remove[model_name].push(instance_id)
88
+ end
89
+
90
+ { ids_to_remove: ids_to_remove, ids_to_add: ids_to_add }
91
+ end
92
+
93
+ def find_edges_with_node(edges, node)
94
+ edges.select do |edge|
95
+ parent, child = edge.split('/')
96
+ parent == node || child == node
97
+ end
98
+ end
99
+
100
+ def find_orphans
101
+ edges = $redis_jason.smembers("jason:subscriptions:#{id}:graph")
102
+ graph = build_graph_from_edges(edges)
103
+ find_orphans_in_graph(graph)
104
+ end
105
+
106
+ def find_orphans_in_graph(graph)
107
+ reachable_nodes = get_reachable_nodes(graph)
108
+ all_nodes = (graph.values + graph.keys).flatten.uniq - ['root']
109
+ all_nodes - reachable_nodes
110
+ end
111
+
112
+ def get_reachable_nodes(graph, parent = 'root')
113
+ reached_nodes = graph[parent] || []
114
+ reached_nodes.each do |child|
115
+ reached_nodes += get_reachable_nodes(graph, child)
116
+ end
117
+ reached_nodes
118
+ end
119
+
120
+ def build_graph_from_edges(edges)
121
+ graph = {}
122
+ edges.each do |edge|
123
+ parent, child = edge.split('/')
124
+ graph[parent] ||= []
125
+ graph[parent].push(child)
126
+ end
127
+ graph
128
+ end
129
+
130
+ private
131
+
132
+ def build_edges(all_models, instance_ids, include_root: true)
133
+ # Build the tree
134
+ # Find parent -> child model relationships
135
+ edges = []
136
+
137
+ all_models.each_with_index do |parent_model, parent_idx|
138
+ all_models.each_with_index do |child_model, child_idx|
139
+ next if parent_model == child_model
140
+ next if !includes_helper.in_sub(parent_model, child_model)
141
+
142
+ pairs = instance_ids.map { |row| [row[parent_idx], row[child_idx]] }
143
+ .uniq
144
+ .reject{ |pair| pair[0].blank? || pair[1].blank? }
145
+
146
+ edges += pairs.map.each do |pair|
147
+ "#{parent_model}:#{pair[0]}/#{child_model}:#{pair[1]}"
148
+ end
149
+ end
150
+ end
151
+
152
+ root_model = includes_helper.root_model
153
+
154
+ if include_root && all_models.include?(root_model)
155
+ root_idx = all_models.find_index(root_model)
156
+ root_ids = instance_ids.map { |row| row[root_idx] }.uniq.compact
157
+
158
+ edges += root_ids.map do |id|
159
+ "root/#{root_model}:#{id}"
160
+ end
161
+ end
162
+
163
+ edges
164
+ end
165
+ end
@@ -0,0 +1,108 @@
1
+ # Helper to provide other modules with information about the includes of a subscription
2
+
3
+ class Jason::IncludesHelper
4
+ attr_accessor :main_tree
5
+
6
+ def initialize(main_tree)
7
+ raise "Root must be hash" if !main_tree.is_a?(Hash)
8
+ raise "Only one root key allowed" if main_tree.keys.size != 1
9
+ @main_tree = main_tree
10
+ end
11
+
12
+ def all_models_recursive(tree)
13
+ sub_models = if tree.is_a?(Hash)
14
+ tree.map do |k,v|
15
+ [k, all_models_recursive(v)]
16
+ end
17
+ elsif tree.is_a?(Array)
18
+ tree.map do |v|
19
+ all_models_recursive(v)
20
+ end
21
+ else
22
+ tree
23
+ end
24
+ end
25
+
26
+ def all_models(model_name = nil)
27
+ model_name = model_name.presence || root_model
28
+ assoc_name = get_assoc_name(model_name)
29
+ tree = get_tree_for(assoc_name)
30
+ [model_name, all_models_recursive(tree)].flatten.uniq.map(&:to_s).map(&:singularize)
31
+ end
32
+
33
+ def root_model
34
+ main_tree.keys[0]
35
+ end
36
+
37
+ # assoc could be plural or not, so need to scan both.
38
+ def get_assoc_name(model_name, haystack = main_tree)
39
+ if haystack.is_a?(Hash)
40
+ haystack.each do |assoc_name, includes_tree|
41
+ if model_name.pluralize == assoc_name.to_s.pluralize
42
+ return assoc_name
43
+ else
44
+ found_assoc = get_assoc_name(model_name, includes_tree)
45
+ return found_assoc if found_assoc
46
+ end
47
+ end
48
+ elsif haystack.is_a?(Array)
49
+ haystack.each do |element|
50
+ if element.is_a?(String)
51
+ if model_name.pluralize == element.pluralize
52
+ return element
53
+ end
54
+ else
55
+ found_assoc = get_assoc_name(model_name, element)
56
+ return found_assoc if found_assoc
57
+ end
58
+ end
59
+ else
60
+ if model_name.pluralize == haystack.to_s.pluralize
61
+ return haystack
62
+ end
63
+ end
64
+
65
+ return nil
66
+ end
67
+
68
+ def get_tree_for(needle, assoc_name = nil, haystack = main_tree)
69
+ return haystack if needle.to_s.pluralize == assoc_name.to_s.pluralize
70
+
71
+ if haystack.is_a?(Hash)
72
+ haystack.each do |assoc_name, includes_tree|
73
+ found_haystack = get_tree_for(needle, assoc_name, includes_tree)
74
+ return found_haystack if found_haystack.present?
75
+ end
76
+ elsif haystack.is_a?(Array)
77
+ haystack.each do |includes_tree|
78
+ found_haystack = get_tree_for(needle, nil, includes_tree)
79
+ return found_haystack if found_haystack.present?
80
+ end
81
+ elsif haystack.is_a?(String)
82
+ found_haystack = get_tree_for(needle, haystack, nil)
83
+ return found_haystack if found_haystack.present?
84
+ end
85
+
86
+ return []
87
+ end
88
+
89
+ def in_sub(parent_model, child_model)
90
+ tree = get_tree_for(parent_model)
91
+
92
+ if tree.is_a?(Hash)
93
+ return tree.keys.map(&:singularize).include?(child_model)
94
+ elsif tree.is_a?(Array)
95
+ tree.each do |element|
96
+ if element.is_a?(String)
97
+ return true if element.singularize == child_model
98
+ elsif element.is_a?(Hash)
99
+ return true if element.keys.map(&:singularize).include?(child_model)
100
+ end
101
+ end
102
+ elsif tree.is_a?(String)
103
+ return tree.singularize == child_model
104
+ end
105
+
106
+ return false
107
+ end
108
+ end
@@ -44,6 +44,28 @@ class Jason::LuaGenerator
44
44
  end
45
45
  LUA
46
46
 
47
- result = $redis_jason.eval cmd, [], [model_name, id, sub_id, gidx]
47
+ $redis_jason.eval cmd, [], [model_name, id, sub_id, gidx]
48
+ end
49
+
50
+ def update_set_with_diff(key, add_members, remove_members)
51
+ cmd = <<~LUA
52
+ local old_members = redis.call('smembers', KEYS[1])
53
+ local add_size = ARGV[1]
54
+
55
+ for k, m in pairs({unpack(ARGV, 2, add_size + 1)}) do
56
+ redis.call('sadd', KEYS[1], m)
57
+ end
58
+
59
+ for k, m in pairs({unpack(ARGV, add_size + 2, #ARGV)}) do
60
+ redis.call('srem', KEYS[1], m)
61
+ end
62
+
63
+ return old_members
64
+ LUA
65
+
66
+ args = [add_members.size, add_members, remove_members].flatten
67
+
68
+ old_members = $redis_jason.eval cmd, [key], args
69
+ return [old_members, (old_members + add_members - remove_members).uniq]
48
70
  end
49
71
  end
@@ -25,8 +25,15 @@ module Jason::Publisher
25
25
  end
26
26
  end
27
27
 
28
- def publish_json
28
+ def force_publish_json
29
+ # As-if newly created
30
+ publish_json(self.attributes.map { |k,v| [k, [nil, v]] }.to_h)
31
+ end
32
+
33
+ def publish_json(previous_changes = {})
29
34
  payload, gidx = cache_json
35
+
36
+ return if skip_publish_json
30
37
  subs = jason_subscriptions # Get this first, because could be changed
31
38
 
32
39
  # Situations where IDs may need to change and this can't be immediately determined
@@ -37,20 +44,19 @@ module Jason::Publisher
37
44
  # TODO: Optimize this, by caching associations rather than checking each time instance is saved
38
45
  jason_assocs = self.class.reflect_on_all_associations(:belongs_to).select { |assoc| assoc.klass.respond_to?(:has_jason?) }
39
46
  jason_assocs.each do |assoc|
40
- if self.previous_changes[assoc.foreign_key].present?
41
-
47
+ if previous_changes[assoc.foreign_key].present?
42
48
  Jason::Subscription.update_ids(
43
49
  self.class.name.underscore,
44
50
  id,
45
- assoc.klass.name.underscore,
46
- self.previous_changes[assoc.foreign_key][0],
47
- self.previous_changes[assoc.foreign_key][1]
51
+ assoc.name.to_s.singularize,
52
+ previous_changes[assoc.foreign_key][0],
53
+ previous_changes[assoc.foreign_key][1]
48
54
  )
49
55
  elsif (persisted? && @was_a_new_record && send(assoc.foreign_key).present?)
50
56
  Jason::Subscription.update_ids(
51
57
  self.class.name.underscore,
52
58
  id,
53
- assoc.klass.name.underscore,
59
+ assoc.name.to_s.singularize,
54
60
  nil,
55
61
  send(assoc.foreign_key)
56
62
  )
@@ -65,26 +71,20 @@ module Jason::Publisher
65
71
  end
66
72
 
67
73
  # - An instance is created where it belongs_to an _all_ subscription
68
- if self.previous_changes['id'].present?
74
+ if previous_changes['id'].present?
69
75
  Jason::Subscription.add_id(self.class.name.underscore, id)
70
76
  end
71
77
 
72
- return if skip_publish_json
73
-
74
- if self.persisted?
78
+ if persisted?
75
79
  jason_subscriptions.each do |sub_id|
76
80
  Jason::Subscription.new(id: sub_id).update(self.class.name.underscore, id, payload, gidx)
77
81
  end
78
- else
79
- subs.each do |sub_id|
80
- Jason::Subscription.new(id: sub_id).destroy(self.class.name.underscore, id)
81
- end
82
82
  end
83
83
  end
84
84
 
85
85
  def publish_json_if_changed
86
86
  subscribed_fields = api_model.subscribed_fields
87
- publish_json if (self.previous_changes.keys.map(&:to_sym) & subscribed_fields).present? || !self.persisted?
87
+ publish_json(self.previous_changes) if (self.previous_changes.keys.map(&:to_sym) & subscribed_fields).present? || !self.persisted?
88
88
  end
89
89
 
90
90
  def jason_subscriptions