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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2883964fb830dbb506a0e845caa3398436d72262f72cc6b295efce14dcc96346
4
- data.tar.gz: 631d00d04195b1e1f48111ca5a6cc02775488e48f94ab5d8d29783a66a6c91a8
3
+ metadata.gz: 453e1a0d649445d973de2e3adf27f88f91c5bf983e65b3c2d993f090816db72b
4
+ data.tar.gz: ec2600988c60bcb6f661b8835cd3bcb7d3a4b740749ab0f21109e9b6a513b4ca
5
5
  SHA512:
6
- metadata.gz: 6de81fcdba79fc067fe7515ad83be895e30d19686e548bfb774026993ab965cf6f0aaa9cebcd4807448e5218c8c9609f414e6ec05d5eb717aef880e3f2e012ea
7
- data.tar.gz: adb6476af69fe904588f55214b930959a0b0d1d2357c3edafca77cc4f8d528a72cb0f72cde6dbec4e097e58d6e5758a2c429419cd9ad60f02d0352dd9abe385b
6
+ metadata.gz: cc2ba8529669a1fe4c48d2ee2ba595a7e3ae907a0d10234f12d68d797de63177143187c5c3767cf32ab35bd053c3c9f3bfa14461f8b6be71d8df437e3e997c7d
7
+ data.tar.gz: c93e0002f3a7e2379ed29483b6a8323b4b492a319ccc4f9948f0638c47e62a9eeb8b416fad2bbe2cc6b5c13f11a103eab462b7a35f5a36145037981e49c99cc6
data/.gitignore CHANGED
@@ -11,4 +11,7 @@
11
11
  .rspec_status
12
12
  client/node_modules/
13
13
 
14
- client/yarn-error.log
14
+ client/yarn-error.log
15
+ *.log
16
+ *.gem
17
+ .DS_Store
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.7.2
data/Gemfile.lock ADDED
@@ -0,0 +1,184 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ jason-rails (0.5.1)
5
+ connection_pool (>= 2.2.3)
6
+ jsondiff
7
+ rails (>= 5)
8
+ redis (>= 4)
9
+
10
+ GEM
11
+ remote: https://rubygems.org/
12
+ specs:
13
+ actioncable (6.1.1)
14
+ actionpack (= 6.1.1)
15
+ activesupport (= 6.1.1)
16
+ nio4r (~> 2.0)
17
+ websocket-driver (>= 0.6.1)
18
+ actionmailbox (6.1.1)
19
+ actionpack (= 6.1.1)
20
+ activejob (= 6.1.1)
21
+ activerecord (= 6.1.1)
22
+ activestorage (= 6.1.1)
23
+ activesupport (= 6.1.1)
24
+ mail (>= 2.7.1)
25
+ actionmailer (6.1.1)
26
+ actionpack (= 6.1.1)
27
+ actionview (= 6.1.1)
28
+ activejob (= 6.1.1)
29
+ activesupport (= 6.1.1)
30
+ mail (~> 2.5, >= 2.5.4)
31
+ rails-dom-testing (~> 2.0)
32
+ actionpack (6.1.1)
33
+ actionview (= 6.1.1)
34
+ activesupport (= 6.1.1)
35
+ rack (~> 2.0, >= 2.0.9)
36
+ rack-test (>= 0.6.3)
37
+ rails-dom-testing (~> 2.0)
38
+ rails-html-sanitizer (~> 1.0, >= 1.2.0)
39
+ actiontext (6.1.1)
40
+ actionpack (= 6.1.1)
41
+ activerecord (= 6.1.1)
42
+ activestorage (= 6.1.1)
43
+ activesupport (= 6.1.1)
44
+ nokogiri (>= 1.8.5)
45
+ actionview (6.1.1)
46
+ activesupport (= 6.1.1)
47
+ builder (~> 3.1)
48
+ erubi (~> 1.4)
49
+ rails-dom-testing (~> 2.0)
50
+ rails-html-sanitizer (~> 1.1, >= 1.2.0)
51
+ activejob (6.1.1)
52
+ activesupport (= 6.1.1)
53
+ globalid (>= 0.3.6)
54
+ activemodel (6.1.1)
55
+ activesupport (= 6.1.1)
56
+ activerecord (6.1.1)
57
+ activemodel (= 6.1.1)
58
+ activesupport (= 6.1.1)
59
+ activestorage (6.1.1)
60
+ actionpack (= 6.1.1)
61
+ activejob (= 6.1.1)
62
+ activerecord (= 6.1.1)
63
+ activesupport (= 6.1.1)
64
+ marcel (~> 0.3.1)
65
+ mimemagic (~> 0.3.2)
66
+ activesupport (6.1.1)
67
+ concurrent-ruby (~> 1.0, >= 1.0.2)
68
+ i18n (>= 1.6, < 2)
69
+ minitest (>= 5.1)
70
+ tzinfo (~> 2.0)
71
+ zeitwerk (~> 2.3)
72
+ builder (3.2.4)
73
+ coderay (1.1.3)
74
+ concurrent-ruby (1.1.7)
75
+ connection_pool (2.2.3)
76
+ crass (1.0.6)
77
+ diff-lcs (1.4.4)
78
+ erubi (1.10.0)
79
+ globalid (0.4.2)
80
+ activesupport (>= 4.2.0)
81
+ i18n (1.8.7)
82
+ concurrent-ruby (~> 1.0)
83
+ jsondiff (0.0.5)
84
+ loofah (2.8.0)
85
+ crass (~> 1.0.2)
86
+ nokogiri (>= 1.5.9)
87
+ mail (2.7.1)
88
+ mini_mime (>= 0.1.1)
89
+ marcel (0.3.3)
90
+ mimemagic (~> 0.3.2)
91
+ method_source (1.0.0)
92
+ mimemagic (0.3.5)
93
+ mini_mime (1.0.2)
94
+ mini_portile2 (2.5.0)
95
+ minitest (5.14.3)
96
+ nio4r (2.5.4)
97
+ nokogiri (1.11.1)
98
+ mini_portile2 (~> 2.5.0)
99
+ racc (~> 1.4)
100
+ pry (0.13.1)
101
+ coderay (~> 1.1)
102
+ method_source (~> 1.0)
103
+ racc (1.5.2)
104
+ rack (2.2.3)
105
+ rack-test (1.1.0)
106
+ rack (>= 1.0, < 3)
107
+ rails (6.1.1)
108
+ actioncable (= 6.1.1)
109
+ actionmailbox (= 6.1.1)
110
+ actionmailer (= 6.1.1)
111
+ actionpack (= 6.1.1)
112
+ actiontext (= 6.1.1)
113
+ actionview (= 6.1.1)
114
+ activejob (= 6.1.1)
115
+ activemodel (= 6.1.1)
116
+ activerecord (= 6.1.1)
117
+ activestorage (= 6.1.1)
118
+ activesupport (= 6.1.1)
119
+ bundler (>= 1.15.0)
120
+ railties (= 6.1.1)
121
+ sprockets-rails (>= 2.0.0)
122
+ rails-dom-testing (2.0.3)
123
+ activesupport (>= 4.2.0)
124
+ nokogiri (>= 1.6)
125
+ rails-html-sanitizer (1.3.0)
126
+ loofah (~> 2.3)
127
+ railties (6.1.1)
128
+ actionpack (= 6.1.1)
129
+ activesupport (= 6.1.1)
130
+ method_source
131
+ rake (>= 0.8.7)
132
+ thor (~> 1.0)
133
+ rake (12.3.3)
134
+ redis (4.2.5)
135
+ rspec (3.10.0)
136
+ rspec-core (~> 3.10.0)
137
+ rspec-expectations (~> 3.10.0)
138
+ rspec-mocks (~> 3.10.0)
139
+ rspec-core (3.10.0)
140
+ rspec-support (~> 3.10.0)
141
+ rspec-expectations (3.10.0)
142
+ diff-lcs (>= 1.2.0, < 2.0)
143
+ rspec-support (~> 3.10.0)
144
+ rspec-mocks (3.10.0)
145
+ diff-lcs (>= 1.2.0, < 2.0)
146
+ rspec-support (~> 3.10.0)
147
+ rspec-rails (4.0.2)
148
+ actionpack (>= 4.2)
149
+ activesupport (>= 4.2)
150
+ railties (>= 4.2)
151
+ rspec-core (~> 3.10)
152
+ rspec-expectations (~> 3.10)
153
+ rspec-mocks (~> 3.10)
154
+ rspec-support (~> 3.10)
155
+ rspec-support (3.10.0)
156
+ sprockets (4.0.2)
157
+ concurrent-ruby (~> 1.0)
158
+ rack (> 1, < 3)
159
+ sprockets-rails (3.2.2)
160
+ actionpack (>= 4.0)
161
+ activesupport (>= 4.0)
162
+ sprockets (>= 3.0.0)
163
+ sqlite3 (1.4.2)
164
+ thor (1.0.1)
165
+ tzinfo (2.0.4)
166
+ concurrent-ruby (~> 1.0)
167
+ websocket-driver (0.7.3)
168
+ websocket-extensions (>= 0.1.0)
169
+ websocket-extensions (0.1.5)
170
+ zeitwerk (2.4.2)
171
+
172
+ PLATFORMS
173
+ ruby
174
+
175
+ DEPENDENCIES
176
+ jason-rails!
177
+ pry
178
+ rake (~> 12.0)
179
+ rspec (~> 3.0)
180
+ rspec-rails
181
+ sqlite3
182
+
183
+ BUNDLED WITH
184
+ 2.1.4
data/README.md CHANGED
@@ -1,10 +1,12 @@
1
1
  # Jason
2
2
 
3
+ Jason is still in a highly experimental phase with a rapidly changing API. Production use not recommended - but please give it a try!
4
+
3
5
  ## The goal
4
6
 
5
7
  I wanted:
6
8
  - Automatic updates to client state based on database state
7
- - Automatic persistence to database
9
+ - Persistence to the database without many layers of passing parameters
8
10
  - Redux for awesome state management
9
11
  - Optimistic updates
10
12
 
@@ -14,35 +16,141 @@ Jason attempts to minimize this repitition by auto-generating API endpoints, red
14
16
 
15
17
  ## Installation
16
18
 
17
- Add this line to your application's Gemfile:
19
+ Add the gem and the NPM package
18
20
 
19
21
  ```ruby
20
22
  gem 'jason-rails'
21
23
  ```
22
24
 
23
- And then execute:
25
+ ```bash
26
+ yarn add @jamesr2323/jason
27
+ ```
28
+
29
+ You will also need have peer dependencies of `redux`, `react-redux` and `@reduxjs/toolkit`.
30
+
31
+ ### In Rails
32
+
33
+ Include the module `Jason::Publisher` in all models you want to publish via Jason.
34
+
35
+ Create a new initializer e.g. `jason.rb` which defines your schema
36
+
37
+ ```ruby
38
+ Jason.setup do |config|
39
+ config.schema = {
40
+ post: {
41
+ subscribed_fields: [:id, :name]
42
+ },
43
+ comment: {
44
+ subscribed_fields: [:id]
45
+ },
46
+ user: {
47
+ subscribed_fields: [:id]
48
+ }
49
+ }
50
+ end
51
+ ```
24
52
 
25
- $ bundle install
53
+ ### In your frontend code
26
54
 
27
- Or install it yourself as:
55
+ First you need to wrap your root component in a `JasonProvider`.
28
56
 
29
- $ gem install jason
57
+ ```jsx
58
+ import { JasonProvider } from '@jamesr2323/jason'
59
+
60
+ return <JasonProvider>
61
+ <YourApp />
62
+ </JasonProvider>
63
+ ```
64
+
65
+ This is a wrapper around `react-redux` Provider component. This accepts the following props (all optional):
66
+
67
+ - `reducers` - An object of reducers that will be included in `configureStore`. Make sure these do not conflict with the names of any of the models you are configuring for use with Jason
68
+ - `extraActions` - Extra actions you want to be available via the `useAct` hook. (See below)
69
+ This must be a function which returns an object which will be merged with the main Jason actions. The function will be passed a dispatch function, store, axios instance and the Jason actions. For example you can add actions for one of your custom slices:
70
+
71
+ ```js
72
+ function extraActions(dispatch, store, restClient, act) {
73
+ return {
74
+ local: {
75
+ upsert: payload => dis({ type: 'local/upsert', payload })
76
+ }
77
+ }
78
+ }
79
+ ```
80
+
81
+ - `middleware` - Passed directly to `configureStore` with additional Jason middleware
30
82
 
31
83
  ## Usage
84
+ Jason provides two custom hooks to access functionality.
85
+
86
+ ### useAct
87
+ This returns an object which allows you to access actions which both update models on the server, and perform an optimistic update to the Redux store.
32
88
 
33
- TODO: Write usage instructions here
89
+ Example
90
+ ```jsx
91
+ import React, { useState } from 'react'
92
+ import { useAct } from '@jamesr2323/jason'
93
+
94
+ export default function PostCreator() {
95
+ const act = useAct()
96
+ const [name, setName] = useState('')
97
+
98
+ function handleClick() {
99
+ act.posts.add({ name })
100
+ }
101
+
102
+ return <div>
103
+ <input value={name} onChange={e => setName(e.target.value)} />
104
+ <button onClick={handleClick}>Add</button>
105
+ </div>
106
+ }
107
+ ```
108
+
109
+ ### useSub
110
+ This subscribes your Redux store to a model or set of models. It will automatically unsubscribe when the component unmounts.
111
+
112
+ Example
113
+ ```jsx
114
+ import React from 'react'
115
+ import { useSelector } from 'react-redux'
116
+ import { useSub } from '@jamesr2323/jason'
117
+ import _ from 'lodash'
118
+
119
+ export default function PostsList() {
120
+ useSub({ model: 'post', includes: ['comments'] })
121
+ const posts = useSelector(s => _.values(s.posts.entities))
122
+
123
+ return <div>
124
+ { posts.map(({ id, name }) => <div key={id}>{ name }</div>) }
125
+ </div>
126
+ }
127
+ ```
128
+
129
+ ## Authorization
130
+
131
+ By default all models can be subscribed to and updated without authentication or authorization. Probably you want to lock down access.
132
+
133
+ ### Authorizing subscriptions
134
+ 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.
135
+
136
+ ### Authorizing updates
137
+ 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.
138
+
139
+ ## Roadmap
140
+
141
+ Development is primarily driven by the needs of projects we're using Jason in. In no particular order, being considered is:
142
+ - Failure handling - rolling back local state in case of an error on the server
143
+ - Authorization - more thorough authorization integration, with utility functions for common authorizations. Allowing authorization of access to particular fields such as restricting the fields of a user that are publicly broadcast.
144
+ - 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)
34
145
 
35
146
  ## Development
36
147
 
37
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
38
148
 
39
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
40
149
 
41
150
  ## Contributing
42
151
 
43
152
  Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/jason. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/jason/blob/master/CODE_OF_CONDUCT.md).
44
153
 
45
-
46
154
  ## License
47
155
 
48
156
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,15 @@
1
+ class Jason::Api::PusherController < ApplicationController
2
+ skip_before_action :verify_authenticity_token
3
+
4
+ def auth
5
+ channel_main_name = params[:channel_name].remove("private-#{Jason.pusher_channel_prefix}-")
6
+ subscription_id = channel_main_name.remove('jason-')
7
+
8
+ if Jason::Subscription.find_by_id(subscription_id).user_can_access?(current_user)
9
+ response = Pusher.authenticate(params[:channel_name], params[:socket_id])
10
+ return render json: response
11
+ else
12
+ return head :forbidden
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,78 @@
1
+ class Jason::ApiController < ::ApplicationController
2
+ before_action :load_and_authorize_subscription, only: [:create_subscription, :remove_subscription, :get_payload]
3
+ # config seems to be a reserved name, resulting in infinite loop
4
+ def configuration
5
+ payload = {
6
+ schema: Jason.schema,
7
+ transportService: Jason.transport_service,
8
+ }
9
+
10
+ if Jason.transport_service == :pusher
11
+ payload.merge!({
12
+ pusherKey: Jason.pusher_key,
13
+ pusherRegion: Jason.pusher_region,
14
+ pusherChannelPrefix: Jason.pusher_channel_prefix
15
+ })
16
+ end
17
+
18
+ render json: payload
19
+ end
20
+
21
+ def action
22
+ type = params[:type]
23
+ entity = type.split('/')[0].underscore
24
+ api_model = Jason::ApiModel.new(entity.singularize)
25
+ model = entity.singularize.camelize.constantize
26
+ action = type.split('/')[1].underscore
27
+
28
+ if action == 'move_priority'
29
+ id, priority = params[:payload].values_at(:id, :priority)
30
+
31
+ instance = model.find(id)
32
+ priority_filter = instance.as_json.with_indifferent_access.slice(*api_model.priority_scope)
33
+
34
+ all_instance_ids = model.send(api_model.scope || :all).where(priority_filter).where.not(id: instance.id).order(:priority).pluck(:id)
35
+ all_instance_ids.insert(priority.to_i, instance.id)
36
+
37
+ all_instance_ids.each_with_index do |id, i|
38
+ model.find(id).update!(priority: i, skip_publish_json: true)
39
+ end
40
+
41
+ model.publish_all(model.find(all_instance_ids))
42
+ elsif action == 'upsert' || action == 'add'
43
+ payload = api_model.permit(params)
44
+ return render json: model.find_or_create_by_id!(payload).as_json(api_model.as_json_config)
45
+ elsif action == 'remove'
46
+ model.find(params[:payload]).destroy!
47
+ end
48
+
49
+ return head :ok
50
+ end
51
+
52
+ def create_subscription
53
+ @subscription.add_consumer(params[:consumer_id])
54
+ render json: { channelName: @subscription.channel }
55
+ end
56
+
57
+ def remove_subscription
58
+ @subscription.remove_consumer(params[:consumer_id])
59
+ end
60
+
61
+ def get_payload
62
+ if params[:options].try(:[], :force_refresh)
63
+ @subscription.set_ids_for_sub_models
64
+ end
65
+
66
+ render json: @subscription.get
67
+ end
68
+
69
+ private
70
+
71
+ def load_and_authorize_subscription
72
+ config = params[:config].to_unsafe_h
73
+ @subscription = Jason::Subscription.upsert_by_config(config['model'], conditions: config['conditions'], includes: config['includes'])
74
+ if !@subscription.user_can_access?(current_user)
75
+ return head :forbidden
76
+ end
77
+ end
78
+ end