jason-rails 0.7.0 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a0bfff0c4de046d090ba0c1708fcd4692da2cccc2caf0911d6ae752310a7a8ee
4
- data.tar.gz: 8851a0bfdbd521426f3650cbfa44eeea25a4e4bc0bceb8fac3bad7a667563a9c
3
+ metadata.gz: 0e0d37099f467ff5bd4ab868732ce57c53bf055e6ecb8d0f0506bb14ee723d77
4
+ data.tar.gz: 6912d141317dbc95edb96bc11ed29235a90fc6dd8575a4dcba18017b81a9964f
5
5
  SHA512:
6
- metadata.gz: 47cc3c7df9c3a4c9fedef6899a3948ba78d44a6a507f830c1f7a50dd920794654777d5a5a3c98f6431d2ef2edef3c1d104222bc06abccc39937f5d55fcd3d33e
7
- data.tar.gz: b6305008dd05be63e8fa1dbfa785179129ecf4554877a801dbe44b979906997ce44761d2978ffd7a1129ab1ed4ce44e83e4464ab8566c3fccb9a137516dd48e7
6
+ metadata.gz: 3cce0310a94bba9c73237d3d18bcd7aa949fb35905b7dd76c69c05b6d8259f1fc6d0b79da2a01b3db34b372cf438ea96777c14d2bd3bdbfacf9ba27578e81f19
7
+ data.tar.gz: f2460fea5f459d24966741cce29855b97b9361a900a848e8655ccdfb4cbaa03aa4d47135b2fa661961f7c8d46d37f5e9bc821ff97a6f5cdd22265f7a8d755758
data/CHANGELOG.md CHANGED
@@ -1,3 +1,6 @@
1
+ ## v0.7.1
2
+ - Added: Authorization for REST endpoints. Previously these just inherited logic from ApplicationController. Pass a `update_authorization_service` option to the Jason initializer to use this.
3
+
1
4
  ## v0.7.0
2
5
  - Added: New forms of conditional subscription. You can now add conditions on fields other than the primary key.
3
6
  E.g.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- jason-rails (0.6.8)
4
+ jason-rails (0.7.0)
5
5
  connection_pool (>= 2.2.3)
6
6
  jsondiff
7
7
  rails (>= 5)
@@ -89,7 +89,8 @@ GEM
89
89
  marcel (0.3.3)
90
90
  mimemagic (~> 0.3.2)
91
91
  method_source (1.0.0)
92
- mimemagic (0.3.5)
92
+ mimemagic (0.3.8)
93
+ nokogiri (~> 1)
93
94
  mini_mime (1.0.2)
94
95
  mini_portile2 (2.5.0)
95
96
  minitest (5.14.3)
data/README.md CHANGED
@@ -152,13 +152,15 @@ export default function Comment({ id }) {
152
152
 
153
153
  ## Authorization
154
154
 
155
- By default all models can be subscribed to and updated without authentication or authorization. Probably you want to lock down access.
155
+ By default all models can be subscribed to and updated without authentication or authorization. Probably you want to lock down access. At the moment Jason has no opinion on how to handle authorization, it simply forwards parameters to a service that you provide - so the implementation can be as simple or as complex as you need.
156
156
 
157
157
  ### Authorizing subscriptions
158
158
  You can do this by providing an class to Jason in the initializer under the `subscription_authorization_service` key. This must be a class receiving a message `call` with the parameters `user`, `model`, `conditions`, `sub_models` and return true or false for whether the user is allowed to access a subscription with those parameters. You can decide the implementation details of this to be as simple or complex as your app requires.
159
159
 
160
160
  ### Authorizing updates
161
- Similarly to authorizing subscriptions, you can do this by providing an class to Jason in the initializer under the `update_authorization_service` key. This must be a class receiving a message `call` with the parameters `user`, `model`, `instance`, `update`, `remove` and return true or false for whether the user is allowed to access a subscription with those parameters.
161
+ Similarly to authorizing subscriptions, you can do this by providing an class to Jason in the initializer under the `update_authorization_service` key. This must be a class receiving a message `call` with the parameters `user`, `model`, `action`, `instance`, `params` and return true or false for whether the user is allowed to make this update.
162
+
163
+ See the specs for some examples of this.
162
164
 
163
165
  ## Roadmap
164
166
 
@@ -169,9 +171,10 @@ Development is primarily driven by the needs of projects we're using Jason in. I
169
171
  - Utilities for "Draft editing" - both storing client-side copies of model trees which can be committed or discarded, as well as persisting a shadow copy to the database (to allow resumable editing, or possibly collaborative editing features)
170
172
  - Benchmark and migrate if necessary ConnectionPool::Wrapper vs ConnectionPool
171
173
  - Assess using RedisGraph for the graph diffing functionality, to see if this would provide a performance boost
174
+ - Improve the Typescript definitions (ie remove the abundant `any` typing currently used)
172
175
 
173
176
  ## License
174
177
 
175
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
178
+ The gem, npm package and source code in the git repository are available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
176
179
 
177
180
 
@@ -21,14 +21,17 @@ class Jason::JasonController < ::ApplicationController
21
21
  def action
22
22
  type = params[:type]
23
23
  entity = type.split('/')[0].underscore
24
- api_model = Jason::ApiModel.new(entity.singularize)
25
- model = entity.singularize.camelize.constantize
24
+ model_name = entity.singularize
25
+ api_model = Jason::ApiModel.new(model_name)
26
+ model = model_name.camelize.constantize
26
27
  action = type.split('/')[1].underscore
27
28
 
28
29
  if action == 'move_priority'
29
30
  id, priority = params[:payload].values_at(:id, :priority)
30
31
 
31
32
  instance = model.find(id)
33
+
34
+ return head :forbidden if !action_permitted?(model_name, action, instance, params)
32
35
  priority_filter = instance.as_json.with_indifferent_access.slice(*api_model.priority_scope)
33
36
 
34
37
  all_instance_ids = model.send(api_model.scope || :all).where(priority_filter).where.not(id: instance.id).order(:priority).pluck(:id)
@@ -40,10 +43,24 @@ class Jason::JasonController < ::ApplicationController
40
43
 
41
44
  model.find(all_instance_ids).each(&:force_publish_json)
42
45
  elsif action == 'upsert' || action == 'add'
46
+ id = params[:payload][:id]
43
47
  payload = api_model.permit(params)
44
- return render json: model.find_or_create_by_id!(payload).as_json(api_model.as_json_config)
48
+
49
+ instance = model.find_by(id: id)
50
+ return head :forbidden if !action_permitted?(model_name, action, instance, params)
51
+
52
+ if instance.present?
53
+ instance.update!(payload)
54
+ else
55
+ instance = model.create!(payload)
56
+ end
57
+
58
+ return render json: instance.as_json(api_model.as_json_config)
45
59
  elsif action == 'remove'
46
- model.find(params[:payload]).destroy!
60
+ instance = model.find(params[:payload])
61
+ return head :forbidden if !action_permitted?(model_name, action, instance, params)
62
+
63
+ instance.destroy!
47
64
  end
48
65
 
49
66
  return head :ok
@@ -75,4 +92,9 @@ class Jason::JasonController < ::ApplicationController
75
92
  return head :forbidden
76
93
  end
77
94
  end
95
+
96
+ def action_permitted?(model_name, action, instance, params)
97
+ return true if Jason.update_authorization_service.blank?
98
+ Jason.update_authorization_service.call(current_user, model_name, action, instance, params)
99
+ end
78
100
  end
data/client/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jamesr2323/jason",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "module": "./lib/index.js",
5
5
  "types": "./lib/index.d.ts",
6
6
  "scripts": {
data/lib/jason.rb CHANGED
@@ -25,7 +25,8 @@ module Jason
25
25
  self.mattr_accessor :pusher_key
26
26
  self.mattr_accessor :pusher_region
27
27
  self.mattr_accessor :pusher_channel_prefix
28
- self.mattr_accessor :authorization_service
28
+ self.mattr_accessor :subscription_authorization_service
29
+ self.mattr_accessor :update_authorization_service
29
30
  self.mattr_accessor :sidekiq_queue
30
31
 
31
32
  self.schema = {}
@@ -150,35 +150,6 @@ module Jason::Publisher
150
150
  self.after_commit :force_publish_json, on: [:create, :destroy]
151
151
  self.after_commit :publish_json_if_changed, on: [:update]
152
152
  end
153
-
154
- def find_or_create_by_id(params)
155
- object = find_by(id: params[:id])
156
-
157
- if object
158
- object.update(params)
159
- elsif params[:hidden]
160
- return false ## If an object is passed with hidden = true but didn't already exist, it's safe to never create it
161
- else
162
- object = create!(params)
163
- end
164
-
165
- object
166
- end
167
-
168
- def find_or_create_by_id!(params)
169
- object = find_by(id: params[:id])
170
-
171
- if object
172
- object.update!(params)
173
- elsif params[:hidden]
174
- ## TODO: We're diverging from semantics of the Rails bang! methods here, which would normally either raise or return an object. Find a way to make this better.
175
- return false ## If an object is passed with hidden = true but didn't already exist, it's safe to never create it
176
- else
177
- object = create!(params)
178
- end
179
-
180
- object
181
- end
182
153
  end
183
154
 
184
155
  included do
@@ -390,8 +390,8 @@ class Jason::Subscription
390
390
 
391
391
  def user_can_access?(user)
392
392
  # td: implement the authorization logic here
393
- return true if Jason.authorization_service.blank?
394
- Jason.authorization_service.call(user, model, conditions, includes_helper.all_models - [model])
393
+ return true if Jason.subscription_authorization_service.blank?
394
+ Jason.subscription_authorization_service.call(user, model, conditions, includes_helper.all_models - [model])
395
395
  end
396
396
 
397
397
  def get
data/lib/jason/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Jason
2
- VERSION = "0.7.0"
2
+ VERSION = "0.7.1"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jason-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Rees
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-04-02 00:00:00.000000000 Z
11
+ date: 2021-04-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -238,9 +238,7 @@ files:
238
238
  - lib/jason/includes_helper.rb
239
239
  - lib/jason/lua_generator.rb
240
240
  - lib/jason/publisher.rb
241
- - lib/jason/publisher_old.rb
242
241
  - lib/jason/subscription.rb
243
- - lib/jason/subscription_old.rb
244
242
  - lib/jason/version.rb
245
243
  homepage: https://github.com/jamesr2323/jason
246
244
  licenses:
@@ -1,112 +0,0 @@
1
- module Jason::PublisherOld
2
- extend ActiveSupport::Concern
3
-
4
- def cache_json
5
- as_json_config = api_model.as_json_config
6
- scope = api_model.scope
7
-
8
- if self.persisted? && (scope.blank? || self.class.unscoped.send(scope).exists?(self.id))
9
- payload = self.reload.as_json(as_json_config)
10
- $redis_jason.hset("jason:#{self.class.name.underscore}:cache", self.id, payload.to_json)
11
- else
12
- $redis_jason.hdel("jason:#{self.class.name.underscore}:cache", self.id)
13
- end
14
- end
15
-
16
- def publish_json
17
- cache_json
18
- return if skip_publish_json
19
-
20
- self.class.jason_subscriptions.each do |id, config_json|
21
- config = JSON.parse(config_json)
22
-
23
- if (config['conditions'] || {}).all? { |field, value| self.send(field) == value }
24
- Jason::Subscription.new(id: id).update(self.class.name.underscore)
25
- end
26
- end
27
- end
28
-
29
- def publish_json_if_changed
30
- subscribed_fields = api_model.subscribed_fields
31
- publish_json if (self.previous_changes.keys.map(&:to_sym) & subscribed_fields).present? || !self.persisted?
32
- end
33
-
34
- class_methods do
35
- def subscriptions
36
- $redis_jason.hgetall("jason:#{self.name.underscore}:subscriptions")
37
- end
38
-
39
- def jason_subscriptions
40
- $redis_jason.hgetall("jason:#{self.name.underscore}:subscriptions")
41
- end
42
-
43
- def publish_all(instances)
44
- instances.each(&:cache_json)
45
-
46
- subscriptions.each do |id, config_json|
47
- Jason::Subscription.new(id: id).update(self.name.underscore)
48
- end
49
- end
50
-
51
- def flush_cache
52
- $redis_jason.del("jason:#{self.name.underscore}:cache")
53
- end
54
-
55
- def setup_json
56
- self.after_initialize -> {
57
- @api_model = Jason::ApiModel.new(self.class.name.underscore)
58
- }
59
- self.after_commit :publish_json_if_changed
60
-
61
- include_models = Jason::ApiModel.new(self.name.underscore).include_models
62
-
63
- include_models.map do |assoc|
64
- puts assoc
65
- reflection = self.reflect_on_association(assoc.to_sym)
66
- reflection.klass.after_commit -> {
67
- subscribed_fields = Jason::ApiModel.new(self.class.name.underscore).subscribed_fields
68
- puts subscribed_fields.inspect
69
-
70
- if (self.previous_changes.keys.map(&:to_sym) & subscribed_fields).present?
71
- self.send(reflection.inverse_of.name)&.publish_json
72
- end
73
- }
74
- end
75
- end
76
-
77
- def find_or_create_by_id(params)
78
- object = find_by(id: params[:id])
79
-
80
- if object
81
- object.update(params)
82
- elsif params[:hidden]
83
- return false ## If an object is passed with hidden = true but didn't already exist, it's safe to never create it
84
- else
85
- object = create!(params)
86
- end
87
-
88
- object
89
- end
90
-
91
- def find_or_create_by_id!(params)
92
- object = find_by(id: params[:id])
93
-
94
- if object
95
- object.update!(params)
96
- elsif params[:hidden]
97
- ## TODO: We're diverging from semantics of the Rails bang! methods here, which would normally either raise or return an object. Find a way to make this better.
98
- return false ## If an object is passed with hidden = true but didn't already exist, it's safe to never create it
99
- else
100
- object = create!(params)
101
- end
102
-
103
- object
104
- end
105
- end
106
-
107
- included do
108
- attr_accessor :skip_publish_json, :api_model
109
-
110
- setup_json
111
- end
112
- end
@@ -1,171 +0,0 @@
1
- class Jason::SubscriptionOld
2
- attr_accessor :id, :config
3
-
4
- def initialize(id: nil, config: nil)
5
- if id
6
- @id = id
7
- raw_config = $redis_jason.hgetall("jason:subscriptions:#{id}").map { |k,v| [k, JSON.parse(v)] }.to_h
8
- set_config(raw_config)
9
- else
10
- @id = Digest::MD5.hexdigest(config.to_json)
11
- configure(config)
12
- end
13
- end
14
-
15
- def set_config(raw_config)
16
- @config = raw_config.with_indifferent_access.map { |k,v| [k.underscore.to_s, v] }.to_h
17
- end
18
-
19
- def configure(raw_config)
20
- set_config(raw_config)
21
- $redis_jason.hmset("jason:subscriptions:#{id}", *config.map { |k,v| [k, v.to_json]}.flatten)
22
- end
23
-
24
- def destroy
25
- config.each do |model, value|
26
- $redis_jason.srem("jason:#{model.to_s.underscore}:subscriptions", id)
27
- end
28
- $redis_jason.del("jason:subscriptions:#{id}")
29
- end
30
-
31
- def add_consumer(consumer_id)
32
- before_consumer_count = consumer_count
33
- $redis_jason.sadd("jason:subscriptions:#{id}:consumers", consumer_id)
34
- $redis_jason.hset("jason:consumers", consumer_id, Time.now.utc)
35
-
36
- add_subscriptions
37
- publish_all
38
- end
39
-
40
- def remove_consumer(consumer_id)
41
- $redis_jason.srem("jason:subscriptions:#{id}:consumers", consumer_id)
42
- $redis_jason.hdel("jason:consumers", consumer_id)
43
-
44
- if consumer_count == 0
45
- remove_subscriptions
46
- end
47
- end
48
-
49
- def consumer_count
50
- $redis_jason.scard("jason:subscriptions:#{id}:consumers")
51
- end
52
-
53
- def channel
54
- "jason:#{id}"
55
- end
56
-
57
- def publish_all
58
- config.each do |model, model_config|
59
- klass = model.to_s.classify.constantize
60
- conditions = model_config['conditions'] || {}
61
- klass.where(conditions).find_each(&:cache_json)
62
- update(model)
63
- end
64
- end
65
-
66
- def add_subscriptions
67
- config.each do |model, value|
68
- $redis_jason.hset("jason:#{model.to_s.underscore}:subscriptions", id, value.to_json)
69
- update(model)
70
- end
71
- end
72
-
73
- def remove_subscriptions
74
- config.each do |model, _|
75
- $redis_jason.hdel("jason:#{model.to_s.underscore}:subscriptions", id)
76
- end
77
- end
78
-
79
- def self.publish_all
80
- JASON_API_MODEL.each do |model, _v|
81
- klass = model.to_s.classify.constantize
82
- klass.publish_all(klass.all) if klass.respond_to?(:publish_all)
83
- end
84
- end
85
-
86
- def get(model_name)
87
- LuaGenerator.new.index_hash_by_set("jason:cache:#{model_name}", "")
88
-
89
- value = JSON.parse($redis_jason.get("#{channel}:#{model}:value") || '[]')
90
- idx = $redis_jason.get("#{channel}:#{model}:idx").to_i
91
-
92
- {
93
- type: 'payload',
94
- md5Hash: id,
95
- model: model,
96
- value: value,
97
- idx: idx
98
- }
99
- end
100
-
101
- def get_diff(old_value, value)
102
- JsonDiff.generate(old_value, value)
103
- end
104
-
105
- def deep_stringify(value)
106
- if value.is_a?(Hash)
107
- value.deep_stringify_keys
108
- elsif value.is_a?(Array)
109
- value.map { |x| x.deep_stringify_keys }
110
- end
111
- end
112
-
113
- def get_throttle
114
- if !$throttle_rate || !$throttle_timeout || Time.now.utc > $throttle_timeout
115
- $throttle_timeout = Time.now.utc + 5.seconds
116
- $throttle_rate = ($redis_jason.get('global_throttle_rate') || 0).to_i
117
- else
118
- $throttle_rate
119
- end
120
- end
121
-
122
- # Atomically update and return patch
123
- def update(model)
124
- start_time = Time.now.utc
125
- conditions = config[model]['conditions']
126
-
127
- value = $redis_jason.hgetall("jason:#{model}:cache")
128
- .values.map { |v| JSON.parse(v) }
129
- .select { |v| (conditions || {}).all? { |field, value| v[field] == value } }
130
- .sort_by { |v| v['id'] }
131
-
132
- # lfsa = last finished, started at
133
- # If another job that started after this one, finished before this one, skip sending this state update
134
- if Time.parse($redis_jason.get("jason:#{channel}:lfsa") || '1970-01-01 00:00:00 UTC') < start_time
135
- $redis_jason.set("jason:#{channel}:lfsa", start_time)
136
- else
137
- return
138
- end
139
-
140
- value = deep_stringify(value)
141
-
142
- # If value has changed, return old value and new idx. Otherwise do nothing.
143
- cmd = <<~LUA
144
- local old_val=redis.call('get', ARGV[1] .. ':value')
145
- if old_val ~= ARGV[2] then
146
- redis.call('set', ARGV[1] .. ':value', ARGV[2])
147
- local new_idx = redis.call('incr', ARGV[1] .. ':idx')
148
- return { new_idx, old_val }
149
- end
150
- LUA
151
-
152
- result = $redis_jason.eval cmd, [], ["#{channel}:#{model}", value.to_json]
153
- return if result.blank?
154
-
155
- idx = result[0]
156
- old_value = JSON.parse(result[1] || '[]')
157
- diff = get_diff(old_value, value)
158
-
159
- end_time = Time.now.utc
160
-
161
- payload = {
162
- model: model,
163
- md5Hash: id,
164
- diff: diff,
165
- idx: idx.to_i,
166
- latency: ((end_time - start_time)*1000).round
167
- }
168
-
169
- ActionCable.server.broadcast("jason:#{id}", payload)
170
- end
171
- end