live_record 0.2.8 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +154 -59
- data/app/assets/javascripts/live_record/model/create.coffee +78 -0
- data/app/channels/live_record/autoloads_channel.rb +92 -0
- data/app/channels/live_record/base_channel.rb +0 -25
- data/app/channels/live_record/base_channel/helpers.rb +27 -0
- data/app/channels/live_record/{publications_channel → base_channel}/search_adapters.rb +1 -1
- data/app/channels/live_record/changes_channel.rb +3 -3
- data/app/channels/live_record/publications_channel.rb +5 -5
- data/config/puma.rb +2 -0
- data/lib/live_record/model/callbacks.rb +29 -2
- data/lib/live_record/version.rb +1 -1
- data/live_record.gemspec +10 -6
- data/spec/features/live_record_syncing_spec.rb +219 -35
- data/spec/internal/app/views/posts/index.html.erb +0 -5
- data/spec/internal/db/schema.rb +4 -0
- data/spec/rails_helper.rb +1 -1
- metadata +53 -36
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 913e2af230c7be4690d16fc2015c5cc493d720e8
|
4
|
+
data.tar.gz: db0f1ae77acc7db8e2c4449659e3fe49a9e7e7c3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4a22a9f57b70907b6920481a4f54285e13e9f2e5d29e48b957ab2e0a229d7f571ccfde784ac92c7c4a1b46f57142d85626fde0349affef7bd6468b5147e065ab
|
7
|
+
data.tar.gz: 90a3db0faee89cc23cd179d7b6846c621c268e4fd6b933dc537b08a8cff1a18083f5fcd94e2e6da020eb27173e1790a674ad17674d168e05c15eee1880ce6804
|
data/README.md
CHANGED
@@ -30,7 +30,7 @@
|
|
30
30
|
* `is_enabled:boolean`
|
31
31
|
* on the JS client-side:
|
32
32
|
|
33
|
-
### Subscribing to
|
33
|
+
### Subscribing to Records Creation
|
34
34
|
```js
|
35
35
|
// subscribe and auto-receive newly created Book records from the Rails server
|
36
36
|
LiveRecord.Model.all.Book.subscribe()
|
@@ -49,6 +49,28 @@
|
|
49
49
|
})
|
50
50
|
```
|
51
51
|
|
52
|
+
### Subscribing to Records Creation/Updates
|
53
|
+
|
54
|
+
```js
|
55
|
+
// auto-load newly created Book records OR newly updated Book records
|
56
|
+
LiveRecord.Model.all.Book.autoload()
|
57
|
+
|
58
|
+
// ... or also load all Book records as well, and then subscribes for new ones that will be created / updated
|
59
|
+
// LiveRecord.Model.all.Book.autoload({reload: true})
|
60
|
+
|
61
|
+
// ...or only those which are enabled (you can also combine this with `reload: true`)
|
62
|
+
// LiveRecord.Model.all.Book.autoload({where: {is_enabled_eq: true}})
|
63
|
+
|
64
|
+
// now, we can just simply add a "create_or_update" callback, to apply our own logic whenever a new Book record is streamed from the backend
|
65
|
+
LiveRecord.Model.all.Book.addCallback('after:createOrUpdate', function() {
|
66
|
+
// let's say you have a code here that adds this new Book on the page
|
67
|
+
// `this` refers to the Book record that has been created / updated
|
68
|
+
console.log(this);
|
69
|
+
})
|
70
|
+
```
|
71
|
+
|
72
|
+
> Now you may be wondering what the differences between `autoload()` and `subscribe()` are. Simply put, `subscribe()` only receives NEWLY CREATED records, while `autoload()` receives both CREATED and UPDATED records. Let's say your JS-client has `subscribe({where: {is_enabled_eq: true}})`. You only then receive NEW records that are "enabled", however you won't receive OLD records that were "disabled" upon creation, but then got updated to be "enabled". Now, this is where you use `autoload({where: {is_enabled_eq: true}})` instead.
|
73
|
+
|
52
74
|
### Subscribing to Record Updates/Destroy
|
53
75
|
|
54
76
|
```js
|
@@ -127,7 +149,7 @@
|
|
127
149
|
1. Add the following to your `Gemfile`:
|
128
150
|
|
129
151
|
```ruby
|
130
|
-
gem 'live_record', '~> 0.2.
|
152
|
+
gem 'live_record', '~> 0.2.9'
|
131
153
|
```
|
132
154
|
|
133
155
|
2. Run:
|
@@ -371,9 +393,9 @@
|
|
371
393
|
})
|
372
394
|
```
|
373
395
|
|
374
|
-
### Example 3 - Using `
|
396
|
+
### Example 3 - Using `subscribe({reload: true})` or `autoload({reload: true})`
|
375
397
|
|
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-
|
398
|
+
> 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-fetch new records that will be created. `autoload({reload: true})` can also be used instead of `subscribe({reload: true})`.
|
377
399
|
|
378
400
|
```js
|
379
401
|
var subscription = LiveRecord.Model.all.Book.subscribe({
|
@@ -387,12 +409,12 @@
|
|
387
409
|
});
|
388
410
|
```
|
389
411
|
|
390
|
-
> Take note however that `subscribe()`
|
412
|
+
> Take note however that `subscribe()` and `autoload()` not only LOADS but also SUBSCRIBES! See 9. below for details
|
391
413
|
|
392
|
-
9. To automatically receive new Book records,
|
414
|
+
9. To automatically receive new Book records, you may subscribe:
|
393
415
|
|
394
416
|
```js
|
395
|
-
// subscribe and auto-
|
417
|
+
// subscribe and auto-fetch newly created Book records from the backend
|
396
418
|
var subscription = LiveRecord.Model.all.Book.subscribe();
|
397
419
|
|
398
420
|
// ...or also load all Book records (not just the new ones).
|
@@ -418,66 +440,113 @@
|
|
418
440
|
LiveRecord.Model.all.Book.unsubscribe(subscription);
|
419
441
|
```
|
420
442
|
|
421
|
-
|
443
|
+
10. To automatically receive new/updated Book records, you may autoload:
|
422
444
|
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
// querying upon the `belongs_to :user`
|
427
|
-
subscription = LiveRecord.Model.all.Book.subscribe({where: {user_is_admin_eq: true, is_enabled_eq: true}});
|
445
|
+
```js
|
446
|
+
// subscribe and auto-fetch newly created / updated Book records from the backend
|
447
|
+
var subscription = LiveRecord.Model.all.Book.autoload();
|
428
448
|
|
429
|
-
|
430
|
-
|
431
|
-
|
449
|
+
// ...or also load all Book records (not just the new ones).
|
450
|
+
// useful for populating records at the start, and therefore you may skip using `LiveRecord.helpers.loadRecords()` already
|
451
|
+
// subscription = LiveRecord.Model.all.Book.autoload({reload: true});
|
432
452
|
|
433
|
-
|
453
|
+
// ...or subscribe only to certain conditions (i.e. when `is_enabled` attribute value is `true`)
|
454
|
+
// For the list of supported operators (like `..._eq`), see JS API `MODEL.autoload(CONFIG)` below
|
455
|
+
// subscription = LiveRecord.Model.all.Book.autoload({where: {is_enabled_eq: true}});
|
434
456
|
|
435
|
-
|
436
|
-
# app/models/book.rb
|
437
|
-
class Book < ApplicationRecord
|
438
|
-
include LiveRecord::Model::Callbacks
|
439
|
-
has_many :live_record_updates, as: :recordable, dependent: :destroy
|
457
|
+
// you may choose to combine both `where` and `reload` arguments described above
|
440
458
|
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
## ransack's methods like `ransackable_attributes` below will be invoked instead
|
448
|
-
# def self.live_record_queryable_attributes(book, current_user)
|
449
|
-
# [:id, :title, :is_enabled]
|
450
|
-
# end
|
451
|
-
|
452
|
-
private
|
459
|
+
// now, we can just simply add a "createOrUpdate" callback, to apply our own logic whenever a new/updated Book record is streamed from the backend
|
460
|
+
LiveRecord.Model.all.Book.addCallback('after:createOrUpdate', function() {
|
461
|
+
// let's say you have a code here that adds this new Book on the page
|
462
|
+
// `this` refers to the Book record that has been created/updated
|
463
|
+
console.log(this);
|
464
|
+
})
|
453
465
|
|
454
|
-
|
466
|
+
// you may also add callbacks specific to this `subscription`, as you may want to have multiple subscriptions. Then, see JS API `MODEL.autoload(CONFIG)` below for information
|
455
467
|
|
456
|
-
|
457
|
-
|
458
|
-
def self.ransackable_attributes(auth_object = nil)
|
459
|
-
column_names + _ransackers.keys
|
460
|
-
end
|
461
|
-
end
|
468
|
+
// you may also want to unsubscribe as you wish
|
469
|
+
LiveRecord.Model.all.Book.unsubscribe(subscription);
|
462
470
|
```
|
463
471
|
|
464
|
-
|
472
|
+
### Ransack Search Queries (Optional)
|
465
473
|
|
466
|
-
|
474
|
+
* If you need more complex queries to pass into the `.subscribe(where: { ... })` or `.autoload({where: {...}})` above, [ransack](https://github.com/activerecord-hackery/ransack) gem is supported.
|
475
|
+
* For example you can then do:
|
476
|
+
```js
|
477
|
+
// querying upon the `belongs_to :user`
|
478
|
+
subscription = LiveRecord.Model.all.Book.subscribe({where: {user_is_admin_eq: true, is_enabled_eq: true}});
|
467
479
|
|
468
|
-
|
469
|
-
|
470
|
-
rails generate migration add_created_at_index_to_MODELNAME
|
480
|
+
// or querying "OR" conditions
|
481
|
+
subscription = LiveRecord.Model.all.Book.subscribe({where: {title_eq: 'I am Batman', content_eq: 'I am Batman', m: 'or'}});
|
471
482
|
```
|
472
483
|
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
484
|
+
#### Model File (w/ Ransack) Example
|
485
|
+
|
486
|
+
```ruby
|
487
|
+
# app/models/book.rb
|
488
|
+
class Book < ApplicationRecord
|
489
|
+
include LiveRecord::Model::Callbacks
|
490
|
+
has_many :live_record_updates, as: :recordable, dependent: :destroy
|
491
|
+
|
492
|
+
def self.live_record_whitelisted_attributes(book, current_user)
|
493
|
+
[:id, :title, :is_enabled]
|
479
494
|
end
|
480
|
-
|
495
|
+
|
496
|
+
## this method will be invoked when `subscribe()` or `autoload()` is called
|
497
|
+
## but, you should not use this method when using `ransack` gem!
|
498
|
+
## ransack's methods like `ransackable_attributes` below will be invoked instead
|
499
|
+
# def self.live_record_queryable_attributes(book, current_user)
|
500
|
+
# [:id, :title, :is_enabled]
|
501
|
+
# end
|
502
|
+
|
503
|
+
private
|
504
|
+
|
505
|
+
# see ransack gem for more details regarding Authorization: https://github.com/activerecord-hackery/ransack#authorization-whitelistingblacklisting
|
506
|
+
|
507
|
+
# this method will be invoked when `subscribe()` or `autoload()` is called
|
508
|
+
# LiveRecord passes the `current_user` into `auth_object`, so you can access `current_user` inside below
|
509
|
+
def self.ransackable_attributes(auth_object = nil)
|
510
|
+
column_names + _ransackers.keys
|
511
|
+
end
|
512
|
+
end
|
513
|
+
```
|
514
|
+
|
515
|
+
### Reconnection Streaming For `subscribe()` (when client got disconnected)
|
516
|
+
|
517
|
+
* To be able to restream newly created records upon reconnection, the only requirement is that you should have a `created_at` attribute on your Models, which by default should already be there. However, to speed up queries, I highly suggest to add index on `created_at` with the following
|
518
|
+
|
519
|
+
```bash
|
520
|
+
# this will create a file under db/migrate folder, then edit that file (see the ruby code below)
|
521
|
+
rails generate migration add_created_at_index_to_MODELNAME
|
522
|
+
```
|
523
|
+
|
524
|
+
```ruby
|
525
|
+
# db/migrate/2017**********_add_created_at_index_to_MODELNAME.rb
|
526
|
+
class AddCreatedAtIndexToMODELNAME < ActiveRecord::Migration[5.0] # or 5.1, etc
|
527
|
+
def change
|
528
|
+
add_index :TABLENAME, :created_at
|
529
|
+
end
|
530
|
+
end
|
531
|
+
```
|
532
|
+
|
533
|
+
### Reconnection Streaming For `autoload()` (when client got disconnected)
|
534
|
+
|
535
|
+
* To be able to restream newly created/updated records upon reconnection, the only requirement is that you should have a `updated_at` attribute on your Models, which by default should already be there. However, to speed up queries, I highly suggest to add index on `updated_at` with the following
|
536
|
+
|
537
|
+
```bash
|
538
|
+
# this will create a file under db/migrate folder, then edit that file (see the ruby code below)
|
539
|
+
rails generate migration add_updated_at_index_to_MODELNAME
|
540
|
+
```
|
541
|
+
|
542
|
+
```ruby
|
543
|
+
# db/migrate/2017**********_add_created_at_index_to_MODELNAME.rb
|
544
|
+
class AddUpdatedAtIndexToMODELNAME < ActiveRecord::Migration[5.0] # or 5.1, etc
|
545
|
+
def change
|
546
|
+
add_index :TABLENAME, :updated_at
|
547
|
+
end
|
548
|
+
end
|
549
|
+
```
|
481
550
|
|
482
551
|
## Plugins
|
483
552
|
|
@@ -541,9 +610,6 @@
|
|
541
610
|
* `hasMany` and `belongsTo` `modelName` above should be a valid defined `LiveRecord.Model`
|
542
611
|
* returns the newly created `MODEL`
|
543
612
|
|
544
|
-
### `MODEL.all`
|
545
|
-
* Object of which properties are IDs of the records
|
546
|
-
|
547
613
|
### `MODEL.subscribe(CONFIG)`
|
548
614
|
* `CONFIG` (Object, Optional)
|
549
615
|
* `reload`: (Boolean, Default: false)
|
@@ -554,6 +620,7 @@
|
|
554
620
|
* `on:disconnect`: (function Object)
|
555
621
|
* `before:create`: (function Object; function argument = record)
|
556
622
|
* `after:create`: (function Object; function argument = record)
|
623
|
+
* returns an ActionCable subscription object
|
557
624
|
* subscribes to the `LiveRecord::PublicationsChannel`, which then automatically receives new records from the backend.
|
558
625
|
* when `reload: true`, all records (subject to `where` condition above) are immediately loaded, and not just the new ones.
|
559
626
|
* 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.
|
@@ -572,12 +639,35 @@
|
|
572
639
|
* `gteq` greater than or equal to; i.e. `created_at_gteq: '2017-12-291T13:47:59.238Z'`
|
573
640
|
* `in` in Array; i.e. `id_in: [2, 56, 19, 68]`
|
574
641
|
* `not_in` in Array; i.e. `id_not_in: [2, 56, 19, 68]`
|
575
|
-
* `matches` matches using SQL `LIKE`; i.e. `
|
576
|
-
* `does_not_match` does not match using SQL `NOT LIKE`; i.e. `
|
642
|
+
* `matches` matches using SQL `LIKE`; i.e. `title_matches: '%Harry Potter%'`
|
643
|
+
* `does_not_match` does not match using SQL `NOT LIKE`; i.e. `title_does_not_match: '%Harry Potter%'`
|
644
|
+
|
645
|
+
### `MODEL.autoload(CONFIG)`
|
646
|
+
* `CONFIG` (Object, Optional)
|
647
|
+
* `reload`: (Boolean, Default: false)
|
648
|
+
* `where`: (Object)
|
649
|
+
* `ATTRIBUTENAME_OPERATOR`: (Any Type)
|
650
|
+
* `callbacks`: (Object)
|
651
|
+
* `on:connect`: (function Object)
|
652
|
+
* `on:disconnect`: (function Object)
|
653
|
+
* `before:createOrUpdate`: (function Object; function argument = record)
|
654
|
+
* `after:createOrUpdate`: (function Object; function argument = record)
|
655
|
+
* returns an ActionCable subscription object
|
656
|
+
* subscribes to the `LiveRecord::AutoloadsChannel`, which then automatically receives new/updated records from the backend.
|
657
|
+
* when `reload: true`, all records (subject to `where` condition above) are immediately loaded, and not just the future new/updated ones.
|
658
|
+
* 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.
|
659
|
+
* autoload **creates** a record instance to the JS-store if the record does not yet exist, while it just **updates** the record instance if already in the store.
|
660
|
+
* `ATTRIBUTENAME_OPERATOR` means something like (for example): `is_enabled_eq`, where `is_enabled` is the `ATTRIBUTENAME` and `eq` is the `OPERATOR`.
|
661
|
+
* you can have as many `ATTRIBUTENAME_OPERATOR` as you like, but keep in mind that the logic applied to them is "AND", and not "OR". For "OR" conditions, use `ransack`
|
662
|
+
|
663
|
+
> Supported query operators are the same as [`subscribe()` above](#list-of-default-supported-query-operators).
|
577
664
|
|
578
665
|
### `MODEL.unsubscribe(SUBSCRIPTION)`
|
579
666
|
* unsubscribes to the `LiveRecord::PublicationsChannel`, thereby will not be receiving new records anymore.
|
580
667
|
|
668
|
+
### `MODEL.all`
|
669
|
+
* Object of which properties are IDs of the records
|
670
|
+
|
581
671
|
### `new LiveRecord.Model.all.MODELNAME(ATTRIBUTES)`
|
582
672
|
* `ATTRIBUTES` (Object)
|
583
673
|
* returns a `MODELINSTANCE` of the the Model having `ATTRIBUTES` attributes
|
@@ -599,7 +689,7 @@
|
|
599
689
|
### `MODELINSTANCE.subscribe(config)`
|
600
690
|
* `CONFIG` (Object, Optional)
|
601
691
|
* `reload`: (Boolean, Default: false)
|
602
|
-
* subscribes to the `LiveRecord::ChangesChannel`. This instance should already be subscribed by default after being stored, unless there is a `on:
|
692
|
+
* subscribes to the `LiveRecord::ChangesChannel`. This instance should already be subscribed by default after being stored, unless there is a `on:responseError` or manually `unsubscribed()` which then you should manually call this `subscribe()` function after correctly handling the response error, or whenever desired.
|
603
693
|
* when `reload: true`, the record is forced reloaded to make sure all attributes are in-sync
|
604
694
|
* returns the `subscription` object (the ActionCable subscription object itself)
|
605
695
|
|
@@ -652,6 +742,8 @@
|
|
652
742
|
## TODOs
|
653
743
|
* Change `feature` specs into `system` specs after [this rspec-rails pull request](https://github.com/rspec/rspec-rails/pull/1813) gets merged.
|
654
744
|
|
745
|
+
* Rename `subscribe()` and `autoload()` into something more descriptive and intuitive, as both of them actually "subscribes". Perhaps rename `subscribe()` into `subscribe({to: 'create'})` and rename `autoload()` into `subscribe({to: 'createOrUpdate'})`?
|
746
|
+
|
655
747
|
## Contributing
|
656
748
|
* pull requests and forks are very much welcomed! :) Let me know if you find any bug! Thanks
|
657
749
|
|
@@ -659,6 +751,9 @@
|
|
659
751
|
* MIT
|
660
752
|
|
661
753
|
## Changelog
|
754
|
+
* 0.3.0
|
755
|
+
* Ability to now auto-load **created** or **updated** records that match your specified "where" condition.
|
756
|
+
* See [Setup #10 above](#setup)
|
662
757
|
* 0.2.8
|
663
758
|
* You can now specify `:id` into `live_record_whitelisted_attributes` for verbosity; used to be automatically-included by default. Needed to do this otherwise there was this minor bug where `subscribe()` still receives records (having just `:id` attribute though) even when it is specified to be not-authorized.
|
664
759
|
* fixed minor bug when `live_record_whitelisted_attributes` is not returning anything, throwing a `NoMethodError`
|
@@ -62,6 +62,84 @@ LiveRecord.Model.create = (config) ->
|
|
62
62
|
|
63
63
|
Model.subscriptions = []
|
64
64
|
|
65
|
+
Model.autoload = (config = {}) ->
|
66
|
+
config.callbacks ||= {}
|
67
|
+
config.reload ||= false
|
68
|
+
|
69
|
+
subscription = App.cable.subscriptions.create(
|
70
|
+
{
|
71
|
+
channel: 'LiveRecord::AutoloadsChannel'
|
72
|
+
model_name: Model.modelName
|
73
|
+
where: config.where
|
74
|
+
},
|
75
|
+
|
76
|
+
connected: ->
|
77
|
+
# if forced reload of all records after subscribing, reload only once at the very start of connection, and no longer when reconnecting
|
78
|
+
if config.reload
|
79
|
+
config.reload = false
|
80
|
+
@syncRecords()
|
81
|
+
|
82
|
+
if @liveRecord._staleSince != undefined
|
83
|
+
@syncRecords()
|
84
|
+
|
85
|
+
config.callbacks['on:connect'].call(this) if config.callbacks['on:connect']
|
86
|
+
|
87
|
+
disconnected: ->
|
88
|
+
@liveRecord._staleSince = (new Date()).toISOString() unless @liveRecord._staleSince
|
89
|
+
config.callbacks['on:disconnect'].call(this) if config.callbacks['on:disconnect']
|
90
|
+
|
91
|
+
received: (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)
|
97
|
+
|
98
|
+
onAction:
|
99
|
+
create_or_update: (data) ->
|
100
|
+
record = Model.all[data.attributes.id]
|
101
|
+
|
102
|
+
# if record already exists
|
103
|
+
if record
|
104
|
+
doesRecordAlreadyExist = true
|
105
|
+
# else if not
|
106
|
+
else
|
107
|
+
record = new Model(data.attributes)
|
108
|
+
doesRecordAlreadyExist = false
|
109
|
+
|
110
|
+
config.callbacks['before:create_or_update'].call(this, record) if config.callbacks['before:create_or_update']
|
111
|
+
if doesRecordAlreadyExist
|
112
|
+
record.update(data.attributes)
|
113
|
+
else
|
114
|
+
record.create()
|
115
|
+
console.log(record)
|
116
|
+
config.callbacks['after:create_or_update'].call(this, record) if config.callbacks['after:create_or_update']
|
117
|
+
|
118
|
+
# handler for received() callback above
|
119
|
+
onError:
|
120
|
+
forbidden: (data) ->
|
121
|
+
console.error('[LiveRecord Response Error]', data.error.code, ':', data.error.message, 'for', this)
|
122
|
+
bad_request: (data) ->
|
123
|
+
console.error('[LiveRecord Response Error]', data.error.code, ':', data.error.message, 'for', this)
|
124
|
+
|
125
|
+
syncRecords: ->
|
126
|
+
@perform(
|
127
|
+
'sync_records',
|
128
|
+
model_name: Model.modelName,
|
129
|
+
where: config.where,
|
130
|
+
stale_since: @liveRecord._staleSince
|
131
|
+
)
|
132
|
+
@liveRecord._staleSince = undefined
|
133
|
+
)
|
134
|
+
|
135
|
+
subscription.liveRecord = {}
|
136
|
+
subscription.liveRecord.modelName = Model.modelName
|
137
|
+
subscription.liveRecord.where = config.where
|
138
|
+
subscription.liveRecord.callbacks = config.callbacks
|
139
|
+
|
140
|
+
@subscriptions.push(subscription)
|
141
|
+
subscription
|
142
|
+
|
65
143
|
Model.subscribe = (config = {}) ->
|
66
144
|
config.callbacks ||= {}
|
67
145
|
config.reload ||= false
|
@@ -0,0 +1,92 @@
|
|
1
|
+
class LiveRecord::AutoloadsChannel < LiveRecord::BaseChannel
|
2
|
+
def subscribed
|
3
|
+
stream_from 'live_record:autoloads', coder: ActiveSupport::JSON do |message|
|
4
|
+
new_or_updated_record = message['model_name'].safe_constantize.find_by(id: message['record_id']) || break
|
5
|
+
new_or_changed_attributes = message.fetch('attributes')
|
6
|
+
|
7
|
+
is_autoload_dependent_to_new_or_changed_attributes = false
|
8
|
+
|
9
|
+
conditions_hash = params[:where].to_h
|
10
|
+
|
11
|
+
# if no conditions supplied to autoload(), then it means every attribute is dependent
|
12
|
+
if conditions_hash.empty?
|
13
|
+
is_autoload_dependent_to_new_or_changed_attributes = true
|
14
|
+
else
|
15
|
+
@autoload_dependent_attributes ||= [].tap do |autoload_dependent_attributes|
|
16
|
+
conditions_hash.each do |key, value|
|
17
|
+
operator = key.split('_').last
|
18
|
+
# to get attribute_name, we subtract the end part of the string with size of operator substring; i.e.: created_at_lteq -> created_at
|
19
|
+
attribute_name = key[0..(-1 - operator.size - 1)]
|
20
|
+
|
21
|
+
autoload_dependent_attributes << attribute_name
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
@autoload_dependent_attributes.each do |autoload_dependent_attribute|
|
26
|
+
if new_or_changed_attributes.include? autoload_dependent_attribute
|
27
|
+
is_autoload_dependent_to_new_or_changed_attributes = true
|
28
|
+
break
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# now if this autoload subscription is indeed dependent on the attributes,
|
34
|
+
# then we now check if the where condition values actually match
|
35
|
+
# and then transmit data if authorised attributes match condition values
|
36
|
+
if is_autoload_dependent_to_new_or_changed_attributes
|
37
|
+
model_class = params[:model_name].safe_constantize
|
38
|
+
|
39
|
+
active_record_relation = LiveRecord::BaseChannel::SearchAdapters.mapped_active_record_relation(
|
40
|
+
model_class: model_class,
|
41
|
+
conditions_hash: conditions_hash,
|
42
|
+
current_user: current_user,
|
43
|
+
)
|
44
|
+
|
45
|
+
if active_record_relation.exists?(id: new_or_updated_record.id)
|
46
|
+
whitelisted_attributes = LiveRecord::BaseChannel::Helpers.whitelisted_attributes(new_or_updated_record, current_user)
|
47
|
+
|
48
|
+
if whitelisted_attributes.size > 0
|
49
|
+
message = { 'action' => 'create_or_update', 'attributes' => new_or_updated_record.attributes }
|
50
|
+
response = filtered_message(message, whitelisted_attributes)
|
51
|
+
transmit response if response.present?
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def sync_records(data)
|
59
|
+
params = data.symbolize_keys
|
60
|
+
model_class = params[:model_name].safe_constantize
|
61
|
+
|
62
|
+
if model_class && model_class < ApplicationRecord
|
63
|
+
|
64
|
+
active_record_relation = LiveRecord::BaseChannel::SearchAdapters.mapped_active_record_relation(
|
65
|
+
model_class: model_class,
|
66
|
+
conditions_hash: params[:where].to_h,
|
67
|
+
current_user: current_user,
|
68
|
+
)
|
69
|
+
|
70
|
+
if params[:stale_since].present?
|
71
|
+
active_record_relation = active_record_relation.where(
|
72
|
+
'updated_at >= ?', DateTime.parse(params[:stale_since]) - LiveRecord.configuration.sync_record_buffer_time
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
76
|
+
# we `transmmit` a message back to client for each matching record
|
77
|
+
active_record_relation.find_each do |record|
|
78
|
+
# but first, check for the authorised attributes, if exists
|
79
|
+
whitelisted_attributes = LiveRecord::BaseChannel::Helpers.whitelisted_attributes(record, current_user)
|
80
|
+
|
81
|
+
if whitelisted_attributes.size > 0
|
82
|
+
message = { 'action' => 'create_or_update', 'attributes' => record.attributes }
|
83
|
+
response = filtered_message(message, whitelisted_attributes)
|
84
|
+
transmit response if response.present?
|
85
|
+
end
|
86
|
+
end
|
87
|
+
else
|
88
|
+
respond_with_error(:bad_request, 'Not a correct model name')
|
89
|
+
reject_subscription
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|