live_record 0.2.6 → 0.2.7

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: '09689222ba6751a8e3dbeee86e5a1b650efbcad5'
4
- data.tar.gz: 475e74270ad477bc58bbc426b315a0d5c3154b7a
3
+ metadata.gz: 186f7837ba3477c89c64825de64ab23966f633f0
4
+ data.tar.gz: 8a1d31b23fdbf06a16fb18e0c3460b6f90d77d6a
5
5
  SHA512:
6
- metadata.gz: dfdbaeb4e262ed2cd2c8ae08ef501217cdeca8e7e280e0c251bdea881a9ce1255fbb3e02c39d1cb11c4c66969105d4e47eb02c0967aae33494e2ebc8375b4d60
7
- data.tar.gz: 9117d8f61628a0839862f93e15bb73bbee42ee65ca5e96a60f18e6a7fa7129477fa11e8c4e3c5aa64c4d3f382c9e9aa7b23b58ef0654f964094b21e2e393fd0b
6
+ metadata.gz: 72b4f7a7ecd74ad4b4f4ad6d5f9d74ce5561d5ebaee4982a0313b3567331810a1e4bcf554d3f90b276a04a1c7b111b999f700bc178d705ebd125a4d27d63ebeb
7
+ data.tar.gz: 8035a50ec324b8e19f3f143a48379ef510336ffa0f781461fa57df8e1ce0fa964f3ccb7622e1c2125fc3c1724878fdc868165a4af3cef8989af6ede5affb58a8
data/README.md CHANGED
@@ -35,7 +35,7 @@
35
35
  // subscribe and auto-receive newly created Book records from the Rails server
36
36
  LiveRecord.Model.all.Book.subscribe()
37
37
 
38
- // ... or also load all Book records as well (not just the new ones)
38
+ // ... or also load all Book records as well, and then subscribes for new ones that will be created
39
39
  // LiveRecord.Model.all.Book.subscribe({reload: true})
40
40
 
41
41
  // ...or only those which are enabled (you can also combine this with `reload: true`)
@@ -108,6 +108,16 @@
108
108
  []
109
109
  end
110
110
  end
111
+
112
+ def self.live_record_queryable_attributes(current_user)
113
+ # Add attributes to this array that you would like `current_user` to be able to query upon on the `subscribe({where: {...}}) function`
114
+ # empty array means not-authorised
115
+ if current_user.isAdmin?
116
+ [:title, :author, :created_at, :updated_at, :reference_id, :origin_address]
117
+ else
118
+ []
119
+ end
120
+ end
111
121
  end
112
122
  ```
113
123
 
@@ -117,7 +127,7 @@
117
127
  1. Add the following to your `Gemfile`:
118
128
 
119
129
  ```ruby
120
- gem 'live_record', '~> 0.2.6'
130
+ gem 'live_record', '~> 0.2.7'
121
131
  ```
122
132
 
123
133
  2. Run:
@@ -172,6 +182,12 @@
172
182
  # Defaults to empty array, thereby blocking everything by default, only unless explicitly stated here so.
173
183
  [:title, :author, :created_at, :updated_at]
174
184
  end
185
+
186
+ def self.live_record_queryable_attributes(current_user)
187
+ # Add attributes to this array that you would like current_user to query upon when using `.subscribe({where: {...})`
188
+ # Defaults to empty array, thereby blocking everything by default, only unless explicitly stated here so.
189
+ [:title, :author, :created_at, :updated_at]
190
+ end
175
191
  end
176
192
  ```
177
193
 
@@ -194,6 +210,13 @@
194
210
  []
195
211
  end
196
212
  end
213
+
214
+ def self.live_record_queryable_attributes(current_user)
215
+ # this method should look like your `live_record_whitelisted_attributes` above, only except if you want to further customise this for flexibility
216
+ # or... that you may just simply return `[]` (empty array) if you do not want to allow users to use `subscribe()`
217
+ # also take note that this method only has `current_user` argument compared to `live_record_whitelisted_attributes` above which also has the `book` argument. This is intended for SQL performance reasons
218
+ [:title, :author, :created_at, :updated_at]
219
+ end
197
220
  end
198
221
  ```
199
222
 
@@ -348,6 +371,24 @@
348
371
  })
349
372
  ```
350
373
 
374
+ ### Example 3 - Using `Subscribe({reload: true})`
375
+
376
+ > You may also load records from the backend by using `subscribe({reload: true})`. `subscribe()` just auto-loads NEW records that will be created, while `subscribe({reload: true})` first loads ALL records (subject to its {where: ...} condition), and then also auto-loads new records that will be created
377
+
378
+ ```js
379
+ var subscription = LiveRecord.Model.all.Book.subscribe({
380
+ reload: true,
381
+ where: { title_matches: '%Harry Potter%' },
382
+ callbacks: {
383
+ 'after:create': function(book) {
384
+ console.log('Created the following', book);
385
+ }
386
+ }
387
+ });
388
+ ```
389
+
390
+ > Take note however that `subscribe()` above not only LOADS but also SUBSCRIBES! See 9. below for details
391
+
351
392
  9. To automatically receive new Book records, and/or also load the old ones, you may subscribe:
352
393
 
353
394
  ```js
@@ -401,16 +442,22 @@
401
442
  [:title, :is_enabled]
402
443
  end
403
444
 
445
+ ## this method will be invoked when `subscribe()` is called
446
+ ## but, you should not use this method when using `ransack` gem!
447
+ ## ransack's methods like `ransackable_attributes` below will be invoked instead
448
+ # def self.live_record_queryable_attributes(book, current_user)
449
+ # [:title, :is_enabled]
450
+ # end
451
+
404
452
  private
405
453
 
406
- # see ransack gem for more details: https://github.com/activerecord-hackery/ransack#authorization-whitelistingblacklisting
407
- # you can write your own columns here, but you may just simply allow ALL COLUMNS to be searchable, because the `live_record_whitelisted_attributes` method above will be also called anyway, and therefore just simply handle whitelisting there.
408
- # therefore you can actually remove the whole `self.ransackable_attributes` method below
454
+ # see ransack gem for more details regarding Authorization: https://github.com/activerecord-hackery/ransack#authorization-whitelistingblacklisting
409
455
 
410
- ## LiveRecord passes the `current_user` into `auth_object`, so you can access `current_user` inside below
411
- # def self.ransackable_attributes(auth_object = nil)
412
- # column_names + _ransackers.keys
413
- # end
456
+ # this method will be invoked when `subscribe()` is called
457
+ # LiveRecord passes the `current_user` into `auth_object`, so you can access `current_user` inside below
458
+ def self.ransackable_attributes(auth_object = nil)
459
+ column_names + _ransackers.keys
460
+ end
414
461
  end
415
462
  ```
416
463
 
@@ -505,8 +552,8 @@
505
552
  * `callbacks`: (Object)
506
553
  * `on:connect`: (function Object)
507
554
  * `on:disconnect`: (function Object)
508
- * `before:create`: (function Object)
509
- * `after:create`: (function Object)
555
+ * `before:create`: (function Object; function argument = record)
556
+ * `after:create`: (function Object; function argument = record)
510
557
  * subscribes to the `LiveRecord::PublicationsChannel`, which then automatically receives new records from the backend.
511
558
  * when `reload: true`, all records (subject to `where` condition above) are immediately loaded, and not just the new ones.
512
559
  * you can also pass in `callbacks` (see above). These callbacks are only applicable to this subscription, and is independent of the Model and Instance callbacks.
@@ -525,6 +572,8 @@
525
572
  * `gteq` greater than or equal to; i.e. `created_at_gteq: '2017-12-291T13:47:59.238Z'`
526
573
  * `in` in Array; i.e. `id_in: [2, 56, 19, 68]`
527
574
  * `not_in` in Array; i.e. `id_not_in: [2, 56, 19, 68]`
575
+ * `matches` matches using SQL `LIKE`; i.e. `matches: '%Harry Potter%'`
576
+ * `does_not_match` does not match using SQL `NOT LIKE`; i.e. `does_not_match: '%Harry Potter%'`
528
577
 
529
578
  ### `MODEL.unsubscribe(SUBSCRIPTION)`
530
579
  * unsubscribes to the `LiveRecord::PublicationsChannel`, thereby will not be receiving new records anymore.
@@ -610,6 +659,12 @@
610
659
  * MIT
611
660
 
612
661
  ## Changelog
662
+ * 0.2.7
663
+ * improved performance when using `subscribe({reload: true})`, but by doing so, I am forced to a conscious decision to have another separate model method for "queryable" attributes: `live_record_queryable_attributes`, which both has `pro` and `cons`
664
+ * pros:
665
+ * greatly sped up SQL for loading of data
666
+ * pro & con: now two methods in model: `live_record_whitelisted_attributes` and `live_record_queryable_attributes` which should have mostly the same code [(see some differences above)]('#example-1---simple-usage') which makes it repetitive to some degree, although this allows more flexibility.
667
+ * added `matches` and `does_not_match` filters
613
668
  * 0.2.6
614
669
  * fixed minor bug where `MODELINSTANCE.changes` do not accurately work on NULL values.
615
670
  * 0.2.5
@@ -4,7 +4,7 @@ LiveRecord.Model.create = (config) ->
4
4
  config.plugins != undefined || config.callbacks = {}
5
5
 
6
6
  # NEW
7
- Model = (attributes) ->
7
+ Model = (attributes = {}) ->
8
8
  @attributes = attributes
9
9
 
10
10
  Object.keys(@attributes).forEach (attribute_key) ->
@@ -89,14 +89,25 @@ LiveRecord.Model.create = (config) ->
89
89
  config.callbacks['on:disconnect'].call(this) if config.callbacks['on:disconnect']
90
90
 
91
91
  received: (data) ->
92
- @onAction[data.action].call(this, data)
92
+ if data.error
93
+ @liveRecord._staleSince = (new Date()).toISOString() unless @liveRecord._staleSince
94
+ @onError[data.error.code].call(this, data)
95
+ else
96
+ @onAction[data.action].call(this, data)
93
97
 
94
98
  onAction:
95
99
  create: (data) ->
96
- config.callbacks['before:create'].call(this, data) if config.callbacks['before:create']
97
100
  record = new Model(data.attributes)
101
+ config.callbacks['before:create'].call(this, record) if config.callbacks['before:create']
98
102
  record.create()
99
- config.callbacks['after:create'].call(this, data) if config.callbacks['after:create']
103
+ config.callbacks['after:create'].call(this, record) if config.callbacks['after:create']
104
+
105
+ # handler for received() callback above
106
+ onError:
107
+ forbidden: (data) ->
108
+ console.error('[LiveRecord Response Error]', data.error.code, ':', data.error.message, 'for', this)
109
+ bad_request: (data) ->
110
+ console.error('[LiveRecord Response Error]', data.error.code, ':', data.error.message, 'for', this)
100
111
 
101
112
  syncRecords: ->
102
113
  @perform(
@@ -8,10 +8,10 @@ LiveRecord.plugins.LiveDOM.applyToModel = (Model, pluginValue) ->
8
8
  $updatableElements = domContext.find('[data-live-record-update-from]')
9
9
 
10
10
  for key, value of this.attributes
11
- $updatableElements.filter('[data-live-record-update-from="' + Model.modelName + '-' + this.id() + '-' + key + '"]').text(this[key]())
11
+ $updatableElements.filter('[data-live-record-update-from="' + Model.modelName + '-' + this.id() + '-' + key + '"]').text(this.attributes[key])
12
12
 
13
13
  Model._destroyDomCallback = ->
14
14
  $('[data-live-record-destroy-from="' + Model.modelName + '-' + this.id() + '"]').remove()
15
-
15
+
16
16
  Model.addCallback('after:update', Model._updateDomCallback)
17
17
  Model.addCallback('after:destroy', Model._destroyDomCallback)
@@ -4,7 +4,7 @@ class LiveRecord::BaseChannel < ActionCable::Channel::Base
4
4
 
5
5
  def authorised_attributes(record, current_user)
6
6
  whitelisted_attributes = record.class.live_record_whitelisted_attributes(record, current_user)
7
- raise "#{record.model}.live_record_whitelisted_attributes should return an array" unless whitelisted_attributes.is_a? Array
7
+ raise "#{record.model}.live_record_whitelisted_attributes should return an array" unless whitelisted_attributes.is_a? Array
8
8
  ([:id] + whitelisted_attributes).map(&:to_s).to_set
9
9
  end
10
10
 
@@ -38,4 +38,4 @@ class LiveRecord::BaseChannel < ActionCable::Channel::Base
38
38
  transmit error: { 'code' => 'bad_request', 'message' => (message || 'Invalid request parameters') }
39
39
  end
40
40
  end
41
- end
41
+ end
@@ -14,19 +14,22 @@ class LiveRecord::PublicationsChannel < LiveRecord::BaseChannel
14
14
  reject_subscription
15
15
  end
16
16
 
17
+ if !(model_class && model_class.live_record_queryable_attributes(current_user).present?)
18
+ respond_with_error(:forbidden, 'You do not have privileges to query')
19
+ reject_subscription
20
+ end
21
+
17
22
  stream_from "live_record:publications:#{params[:model_name].underscore}", coder: ActiveSupport::JSON do |message|
18
23
  newly_created_record = model_class.find(message['attributes']['id'])
19
24
 
20
- @authorised_attributes ||= authorised_attributes(newly_created_record, current_user)
21
-
22
25
  active_record_relation = SearchAdapters.mapped_active_record_relation(
23
26
  model_class: model_class,
24
27
  conditions_hash: params[:where].to_h,
25
- current_user: current_user,
26
- authorised_attributes: @authorised_attributes
28
+ current_user: current_user
27
29
  )
28
30
 
29
31
  if active_record_relation.exists?(id: newly_created_record.id)
32
+ @authorised_attributes ||= authorised_attributes(newly_created_record, current_user)
30
33
  # if not just :id
31
34
  if @authorised_attributes.size > 1
32
35
  message = { 'action' => 'create', 'attributes' => message['attributes'] }
@@ -42,96 +45,31 @@ class LiveRecord::PublicationsChannel < LiveRecord::BaseChannel
42
45
  model_class = params[:model_name].safe_constantize
43
46
 
44
47
  if model_class && model_class < ApplicationRecord
45
- records = model_class.all
48
+
49
+ active_record_relation = SearchAdapters.mapped_active_record_relation(
50
+ model_class: model_class,
51
+ conditions_hash: params[:where].to_h,
52
+ current_user: current_user,
53
+ )
46
54
 
47
55
  if params[:stale_since].present?
48
- records = records.where(
56
+ active_record_relation = active_record_relation.where(
49
57
  'created_at >= ?', DateTime.parse(params[:stale_since]) - LiveRecord.configuration.sync_record_buffer_time
50
58
  )
51
59
  end
52
60
 
53
61
  # we `transmmit` a message back to client for each matching record
54
- records.find_each do |record|
55
- # now we check each record if it is part of the "where" condition
62
+ active_record_relation.find_each do |record|
63
+ # but first, check for the authorised attributes, if exists
56
64
  current_authorised_attributes = authorised_attributes(record, current_user)
57
65
 
58
- active_record_relation = SearchAdapters.mapped_active_record_relation(
59
- model_class: model_class,
60
- conditions_hash: params[:where].to_h,
61
- current_user: current_user,
62
- authorised_attributes: current_authorised_attributes
63
- )
64
-
65
- if active_record_relation.exists?(id: record)
66
- message = { 'action' => 'create', 'attributes' => record.attributes }
67
- response = filtered_message(message, current_authorised_attributes)
68
- transmit response if response.present?
69
- end
66
+ message = { 'action' => 'create', 'attributes' => record.attributes }
67
+ response = filtered_message(message, current_authorised_attributes)
68
+ transmit response if response.present?
70
69
  end
71
70
  else
72
71
  respond_with_error(:bad_request, 'Not a correct model name')
73
72
  reject_subscription
74
73
  end
75
74
  end
76
-
77
- module SearchAdapters
78
- def self.mapped_active_record_relation(**args)
79
- # if ransack is loaded, use ransack
80
- if Gem.loaded_specs.has_key? 'ransack'
81
- active_record_relation = RansackAdapter.mapped_active_record_relation(args)
82
- else
83
- active_record_relation = ActiveRecordDefaultAdapter.mapped_active_record_relation(args)
84
- end
85
- active_record_relation
86
- end
87
-
88
- module RansackAdapter
89
- def self.mapped_active_record_relation(**args)
90
- model_class = args.fetch(:model_class)
91
- conditions_hash = args.fetch(:conditions_hash)
92
- current_user = args.fetch(:current_user)
93
-
94
- model_class.ransack(conditions_hash, auth_object: current_user).result
95
- end
96
- end
97
-
98
- module ActiveRecordDefaultAdapter
99
- def self.mapped_active_record_relation(**args)
100
- model_class = args.fetch(:model_class)
101
- conditions_hash = args.fetch(:conditions_hash)
102
- authorised_attributes = args.fetch(:authorised_attributes)
103
-
104
- current_active_record_relation = model_class.all
105
-
106
- conditions_hash.each do |key, value|
107
- operator = key.split('_').last
108
- # to get attribute_name, we subtract the end part of the string with size of operator substring; i.e.: created_at_lteq -> created_at
109
- attribute_name = key[0..(-1 - operator.size - 1)]
110
-
111
- if authorised_attributes == :all || authorised_attributes.include?(attribute_name)
112
- case operator
113
- when 'eq'
114
- current_active_record_relation = current_active_record_relation.where(attribute_name => value)
115
- when 'not_eq'
116
- current_active_record_relation = current_active_record_relation.where.not(attribute_name => value)
117
- when 'gt'
118
- current_active_record_relation = current_active_record_relation.where(model_class.arel_table[attribute_name].gt(value))
119
- when 'gteq'
120
- current_active_record_relation = current_active_record_relation.where(model_class.arel_table[attribute_name].gteq(value))
121
- when 'lt'
122
- current_active_record_relation = current_active_record_relation.where(model_class.arel_table[attribute_name].lt(value))
123
- when 'lteq'
124
- current_active_record_relation = current_active_record_relation.where(model_class.arel_table[attribute_name].lteq(value))
125
- when 'in'
126
- current_active_record_relation = current_active_record_relation.where(attribute_name => Array.wrap(value))
127
- when 'not_in'
128
- current_active_record_relation = current_active_record_relation.where.not(attribute_name => Array.wrap(value))
129
- end
130
- end
131
- end
132
-
133
- current_active_record_relation
134
- end
135
- end
136
- end
137
75
  end
@@ -0,0 +1,68 @@
1
+ class LiveRecord::PublicationsChannel
2
+
3
+ module SearchAdapters
4
+ def self.mapped_active_record_relation(**args)
5
+ # if ransack is loaded, use ransack
6
+ if Gem.loaded_specs.has_key? 'ransack'
7
+ active_record_relation = RansackAdapter.mapped_active_record_relation(args)
8
+ else
9
+ active_record_relation = ActiveRecordDefaultAdapter.mapped_active_record_relation(args)
10
+ end
11
+ active_record_relation
12
+ end
13
+
14
+ module RansackAdapter
15
+ def self.mapped_active_record_relation(**args)
16
+ model_class = args.fetch(:model_class)
17
+ conditions_hash = args.fetch(:conditions_hash)
18
+ current_user = args.fetch(:current_user)
19
+
20
+ model_class.ransack(conditions_hash, auth_object: current_user).result
21
+ end
22
+ end
23
+
24
+ module ActiveRecordDefaultAdapter
25
+ def self.mapped_active_record_relation(**args)
26
+ model_class = args.fetch(:model_class)
27
+ conditions_hash = args.fetch(:conditions_hash)
28
+ current_user = args.fetch(:current_user)
29
+
30
+ current_active_record_relation = model_class.all
31
+ queryable_attributes = model_class.live_record_queryable_attributes(current_user)
32
+
33
+ conditions_hash.each do |key, value|
34
+ operator = key.split('_').last
35
+ # to get attribute_name, we subtract the end part of the string with size of operator substring; i.e.: created_at_lteq -> created_at
36
+ attribute_name = key[0..(-1 - operator.size - 1)].to_sym
37
+
38
+ if queryable_attributes.include?(attribute_name)
39
+ case operator
40
+ when 'eq'
41
+ current_active_record_relation = current_active_record_relation.where(attribute_name => value)
42
+ when 'not_eq'
43
+ current_active_record_relation = current_active_record_relation.where.not(attribute_name => value)
44
+ when 'gt'
45
+ current_active_record_relation = current_active_record_relation.where(model_class.arel_table[attribute_name].gt(value))
46
+ when 'gteq'
47
+ current_active_record_relation = current_active_record_relation.where(model_class.arel_table[attribute_name].gteq(value))
48
+ when 'lt'
49
+ current_active_record_relation = current_active_record_relation.where(model_class.arel_table[attribute_name].lt(value))
50
+ when 'lteq'
51
+ current_active_record_relation = current_active_record_relation.where(model_class.arel_table[attribute_name].lteq(value))
52
+ when 'in'
53
+ current_active_record_relation = current_active_record_relation.where(attribute_name => Array.wrap(value))
54
+ when 'not_in'
55
+ current_active_record_relation = current_active_record_relation.where.not(attribute_name => Array.wrap(value))
56
+ when 'matches'
57
+ current_active_record_relation = current_active_record_relation.where("#{attribute_name} LIKE ?", value)
58
+ when 'does_not_match'
59
+ current_active_record_relation = current_active_record_relation.where("#{attribute_name} NOT LIKE ?", value)
60
+ end
61
+ end
62
+ end
63
+
64
+ current_active_record_relation
65
+ end
66
+ end
67
+ end
68
+ end
@@ -9,14 +9,35 @@ class <%= class_name %> < <%= parent_class_name.classify %>
9
9
  <% if attributes.any?(&:password_digest?) -%>
10
10
  has_secure_password
11
11
  <% end -%>
12
-
12
+
13
13
  include LiveRecord::Model::Callbacks
14
14
  has_many :live_record_updates, as: :recordable, dependent: :destroy
15
15
 
16
16
  def self.live_record_whitelisted_attributes(<%= class_name.underscore %>, current_user)
17
- # Add attributes to this array that you would like current_user to have access to.
17
+ # Add attributes to this array that you would like current_user client to be able to receive
18
+ # Defaults to empty array, thereby blocking everything by default, only unless explicitly stated here so.
19
+ # i.e. if this file is a User model, and that a User has been created/updated in the backend,
20
+ # then only these whitelisted attributes will be sent to this current_user client
21
+ # Empty array means unauthorized
22
+ # Example:
23
+ # [:email, :name, :is_admin, :group_id, :created_at, :updated_at]
24
+ []
25
+ end
26
+
27
+ def self.live_record_queryable_attributes(current_user)
28
+ # This method only applies when not using `ransack` gem!
29
+ # If you're using ransack gem, instead of this method, use one or more of the ransack methods:
30
+ # see https://github.com/activerecord-hackery/ransack#authorization-whitelistingblacklisting
31
+ #
32
+ # Add attributes to this array that you would like current_user client to be able to query upon when "subscribing"
18
33
  # Defaults to empty array, thereby blocking everything by default, only unless explicitly stated here so.
34
+ # i.e. if a current_user client subscribes to "new records creation" using `.subscribe({where: {...}})`,
35
+ # then only these attributes will be considered in the "{where: ...}" argument
36
+ # if you're using `ransack` gem, use `ransackable_attributes`
37
+ # Empty array means unauthorized
38
+ # Example:
39
+ # [:email, :name, :is_admin, :group_id, :created_at, :updated_at]
19
40
  []
20
41
  end
21
42
  end
22
- <% end -%>
43
+ <% end -%>
@@ -1,3 +1,3 @@
1
1
  module LiveRecord
2
- VERSION = '0.2.6'.freeze
2
+ VERSION = '0.2.7'.freeze
3
3
  end
@@ -98,7 +98,9 @@ RSpec.feature 'LiveRecord Syncing', type: :feature do
98
98
  scenario 'JS-Client receives live new (create) post records where specified "conditions" matched', js: true do
99
99
  visit '/posts'
100
100
 
101
- sleep(5)
101
+ # wait first for all posts to be loaded
102
+ wait before: -> { evaluate_script("Object.keys( LiveRecord.Model.all.Post.all ).length") }, becomes: -> (value) { value == Post.all.count }, duration: 10.seconds
103
+ expect(evaluate_script("Object.keys( LiveRecord.Model.all.Post.all ).length")).to be Post.all.count
102
104
 
103
105
  post4 = create(:post, id: 98, is_enabled: true)
104
106
  post5 = create(:post, id: 99, is_enabled: false)
@@ -114,7 +116,9 @@ RSpec.feature 'LiveRecord Syncing', type: :feature do
114
116
  scenario 'JS-Client receives live new (create) post records where only considered "conditions" are the whitelisted authorised attributes', js: true do
115
117
  visit '/posts'
116
118
 
117
- sleep(5)
119
+ # wait first for all posts to be loaded
120
+ wait before: -> { evaluate_script("Object.keys( LiveRecord.Model.all.Post.all ).length") }, becomes: -> (value) { value == Post.all.count }, duration: 10.seconds
121
+ expect(evaluate_script("Object.keys( LiveRecord.Model.all.Post.all ).length")).to be Post.all.count
118
122
 
119
123
  # in index.html.erb:
120
124
  # LiveRecord.Model.all.Post.subscribe({ where: { is_enabled: true, content: 'somecontent' }});
@@ -133,7 +137,9 @@ RSpec.feature 'LiveRecord Syncing', type: :feature do
133
137
  scenario 'JS-Client receives live new (create) post records having only the whitelisted authorised attributes', js: true do
134
138
  visit '/posts'
135
139
 
136
- sleep(5)
140
+ # wait first for all posts to be loaded
141
+ wait before: -> { evaluate_script("Object.keys( LiveRecord.Model.all.Post.all ).length") }, becomes: -> (value) { value == Post.all.count }, duration: 10.seconds
142
+ expect(evaluate_script("Object.keys( LiveRecord.Model.all.Post.all ).length")).to be Post.all.count
137
143
 
138
144
  post4 = create(:post, is_enabled: true, title: 'sometitle', content: 'somecontent')
139
145
 
@@ -142,6 +148,44 @@ RSpec.feature 'LiveRecord Syncing', type: :feature do
142
148
  expect(evaluate_script("LiveRecord.Model.all.Post.all[#{post4.id}].content()")).to eq nil
143
149
  end
144
150
 
151
+ scenario 'JS-Client should not have shared callbacks for those callbacks defined only for a particular post record', js: true do
152
+ visit '/posts'
153
+
154
+ # wait first for all posts to be loaded
155
+ wait before: -> { evaluate_script("Object.keys( LiveRecord.Model.all.Post.all ).length") }, becomes: -> (value) { value == Post.all.count }, duration: 10.seconds
156
+ expect(evaluate_script("Object.keys( LiveRecord.Model.all.Post.all ).length")).to be Post.all.count
157
+
158
+ execute_script(
159
+ <<-eos
160
+ var post1 = LiveRecord.Model.all.Post.all[#{post1.id}];
161
+ var someCallbackFunction = function() {};
162
+ post1.addCallback('after:create', someCallbackFunction);
163
+ eos
164
+ )
165
+
166
+ expect(evaluate_script("LiveRecord.Model.all.Post.all[#{post2.id}]._callbacks['after:create'].length")).to eq 0
167
+ end
168
+
169
+ scenario 'JS-Client should receive response :forbidden error when using `subscribe()` but that the current_user is forbidden to do so', js: true do
170
+ visit '/posts'
171
+
172
+ # wait first for all posts to be loaded
173
+ wait before: -> { evaluate_script("Object.keys( LiveRecord.Model.all.Post.all ).length") }, becomes: -> (value) { value == Post.all.count }, duration: 10.seconds
174
+ expect(evaluate_script("Object.keys( LiveRecord.Model.all.Post.all ).length")).to be Post.all.count
175
+
176
+ expect_any_instance_of(LiveRecord::BaseChannel).to(
177
+ receive(:respond_with_error).with(:forbidden, 'You do not have privileges to query').once
178
+ )
179
+
180
+ execute_script(
181
+ <<-eos
182
+ LiveRecord.Model.all.User.subscribe();
183
+ eos
184
+ )
185
+
186
+ sleep(5)
187
+ end
188
+
145
189
  # see spec/internal/app/models/post.rb to view specified whitelisted attributes
146
190
  context 'when client got disconnected, and then reconnected' do
147
191
  scenario 'JS-Client resyncs stale records', js: true do
@@ -150,9 +194,11 @@ RSpec.feature 'LiveRecord Syncing', type: :feature do
150
194
 
151
195
  visit '/posts'
152
196
 
153
- sleep(5)
197
+ # wait first for all posts to be loaded
198
+ wait before: -> { evaluate_script("Object.keys( LiveRecord.Model.all.Post.all ).length") }, becomes: -> (value) { value == Post.all.count }, duration: 10.seconds
199
+ expect(evaluate_script("Object.keys( LiveRecord.Model.all.Post.all ).length")).to be Post.all.count
154
200
 
155
- disconnection_time = 20 # seconds
201
+ disconnection_time = 10 # seconds
156
202
 
157
203
  Thread.new do
158
204
  sleep(2)
@@ -180,9 +226,11 @@ RSpec.feature 'LiveRecord Syncing', type: :feature do
180
226
  scenario 'JS-Client receives all post records created during the time it got disconnected', js: true do
181
227
  visit '/posts'
182
228
 
183
- sleep(5)
229
+ # wait first for all posts to be loaded
230
+ wait before: -> { evaluate_script("Object.keys( LiveRecord.Model.all.Post.all ).length") }, becomes: -> (value) { value == Post.all.count }, duration: 10.seconds
231
+ expect(evaluate_script("Object.keys( LiveRecord.Model.all.Post.all ).length")).to be Post.all.count
184
232
 
185
- disconnection_time = 20 # seconds
233
+ disconnection_time = 10 # seconds
186
234
 
187
235
  post_created_before_disconnection = nil
188
236
  post_created_while_disconnected_1 = nil
@@ -215,22 +263,5 @@ RSpec.feature 'LiveRecord Syncing', type: :feature do
215
263
  wait before: -> { evaluate_script("LiveRecord.Model.all.Post.all[#{post_created_before_disconnection.id}] == undefined") }, becomes: -> (value) { value == false }
216
264
  expect(evaluate_script("LiveRecord.Model.all.Post.all[#{post_created_before_disconnection.id}] == undefined")).to be true
217
265
  end
218
-
219
- scenario 'JS-Client should not have shared callbacks for those callbacks defined only for a particular post record', js: true do
220
- visit '/posts'
221
-
222
- wait before: -> { evaluate_script("Object.keys( LiveRecord.Model.all.Post.all ).length") }, becomes: -> (value) { value == posts.count }, duration: 10.seconds
223
- expect(evaluate_script("Object.keys( LiveRecord.Model.all.Post.all ).length")).to be posts.count
224
-
225
- execute_script(
226
- <<-eos
227
- var post1 = LiveRecord.Model.all.Post.all[#{post1.id}];
228
- var someCallbackFunction = function() {};
229
- post1.addCallback('after:create', someCallbackFunction);
230
- eos
231
- )
232
-
233
- expect(evaluate_script("LiveRecord.Model.all.Post.all[#{post2.id}]._callbacks['after:create'].length")).to eq 0
234
- end
235
266
  end
236
267
  end
@@ -5,8 +5,29 @@ class Post < ApplicationRecord
5
5
  has_many :live_record_updates, as: :recordable, dependent: :destroy
6
6
 
7
7
  def self.live_record_whitelisted_attributes(post, current_user)
8
- # Add attributes to this array that you would like current_user to have access to.
8
+ # Add attributes to this array that you would like current_user client to be able to receive
9
9
  # Defaults to empty array, thereby blocking everything by default, only unless explicitly stated here so.
10
+ # i.e. if this file is a User model, and that a User has been created/updated in the backend,
11
+ # then only these whitelisted attributes will be sent to this current_user client
12
+ # Empty array means unauthorized
13
+ # Example:
14
+ # [:email, :name, :is_admin, :group_id, :created_at, :updated_at]
15
+ [:title, :is_enabled, :user_id, :created_at, :updated_at]
16
+ end
17
+
18
+ def self.live_record_queryable_attributes(current_user)
19
+ # This method only applies when not using `ransack` gem!
20
+ # If you're using ransack gem, instead of this method, use one or more of the ransack methods:
21
+ # see https://github.com/activerecord-hackery/ransack#authorization-whitelistingblacklisting
22
+ #
23
+ # Add attributes to this array that you would like current_user client to be able to query upon when "subscribing"
24
+ # Defaults to empty array, thereby blocking everything by default, only unless explicitly stated here so.
25
+ # i.e. if a current_user client subscribes to "new records creation" using `.subscribe({where: {...}})`,
26
+ # then only these attributes will be considered in the "{where: ...}" argument
27
+ # if you're using `ransack` gem, use `ransackable_attributes`
28
+ # Empty array means unauthorized
29
+ # Example:
30
+ # [:email, :name, :is_admin, :group_id, :created_at, :updated_at]
10
31
  [:title, :is_enabled, :user_id, :created_at, :updated_at]
11
32
  end
12
33
  end
@@ -9,4 +9,8 @@ class User < ApplicationRecord
9
9
  # Defaults to empty array, thereby blocking everything by default, only unless explicitly stated here so.
10
10
  [:email, :created_at, :updated_at]
11
11
  end
12
+
13
+ def self.live_record_queryable_attributes(current_user)
14
+ []
15
+ end
12
16
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: live_record
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.6
4
+ version: 0.2.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jules Roman B. Polidario
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-11-10 00:00:00.000000000 Z
11
+ date: 2017-11-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -284,6 +284,7 @@ files:
284
284
  - app/channels/live_record/base_channel.rb
285
285
  - app/channels/live_record/changes_channel.rb
286
286
  - app/channels/live_record/publications_channel.rb
287
+ - app/channels/live_record/publications_channel/search_adapters.rb
287
288
  - config.ru
288
289
  - lib/live_record.rb
289
290
  - lib/live_record/action_view_extensions/view_helper.rb