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 +4 -4
- data/README.md +66 -11
- data/app/assets/javascripts/live_record/model/create.coffee +15 -4
- data/app/assets/javascripts/live_record/plugins/live_dom/apply_to_model.coffee +2 -2
- data/app/channels/live_record/base_channel.rb +2 -2
- data/app/channels/live_record/publications_channel.rb +19 -81
- data/app/channels/live_record/publications_channel/search_adapters.rb +68 -0
- data/lib/live_record/generators/templates/model.rb.rb +24 -3
- data/lib/live_record/version.rb +1 -1
- data/spec/features/live_record_syncing_spec.rb +55 -24
- data/spec/internal/app/models/post.rb +22 -1
- data/spec/internal/app/models/user.rb +4 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 186f7837ba3477c89c64825de64ab23966f633f0
|
4
|
+
data.tar.gz: 8a1d31b23fdbf06a16fb18e0c3460b6f90d77d6a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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.
|
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
|
-
|
411
|
-
#
|
412
|
-
|
413
|
-
|
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
|
-
|
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,
|
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
|
-
|
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
|
-
|
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
|
-
|
55
|
-
#
|
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
|
-
|
59
|
-
|
60
|
-
|
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
|
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 -%>
|
data/lib/live_record/version.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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 =
|
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
|
-
|
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 =
|
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
|
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.
|
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-
|
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
|