jason-rails 0.5.1 → 0.6.4

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/README.md +41 -7
  4. data/app/controllers/jason/api/pusher_controller.rb +15 -0
  5. data/app/controllers/jason/api_controller.rb +46 -4
  6. data/client/lib/JasonContext.d.ts +1 -1
  7. data/client/lib/JasonContext.js +4 -1
  8. data/client/lib/JasonProvider.js +1 -1
  9. data/client/lib/createJasonReducers.js +9 -3
  10. data/client/lib/createPayloadHandler.d.ts +6 -3
  11. data/client/lib/createPayloadHandler.js +10 -6
  12. data/client/lib/createTransportAdapter.d.ts +5 -0
  13. data/client/lib/createTransportAdapter.js +20 -0
  14. data/client/lib/index.d.ts +2 -0
  15. data/client/lib/index.js +3 -1
  16. data/client/lib/makeEager.js +2 -2
  17. data/client/lib/pruneIdsMiddleware.js +9 -11
  18. data/client/lib/transportAdapters/actionCableAdapter.d.ts +5 -0
  19. data/client/lib/transportAdapters/actionCableAdapter.js +35 -0
  20. data/client/lib/transportAdapters/pusherAdapter.d.ts +5 -0
  21. data/client/lib/transportAdapters/pusherAdapter.js +68 -0
  22. data/client/lib/useEager.d.ts +1 -0
  23. data/client/lib/useEager.js +12 -0
  24. data/client/lib/useJason.js +14 -34
  25. data/client/lib/useJason.test.js +41 -3
  26. data/client/package.json +2 -1
  27. data/client/src/JasonContext.ts +4 -1
  28. data/client/src/JasonProvider.tsx +1 -1
  29. data/client/src/createJasonReducers.ts +9 -3
  30. data/client/src/createPayloadHandler.ts +11 -6
  31. data/client/src/createTransportAdapter.ts +13 -0
  32. data/client/src/index.ts +3 -1
  33. data/client/src/makeEager.ts +2 -2
  34. data/client/src/pruneIdsMiddleware.ts +12 -11
  35. data/client/src/restClient.ts +1 -0
  36. data/client/src/transportAdapters/actionCableAdapter.ts +38 -0
  37. data/client/src/transportAdapters/pusherAdapter.ts +72 -0
  38. data/client/src/useEager.ts +9 -0
  39. data/client/src/useJason.test.ts +49 -3
  40. data/client/src/useJason.ts +15 -36
  41. data/client/yarn.lock +12 -0
  42. data/config/routes.rb +5 -1
  43. data/lib/jason.rb +56 -8
  44. data/lib/jason/broadcaster.rb +19 -0
  45. data/lib/jason/channel.rb +6 -2
  46. data/lib/jason/graph_helper.rb +165 -0
  47. data/lib/jason/includes_helper.rb +108 -0
  48. data/lib/jason/lua_generator.rb +23 -1
  49. data/lib/jason/publisher.rb +20 -16
  50. data/lib/jason/subscription.rb +208 -185
  51. data/lib/jason/version.rb +1 -1
  52. metadata +18 -2
@@ -0,0 +1,38 @@
1
+ import { createConsumer } from "@rails/actioncable"
2
+
3
+ export default function actionCableAdapter(jasonConfig, handlePayload, dispatch, onConnected) {
4
+ const consumer = createConsumer()
5
+ const subscription = (consumer.subscriptions.create({
6
+ channel: 'Jason::Channel'
7
+ }, {
8
+ connected: () => {
9
+ dispatch({ type: 'jason/upsert', payload: { connected: true } })
10
+ console.debug('Connected to ActionCable')
11
+
12
+ // When AC loses connection - all state is lost, so we need to re-initialize all subscriptions
13
+ onConnected()
14
+ },
15
+ received: payload => {
16
+ handlePayload(payload)
17
+ console.debug("ActionCable Payload received: ", payload)
18
+ },
19
+ disconnected: () => {
20
+ dispatch({ type: 'jason/upsert', payload: { connected: false } })
21
+ console.warn('Disconnected from ActionCable')
22
+ }
23
+ }));
24
+
25
+ function getPayload(config, options) {
26
+ subscription.send({ getPayload: config, ...options })
27
+ }
28
+
29
+ function createSubscription(config) {
30
+ subscription.send({ createSubscription: config })
31
+ }
32
+
33
+ function removeSubscription(config) {
34
+ subscription.send({ removeSubscription: config })
35
+ }
36
+
37
+ return { getPayload, createSubscription, removeSubscription }
38
+ }
@@ -0,0 +1,72 @@
1
+ import Pusher from 'pusher-js'
2
+ import { createConsumer } from "@rails/actioncable"
3
+ import restClient from '../restClient'
4
+ import { v4 as uuidv4 } from 'uuid'
5
+ import _ from 'lodash'
6
+
7
+ export default function pusherAdapter(jasonConfig, handlePayload, dispatch) {
8
+ let consumerId = uuidv4()
9
+
10
+ const { pusherKey, pusherRegion, pusherChannelPrefix } = jasonConfig
11
+ const pusher = new Pusher(pusherKey, {
12
+ cluster: 'eu',
13
+ forceTLS: true,
14
+ authEndpoint: '/jason/api/pusher/auth'
15
+ })
16
+ pusher.connection.bind('state_change', ({ current }) => {
17
+ if (current === 'connected') {
18
+ dispatch({ type: 'jason/upsert', payload: { connected: true } })
19
+ } else {
20
+ dispatch({ type: 'jason/upsert', payload: { connected: false } })
21
+ }
22
+ })
23
+ pusher.connection.bind( 'error', error => {
24
+ dispatch({ type: 'jason/upsert', payload: { connected: false } })
25
+ });
26
+
27
+ const configToChannel = {}
28
+
29
+ function createSubscription(config) {
30
+ restClient.post('/jason/api/create_subscription', { config, consumerId })
31
+ .then(({ data: { channelName } }) => {
32
+ configToChannel[JSON.stringify(config)] = channelName
33
+ subscribeToChannel(channelName)
34
+ })
35
+ .catch(e => console.error(e))
36
+ }
37
+
38
+ function removeSubscription(config) {
39
+ const channelName = configToChannel[JSON.stringify(config)]
40
+ unsubscribeFromChannel(fullChannelName(channelName))
41
+ restClient.post('/jason/api/remove_subscription', { config, consumerId })
42
+ .catch(e => console.error(e))
43
+ }
44
+
45
+ function getPayload(config, options) {
46
+ restClient.post('/jason/api/get_payload', {
47
+ config,
48
+ options
49
+ })
50
+ .then(({ data }) => {
51
+ _.map(data, (payload, modelName) => {
52
+ handlePayload(payload)
53
+ })
54
+ })
55
+ .catch(e => console.error(e))
56
+ }
57
+
58
+ function subscribeToChannel(channelName) {
59
+ const channel = pusher.subscribe(fullChannelName(channelName))
60
+ channel.bind('changed', message => handlePayload(message))
61
+ }
62
+
63
+ function unsubscribeFromChannel(channelName) {
64
+ const channel = pusher.unsubscribe(fullChannelName(channelName))
65
+ }
66
+
67
+ function fullChannelName(channelName) {
68
+ return `private-${pusherChannelPrefix}-${channelName}`
69
+ }
70
+
71
+ return { getPayload, createSubscription, removeSubscription }
72
+ }
@@ -0,0 +1,9 @@
1
+ import JasonContext from './JasonContext'
2
+ import { useContext } from 'react'
3
+
4
+ export default function useEager(entity, id = null, relations = []) {
5
+ const { eager } = useContext(JasonContext)
6
+
7
+ return eager(entity, id, relations)
8
+ }
9
+
@@ -5,7 +5,10 @@ import restClient from './restClient'
5
5
  jest.mock('./restClient')
6
6
 
7
7
  test('it works', async () => {
8
- const resp = { data: { post: {} } };
8
+ const resp = { data: {
9
+ schema: { post: {} },
10
+ transportService: 'action_cable'
11
+ } };
9
12
  // @ts-ignore
10
13
  restClient.get.mockResolvedValue(resp);
11
14
 
@@ -45,7 +48,10 @@ test('it works', async () => {
45
48
  })
46
49
 
47
50
  test('pruning IDs', async () => {
48
- const resp = { data: { post: {} } };
51
+ const resp = { data: {
52
+ schema: { post: {} },
53
+ transportService: 'action_cable'
54
+ } };
49
55
 
50
56
  // @ts-ignore
51
57
  restClient.get.mockResolvedValue(resp);
@@ -77,5 +83,45 @@ test('pruning IDs', async () => {
77
83
  })
78
84
 
79
85
  // The ID 4 should have been pruned
80
- expect(store.getState().posts.ids).toStrictEqual([5])
86
+ expect(store.getState().posts.ids).toStrictEqual(['5'])
87
+ })
88
+
89
+ test('pruning IDs by destroy', async () => {
90
+ const resp = { data: {
91
+ schema: { post: {} },
92
+ transportService: 'action_cable'
93
+ } };
94
+
95
+ // @ts-ignore
96
+ restClient.get.mockResolvedValue(resp);
97
+
98
+ const { result, waitForNextUpdate } = renderHook(() => useJason({ reducers: {
99
+ test: (s,a) => s || {}
100
+ }}));
101
+
102
+ await waitForNextUpdate()
103
+ const [store, value, connected] = result.current
104
+ const { handlePayload, subscribe } = value
105
+
106
+ const subscription = subscribe({ post: {} })
107
+
108
+ handlePayload({
109
+ type: 'payload',
110
+ model: 'post',
111
+ payload: [{ id: 4, name: 'test' }, { id: 5, name: 'test it out' }],
112
+ md5Hash: subscription.md5Hash,
113
+ idx: 1
114
+ })
115
+ expect(store.getState().posts.ids).toStrictEqual(['4', '5'])
116
+
117
+ handlePayload({
118
+ destroy: true,
119
+ model: 'post',
120
+ id: 5,
121
+ md5Hash: subscription.md5Hash,
122
+ idx: 2
123
+ })
124
+
125
+ // The ID 4 should have been pruned
126
+ expect(store.getState().posts.ids).toStrictEqual(['4'])
81
127
  })
@@ -5,8 +5,8 @@ import createOptDis from './createOptDis'
5
5
  import createServerActionQueue from './createServerActionQueue'
6
6
  import restClient from './restClient'
7
7
  import pruneIdsMiddleware from './pruneIdsMiddleware'
8
+ import createTransportAdapater from './createTransportAdapter'
8
9
 
9
- import { createConsumer } from "@rails/actioncable"
10
10
  import { createEntityAdapter, createSlice, createReducer, configureStore } from '@reduxjs/toolkit'
11
11
 
12
12
  import makeEager from './makeEager'
@@ -18,17 +18,17 @@ import React, { useState, useEffect } from 'react'
18
18
  export default function useJason({ reducers, middleware = [], extraActions }: { reducers?: any, middleware?: any[], extraActions?: any }) {
19
19
  const [store, setStore] = useState(null as any)
20
20
  const [value, setValue] = useState(null as any)
21
- const [connected, setConnected] = useState(false)
22
21
 
23
22
  useEffect(() => {
24
- restClient.get('/jason/api/schema')
25
- .then(({ data: snakey_schema }) => {
23
+ restClient.get('/jason/api/config')
24
+ .then(({ data: jasonConfig }) => {
25
+ const { schema: snakey_schema } = jasonConfig
26
26
  const schema = camelizeKeys(snakey_schema)
27
27
  console.debug({ schema })
28
28
 
29
29
  const serverActionQueue = createServerActionQueue()
30
30
 
31
- const consumer = createConsumer()
31
+
32
32
  const allReducers = {
33
33
  ...reducers,
34
34
  ...createJasonReducers(schema)
@@ -50,54 +50,32 @@ export default function useJason({ reducers, middleware = [], extraActions }: {
50
50
  function handlePayload(payload) {
51
51
  const { md5Hash } = payload
52
52
 
53
- const handler = payloadHandlers[md5Hash]
54
- if (handler) {
55
- handler(payload)
53
+ const { handlePayload } = payloadHandlers[md5Hash]
54
+ if (handlePayload) {
55
+ handlePayload(payload)
56
56
  } else {
57
57
  console.warn("Payload arrived with no handler", payload, payloadHandlers)
58
58
  }
59
59
  }
60
60
 
61
- const subscription = (consumer.subscriptions.create({
62
- channel: 'Jason::Channel'
63
- }, {
64
- connected: () => {
65
- setConnected(true)
66
- dispatch({ type: 'jason/upsert', payload: { connected: true } })
67
- console.debug('Connected to ActionCable')
68
-
69
- // When AC loses connection - all state is lost, so we need to re-initialize all subscriptions
70
- _.keys(configs).forEach(md5Hash => createSubscription(configs[md5Hash], subOptions[md5Hash]))
71
- },
72
- received: payload => {
73
- handlePayload(payload)
74
- console.debug("ActionCable Payload received: ", payload)
75
- },
76
- disconnected: () => {
77
- setConnected(false)
78
- dispatch({ type: 'jason/upsert', payload: { connected: false } })
79
- console.warn('Disconnected from ActionCable')
80
- }
81
- }));
61
+ const transportAdapter = createTransportAdapater(jasonConfig, handlePayload, dispatch, () => _.keys(configs).forEach(md5Hash => createSubscription(configs[md5Hash], subOptions[md5Hash])))
82
62
 
83
63
  function createSubscription(config, options = {}) {
84
64
  // We need the hash to be consistent in Ruby / Javascript
85
65
  const hashableConfig = _({ conditions: {}, includes: {}, ...config }).toPairs().sortBy(0).fromPairs().value()
86
66
  const md5Hash = md5(JSON.stringify(hashableConfig))
87
- payloadHandlers[md5Hash] = createPayloadHandler({ dispatch, serverActionQueue, subscription, config })
67
+ payloadHandlers[md5Hash] = createPayloadHandler({ dispatch, serverActionQueue, transportAdapter, config })
88
68
  configs[md5Hash] = hashableConfig
89
69
  subOptions[md5Hash] = options
90
70
 
91
- setTimeout(() => subscription.send({ createSubscription: hashableConfig }), 500)
71
+ setTimeout(() => transportAdapter.createSubscription(hashableConfig), 500)
92
72
  let pollInterval = null as any;
93
73
 
94
- console.log("createSubscription", { config, options })
95
-
96
74
  // This is only for debugging / dev - not prod!
97
75
  // @ts-ignore
98
76
  if (options.pollInterval) {
99
77
  // @ts-ignore
100
- pollInterval = setInterval(() => subscription.send({ getPayload: config, forceRefresh: true }), options.pollInterval)
78
+ pollInterval = setInterval(() => transportAdapter.getPayload(hashableConfig, { forceRefresh: true }), options.pollInterval)
101
79
  }
102
80
 
103
81
  return {
@@ -110,8 +88,9 @@ export default function useJason({ reducers, middleware = [], extraActions }: {
110
88
  }
111
89
 
112
90
  function removeSubscription(config) {
113
- subscription.send({ removeSubscription: config })
91
+ transportAdapter.removeSubscription(config)
114
92
  const md5Hash = md5(JSON.stringify(config))
93
+ payloadHandlers[md5Hash].tearDown()
115
94
  delete payloadHandlers[md5Hash]
116
95
  delete configs[md5Hash]
117
96
  delete subOptions[md5Hash]
@@ -127,5 +106,5 @@ export default function useJason({ reducers, middleware = [], extraActions }: {
127
106
  })
128
107
  }, [])
129
108
 
130
- return [store, value, connected]
109
+ return [store, value]
131
110
  }
data/client/yarn.lock CHANGED
@@ -3645,6 +3645,13 @@ punycode@^2.1.0, punycode@^2.1.1:
3645
3645
  resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
3646
3646
  integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
3647
3647
 
3648
+ pusher-js@^7.0.3:
3649
+ version "7.0.3"
3650
+ resolved "https://registry.yarnpkg.com/pusher-js/-/pusher-js-7.0.3.tgz#f81c78cdf2ad32f546caa7532ec7f9081ef00b8d"
3651
+ integrity sha512-HIfCvt00CAqgO4W0BrdpPsDcAwy51rB6DN0VMC+JeVRRbo8mn3XTeUeIFjmmlRLZLX8rPhUtLRo7vPag6b8GCw==
3652
+ dependencies:
3653
+ tweetnacl "^1.0.3"
3654
+
3648
3655
  qs@~6.5.2:
3649
3656
  version "6.5.2"
3650
3657
  resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
@@ -4358,6 +4365,11 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0:
4358
4365
  resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
4359
4366
  integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
4360
4367
 
4368
+ tweetnacl@^1.0.3:
4369
+ version "1.0.3"
4370
+ resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596"
4371
+ integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==
4372
+
4361
4373
  type-check@~0.3.2:
4362
4374
  version "0.3.2"
4363
4375
  resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
data/config/routes.rb CHANGED
@@ -1,4 +1,8 @@
1
1
  Jason::Engine.routes.draw do
2
- get '/api/schema', to: 'api#schema'
2
+ get '/api/config', to: 'api#configuration'
3
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'
4
8
  end
data/lib/jason.rb CHANGED
@@ -7,22 +7,70 @@ 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
+ def self.init
34
+ # Check if the schema has changed since last time app was started. If so, do some work to ensure cache contains the correct data
35
+ got_lock = $redis_jason.set('jason:schema:lock', nx: true, ex: 3600) # Basic lock mechanism for multi-process environments
36
+ return if !got_lock
22
37
 
23
- # this function maps the vars from your app into your engine
24
- def self.setup(&block)
25
- yield self
26
- end
38
+ previous_schema = JSON.parse($redis_jason.get('jason:last_schema') || '{}')
39
+ current_schema = Jason.schema.deep_stringify_keys.deep_transform_values { |v| v.is_a?(Symbol) ? v.to_s : v }
40
+ pp current_schema
41
+ current_schema.each do |model, config|
42
+ if config != previous_schema[model]
43
+ puts "Config changed for #{model}"
44
+ puts "Old config was #{previous_schema[model]}"
45
+ puts "New config is #{config}"
46
+ puts "Rebuilding cache for #{model}"
47
+ model.classify.constantize.cache_all
48
+ puts "Done"
49
+ end
50
+ end
27
51
 
52
+ $redis_jason.set('jason:last_schema', current_schema.to_json)
53
+ ensure
54
+ $redis_jason.del('jason:schema:lock')
55
+
56
+ previous_config = 'test'
57
+ end
58
+
59
+
60
+ # this function maps the vars from your app into your engine
61
+ def self.setup(&block)
62
+ yield self
63
+
64
+ $redis_jason = self.redis || ::ConnectionPool::Wrapper.new(size: 5, timeout: 3) { ::Redis.new(url: ENV['REDIS_URL']) }
65
+
66
+ if ![:action_cable, :pusher].include?(self.transport_service)
67
+ raise "Unknown transport service '#{self.transport_service}' specified"
68
+ end
69
+
70
+ if self.transport_service == :pusher && self.pusher.blank?
71
+ raise "Pusher specified as transport service but no Pusher client provided. Please configure with config.pusher = Pusher::Client.new(...)"
72
+ end
73
+
74
+ init
75
+ end
28
76
  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