jason-rails 0.3.0 → 0.6.0

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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -1
  3. data/.ruby-version +1 -0
  4. data/Gemfile.lock +184 -0
  5. data/README.md +118 -10
  6. data/app/controllers/jason/api/pusher_controller.rb +15 -0
  7. data/app/controllers/jason/api_controller.rb +78 -0
  8. data/client/babel.config.js +13 -0
  9. data/client/lib/JasonContext.d.ts +6 -1
  10. data/client/lib/JasonProvider.d.ts +6 -5
  11. data/client/lib/JasonProvider.js +5 -97
  12. data/client/lib/actionFactory.js +1 -1
  13. data/client/lib/createActions.d.ts +1 -1
  14. data/client/lib/createActions.js +2 -27
  15. data/client/lib/createJasonReducers.js +49 -3
  16. data/client/lib/createOptDis.d.ts +1 -0
  17. data/client/lib/createOptDis.js +43 -0
  18. data/client/lib/createPayloadHandler.d.ts +9 -1
  19. data/client/lib/createPayloadHandler.js +52 -43
  20. data/client/lib/createServerActionQueue.d.ts +10 -0
  21. data/client/lib/createServerActionQueue.js +48 -0
  22. data/client/lib/createServerActionQueue.test.d.ts +1 -0
  23. data/client/lib/createServerActionQueue.test.js +37 -0
  24. data/client/lib/createTransportAdapter.d.ts +5 -0
  25. data/client/lib/createTransportAdapter.js +20 -0
  26. data/client/lib/deepCamelizeKeys.d.ts +1 -0
  27. data/client/lib/deepCamelizeKeys.js +23 -0
  28. data/client/lib/deepCamelizeKeys.test.d.ts +1 -0
  29. data/client/lib/deepCamelizeKeys.test.js +106 -0
  30. data/client/lib/index.d.ts +6 -5
  31. data/client/lib/pruneIdsMiddleware.d.ts +2 -0
  32. data/client/lib/pruneIdsMiddleware.js +24 -0
  33. data/client/lib/restClient.d.ts +2 -0
  34. data/client/lib/restClient.js +17 -0
  35. data/client/lib/transportAdapters/actionCableAdapter.d.ts +5 -0
  36. data/client/lib/transportAdapters/actionCableAdapter.js +35 -0
  37. data/client/lib/transportAdapters/pusherAdapter.d.ts +5 -0
  38. data/client/lib/transportAdapters/pusherAdapter.js +68 -0
  39. data/client/lib/useJason.d.ts +5 -0
  40. data/client/lib/useJason.js +94 -0
  41. data/client/lib/useJason.test.d.ts +1 -0
  42. data/client/lib/useJason.test.js +85 -0
  43. data/client/lib/useSub.d.ts +1 -1
  44. data/client/lib/useSub.js +6 -3
  45. data/client/package.json +19 -4
  46. data/client/src/JasonProvider.tsx +6 -96
  47. data/client/src/actionFactory.ts +1 -1
  48. data/client/src/createActions.ts +2 -33
  49. data/client/src/createJasonReducers.ts +57 -3
  50. data/client/src/createOptDis.ts +45 -0
  51. data/client/src/createPayloadHandler.ts +58 -47
  52. data/client/src/createServerActionQueue.test.ts +42 -0
  53. data/client/src/createServerActionQueue.ts +47 -0
  54. data/client/src/createTransportAdapter.ts +13 -0
  55. data/client/src/deepCamelizeKeys.test.ts +113 -0
  56. data/client/src/deepCamelizeKeys.ts +17 -0
  57. data/client/src/pruneIdsMiddleware.ts +24 -0
  58. data/client/src/restClient.ts +14 -0
  59. data/client/src/transportAdapters/actionCableAdapter.ts +38 -0
  60. data/client/src/transportAdapters/pusherAdapter.ts +72 -0
  61. data/client/src/useJason.test.ts +87 -0
  62. data/client/src/useJason.ts +110 -0
  63. data/client/src/useSub.ts +6 -3
  64. data/client/yarn.lock +4607 -81
  65. data/config/routes.rb +8 -0
  66. data/jason-rails.gemspec +9 -0
  67. data/lib/jason.rb +40 -1
  68. data/lib/jason/api_model.rb +15 -9
  69. data/lib/jason/broadcaster.rb +19 -0
  70. data/lib/jason/channel.rb +50 -21
  71. data/lib/jason/engine.rb +5 -0
  72. data/lib/jason/graph_helper.rb +165 -0
  73. data/lib/jason/includes_helper.rb +108 -0
  74. data/lib/jason/lua_generator.rb +71 -0
  75. data/lib/jason/publisher.rb +103 -30
  76. data/lib/jason/publisher_old.rb +112 -0
  77. data/lib/jason/subscription.rb +352 -101
  78. data/lib/jason/subscription_old.rb +171 -0
  79. data/lib/jason/version.rb +1 -1
  80. metadata +151 -4
data/config/routes.rb ADDED
@@ -0,0 +1,8 @@
1
+ Jason::Engine.routes.draw do
2
+ get '/api/config', to: 'api#configuration'
3
+ post '/api/action', to: 'api#action'
4
+ post '/api/create_subscription', to: 'api#create_subscription'
5
+ post '/api/remove_subscription', to: 'api#remove_subscription'
6
+ post '/api/get_payload', to: 'api#get_payload'
7
+ post '/api/pusher/auth', to: 'api/pusher#auth'
8
+ end
data/jason-rails.gemspec CHANGED
@@ -22,4 +22,13 @@ Gem::Specification.new do |spec|
22
22
  end
23
23
  spec.executables = []
24
24
  spec.require_paths = ["lib"]
25
+
26
+ spec.add_dependency "rails", ">= 5"
27
+ spec.add_dependency "connection_pool", ">= 2.2.3"
28
+ spec.add_dependency "redis", ">= 4"
29
+ spec.add_dependency "jsondiff"
30
+
31
+ spec.add_development_dependency "rspec-rails"
32
+ spec.add_development_dependency "sqlite3"
33
+ spec.add_development_dependency "pry"
25
34
  end
data/lib/jason.rb CHANGED
@@ -1,10 +1,49 @@
1
+ require 'connection_pool'
2
+ require 'redis'
3
+ require 'jsondiff'
4
+
1
5
  require "jason/version"
2
6
  require 'jason/api_model'
3
7
  require 'jason/channel'
4
8
  require 'jason/publisher'
5
9
  require 'jason/subscription'
10
+ require 'jason/broadcaster'
11
+ require 'jason/engine'
12
+ require 'jason/lua_generator'
13
+ require 'jason/includes_helper'
14
+ require 'jason/graph_helper'
6
15
 
7
16
  module Jason
8
17
  class Error < StandardError; end
9
- # Your code goes here...
18
+
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
27
+
28
+ self.schema = {}
29
+ self.transport_service = :action_cable
30
+ self.pusher_region = 'eu'
31
+ self.pusher_channel_prefix = 'jason'
32
+
33
+ # add default values of more config vars here
34
+
35
+ # this function maps the vars from your app into your engine
36
+ def self.setup(&block)
37
+ yield self
38
+ end
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
10
49
  end
@@ -8,15 +8,15 @@ class Jason::ApiModel
8
8
 
9
9
  def initialize(name)
10
10
  @name = name
11
- @model = OpenStruct.new(JASON_API_MODEL[name.to_sym])
11
+ @model = OpenStruct.new(Jason.schema[name.to_sym])
12
12
  end
13
13
 
14
14
  def allowed_params
15
15
  model.allowed_params || []
16
16
  end
17
17
 
18
- def include_models
19
- model.include_models || []
18
+ def allowed_object_params
19
+ model.allowed_object_params || []
20
20
  end
21
21
 
22
22
  def include_methods
@@ -35,13 +35,19 @@ class Jason::ApiModel
35
35
  model.scope
36
36
  end
37
37
 
38
- def as_json_config
39
- include_configs = include_models.map do |assoc|
40
- reflection = name.classify.constantize.reflect_on_association(assoc.to_sym)
41
- api_model = Jason::ApiModel.new(reflection.klass.name.underscore)
42
- { assoc => { only: api_model.subscribed_fields, methods: api_model.include_methods } }
38
+ def permit(params)
39
+ pp self
40
+ pp params
41
+ params = params.require(:payload).permit(allowed_params).tap do |allowed|
42
+ pp "ALLOWED"
43
+ pp allowed
44
+ allowed_object_params.each do |key|
45
+ allowed[key] = params[:payload][key].to_unsafe_h if params[:payload][key]
46
+ end
43
47
  end
48
+ end
44
49
 
45
- { only: subscribed_fields, include: include_configs, methods: include_methods }
50
+ def as_json_config
51
+ { only: subscribed_fields, methods: include_methods }
46
52
  end
47
53
  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
@@ -1,32 +1,27 @@
1
1
  class Jason::Channel < ActionCable::Channel::Base
2
2
  attr_accessor :subscriptions
3
3
 
4
+ def subscribe
5
+ stream_from 'jason'
6
+ end
7
+
4
8
  def receive(message)
5
- subscriptions ||= []
9
+ handle_message(message)
10
+ end
11
+
12
+ private
13
+
14
+ def handle_message(message)
15
+ pp message['createSubscription']
16
+ @subscriptions ||= []
6
17
 
7
18
  begin # ActionCable swallows errors in this message - ensure they're output to logs.
8
19
  if (config = message['createSubscription'])
9
- subscription = Jason::Subscription.new(config: config)
10
- subscriptions.push(subscription)
11
- subscription.add_consumer(identifier)
12
- config.keys.each do |model|
13
- transmit(subscription.get(model.to_s.underscore))
14
- end
15
- stream_from subscription.channel
20
+ create_subscription(config['model'], config['conditions'], config['includes'])
16
21
  elsif (config = message['removeSubscription'])
17
- subscription = Jason::Subscription.new(config: config)
18
- subscriptions.reject! { |s| s.id == subscription.id }
19
- subscription.remove_consumer(identifier)
20
-
21
- # Rails for some reason removed stop_stream_from, so we need to stop all and then restart the other streams
22
- # stop_all_streams
23
- # subscriptions.each do |s|
24
- # stream_from s.channel
25
- # end
26
- elsif (data = message['getPayload'])
27
- config = data.config
28
- model = data.model
29
- Jason::Subscription.new(config: config).get(model.to_s.underscore)
22
+ remove_subscription(config)
23
+ elsif (config = message['getPayload'])
24
+ get_payload(config, message['forceRefresh'])
30
25
  end
31
26
  rescue => e
32
27
  puts e.message
@@ -34,4 +29,38 @@ class Jason::Channel < ActionCable::Channel::Base
34
29
  raise e
35
30
  end
36
31
  end
32
+
33
+ def create_subscription(model, conditions, includes)
34
+ subscription = Jason::Subscription.upsert_by_config(model, conditions: conditions || {}, includes: includes || nil)
35
+
36
+ return if !subscription.user_can_access?(current_user)
37
+ stream_from subscription.channel
38
+
39
+ subscriptions.push(subscription)
40
+ subscription.add_consumer(identifier)
41
+ subscription.get.each do |payload|
42
+ pp payload
43
+ transmit(payload) if payload.present?
44
+ end
45
+ end
46
+
47
+ def remove_subscription(config)
48
+ subscription = Jason::Subscription.upsert_by_config(config['model'], conditions: config['conditions'], includes: config['includes'])
49
+ subscriptions.reject! { |s| s.id == subscription.id }
50
+ subscription.remove_consumer(identifier)
51
+
52
+ # TODO Stop streams
53
+ end
54
+
55
+ def get_payload(config, force_refresh = false)
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)
59
+ if force_refresh
60
+ subscription.set_ids_for_sub_models
61
+ end
62
+ subscription.get.each do |model_name, payload|
63
+ transmit(payload) if payload.present?
64
+ end
65
+ end
37
66
  end
@@ -0,0 +1,5 @@
1
+ module Jason
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Jason
4
+ end
5
+ 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