active-sync 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +29 -4
- data/app/channels/models_channel.rb +27 -0
- data/app/controllers/active_sync/application_controller.rb +1 -1
- data/app/controllers/active_sync/models_controller.rb +14 -2
- data/app/jobs/broadcast_change_job.rb +9 -0
- data/app/models/active_sync/broadcaster.rb +5 -0
- data/app/models/active_sync/model.rb +63 -0
- data/app/models/active_sync/sync.rb +28 -108
- data/config/routes.rb +2 -1
- data/lib/active_sync/engine.rb +0 -1
- data/lib/active_sync/version.rb +1 -1
- data/lib/javascript/active-sync.js +76 -49
- data/lib/javascript/model.js +108 -43
- data/lib/javascript/package.json +3 -2
- data/lib/javascript/util.js +10 -0
- metadata +34 -19
- data/app/channels/active_sync_channel.rb +0 -89
- data/app/models/active_sync/active_record_extension.rb +0 -110
- data/lib/javascript/records.js +0 -216
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 7df1bee7e9b73969455663f528ffe6c0e6bdcf29c51da5fcfe3198435e9c90aa
|
4
|
+
data.tar.gz: d1e8df90405eab22953da90829644acac6c7f3e3003df836d8e32a12f7fb43cf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c31d625e52ef4596916acaa468916c7ea767194a77049026ae1ebfc539ae7c2ff5e5ab05a241cb8ae643c9d376b2da49da5a389780c8ab491379d51d1d6f9609
|
7
|
+
data.tar.gz: 072dcb445cb5288c65cf073bb9dae6a152f393fba25e69031c0e8b7e144468bba4f612e5f2e038c27d5d1318856f4e263481eae2bafe0b0204f25daffaa721b9
|
data/README.md
CHANGED
@@ -1,10 +1,20 @@
|
|
1
1
|
# ActiveSync
|
2
|
-
|
2
|
+
Dynamically created and updated front end models.
|
3
|
+
|
4
|
+
Active record adds an active record interface to your front end. Configured models are available
|
5
|
+
within your Javascript application with many active record type functions such as 'where'.
|
6
|
+
|
7
|
+
Records are lazy loaded and then dynamically updated through actioncable. So any records looked up
|
8
|
+
through ActiveSync will be dynamically updated whether updated by the current user or any one else.
|
9
|
+
|
10
|
+
Currently the only Javascript framework supported is Vue
|
3
11
|
|
4
12
|
## Usage
|
5
13
|
How to use my plugin.
|
6
14
|
|
7
15
|
## Installation
|
16
|
+
|
17
|
+
### Install gem
|
8
18
|
Add this line to your application's Gemfile:
|
9
19
|
|
10
20
|
```ruby
|
@@ -16,11 +26,26 @@ And then execute:
|
|
16
26
|
$ bundle
|
17
27
|
```
|
18
28
|
|
19
|
-
|
20
|
-
|
21
|
-
|
29
|
+
### Import package
|
30
|
+
In packs/application.js import active-sync and create an instance with a list of Models that you
|
31
|
+
want available.
|
32
|
+
|
33
|
+
```javascript
|
34
|
+
import ActiveSync from 'active-sync'
|
35
|
+
|
36
|
+
let activeSync = new ActiveSync({ modelNames: ['Customer', 'Site'] })
|
37
|
+
```
|
38
|
+
|
39
|
+
### Vue setup
|
40
|
+
|
41
|
+
Before creating your Vue instance :
|
42
|
+
|
43
|
+
```javascript
|
44
|
+
Vue.use( activeSync )
|
22
45
|
```
|
23
46
|
|
47
|
+
Then any new Vue instances will have Models globally available
|
48
|
+
|
24
49
|
## Contributing
|
25
50
|
Contribution directions go here.
|
26
51
|
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# Rails currently doesn't allow namespacing channels in an engine
|
2
|
+
# module ActiveSync
|
3
|
+
class ModelsChannel < ActionCable::Channel::Base
|
4
|
+
# To change the data sent implement sync_record in the respective model
|
5
|
+
|
6
|
+
def subscribed
|
7
|
+
transmit(subscription_model.where(params[:filter]).map(&:sync_record))
|
8
|
+
stream_from params[:model], coder: ActiveSupport::JSON do |message|
|
9
|
+
if (params[:filter].to_a - message.to_a).blank?
|
10
|
+
transmit([message])
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def unsubscribed
|
16
|
+
# Any cleanup needed when channel is unsubscribed
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def subscription_model
|
22
|
+
model = params[:model].singularize.camelize.constantize
|
23
|
+
raise "Model '#{params[:model]}' is not set up for syncing model" unless model.sync_model?
|
24
|
+
model
|
25
|
+
end
|
26
|
+
end
|
27
|
+
# end
|
@@ -1,10 +1,22 @@
|
|
1
1
|
module ActiveSync
|
2
2
|
class ModelsController < ApplicationController
|
3
3
|
|
4
|
-
def
|
4
|
+
def update
|
5
|
+
#TODO some oversite on what can be edited for sync records
|
6
|
+
model.find(params[:id]).update(params.permit(model.sync_attributes))
|
7
|
+
head :no_content
|
8
|
+
end
|
5
9
|
|
6
|
-
|
10
|
+
def create
|
11
|
+
#TODO some oversite on what can be created for sync records
|
12
|
+
render json: model.create(params.permit(model.sync_attributes)).id
|
13
|
+
end
|
7
14
|
|
15
|
+
private
|
16
|
+
def model
|
17
|
+
m = params[:model].singularize.camelize.safe_constantize || params[:model].camelize.safe_constantize
|
18
|
+
raise "Cannot edit #{params[:model]} as it is not a sync model" unless m.sync_model?
|
19
|
+
m
|
8
20
|
end
|
9
21
|
end
|
10
22
|
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module ActiveSync
|
2
|
+
module Model
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
# after_update :sync_update
|
7
|
+
after_commit :sync_change
|
8
|
+
end
|
9
|
+
|
10
|
+
def sync_change
|
11
|
+
if sync_model?
|
12
|
+
BroadcastChangeJob.perform_later(self)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def sync_model?
|
17
|
+
true
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
class_methods do
|
22
|
+
|
23
|
+
def sync_model?
|
24
|
+
true
|
25
|
+
end
|
26
|
+
|
27
|
+
# #sync sets the #sync_record method that renders the hash to create the JSON object that is broadcast and sets
|
28
|
+
# #sync_associations which returns a list of associations that are permitted to be broadcast for this model.
|
29
|
+
# define these methods directly in your model if the record sent to the font end needs to be different to what's
|
30
|
+
# available with the below configurations
|
31
|
+
#
|
32
|
+
# This can be passed the following options:
|
33
|
+
#
|
34
|
+
# Example use in Model:
|
35
|
+
# sync :all_attributes, associations: [ :sites ]
|
36
|
+
|
37
|
+
# ATTRIBUTE OPTIONS
|
38
|
+
# Attributes are data that is sent in the actual sync data (this will always include the ID)
|
39
|
+
|
40
|
+
# :all_attributes - sync data will have all attributes
|
41
|
+
# :attributes - an array of symbols that will be called on the record and sent as attributes
|
42
|
+
|
43
|
+
# ASSOCIATION OPTIONS
|
44
|
+
# Associations are lazy loaded, data will not go with the record but if the front end has the association described
|
45
|
+
# then records can be subscribed to through the association.
|
46
|
+
|
47
|
+
# :all_associations - sync data will be associated
|
48
|
+
# :associations - an array of symbols
|
49
|
+
|
50
|
+
def sync *attributes
|
51
|
+
self.class.define_method(:sync_attributes) do
|
52
|
+
ActiveSync::Sync.sync_attributes(self, attributes)
|
53
|
+
end
|
54
|
+
define_method(:sync_record) do
|
55
|
+
ActiveSync::Sync.sync_record(self, attributes)
|
56
|
+
end
|
57
|
+
define_method(:sync_associations) do
|
58
|
+
ActiveSync::Sync.sync_associations(self, attributes)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -1,125 +1,45 @@
|
|
1
1
|
module ActiveSync
|
2
2
|
class Sync
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
@@model_descriptions
|
12
|
-
end
|
13
|
-
|
14
|
-
def self.is_sync_model? model
|
15
|
-
|
16
|
-
model_name = model.class == String ? model : model.name
|
17
|
-
|
18
|
-
model_descriptions.keys.include? model_name
|
19
|
-
|
20
|
-
end
|
21
|
-
|
22
|
-
|
23
|
-
#Hash used in all general sync communication for a given model.
|
24
|
-
def self.sync_record record
|
25
|
-
@@model_descriptions[ record.class.name ][ :attributes ].reduce( {} ) do | hash, attribute |
|
26
|
-
hash[ attribute ] = record.send( attribute )
|
27
|
-
hash
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
def self.configure_model_description model, options
|
32
|
-
|
33
|
-
@@model_descriptions[ model.name ] = {
|
34
|
-
attributes: [],
|
35
|
-
associations: []
|
36
|
-
}
|
37
|
-
|
38
|
-
options.each do | option |
|
39
|
-
|
40
|
-
case
|
41
|
-
when option == :all_attributes_and_associations
|
42
|
-
|
43
|
-
add_attributes_to_description model, model.attribute_names
|
44
|
-
add_associations_to_description model, model.reflect_on_all_associations.map( &:name )
|
45
|
-
|
46
|
-
when option == :all_attributes
|
47
|
-
|
48
|
-
add_attributes_to_description model, model.attribute_names
|
49
|
-
|
50
|
-
when first_key( option ) == :attributes
|
51
|
-
|
52
|
-
add_attributes_to_description model, option[:attributes].map(&:to_s)
|
53
|
-
|
54
|
-
when option == :all_associations
|
55
|
-
|
56
|
-
add_associations_to_description model, model.reflect_on_all_associations.map( &:name )
|
57
|
-
|
58
|
-
when first_key( option ) == :associations
|
59
|
-
|
60
|
-
add_associations_to_description model, option[ :associations ]
|
61
|
-
|
4
|
+
def self.sync_attributes model, args
|
5
|
+
@@sync_attributes ||= args.reduce([]) do |array, option|
|
6
|
+
case option
|
7
|
+
when :all_attributes_and_associations, :all_attributes
|
8
|
+
array + model.column_names.map(&:to_sym)
|
9
|
+
when ->(option){ option.is_a?(Hash) }
|
10
|
+
array + option[:attributes]
|
62
11
|
else
|
63
|
-
|
64
|
-
throw "Unknown sync option: #{option}"
|
65
|
-
|
12
|
+
raise "Unknown sync record option #{option.inspect}"
|
66
13
|
end
|
67
14
|
end
|
68
15
|
end
|
69
16
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
association = model.reflect_on_all_associations.find{ |a| a.name == association_name }
|
80
|
-
|
81
|
-
unless association.nil?
|
82
|
-
begin
|
83
|
-
|
84
|
-
@@model_descriptions[ model.name ][ :associations ] << { name: association.name.to_s, class: association.class_name, type: association.association_class.name }
|
85
|
-
|
86
|
-
rescue NotImplementedError
|
87
|
-
|
88
|
-
@@model_descriptions[ model.name ][ :associations ] << { name: association.name.to_s, class: association.class_name, type: 'ActiveRecord::Associations::HasManyThroughAssociation' }
|
89
|
-
|
90
|
-
end
|
91
|
-
|
17
|
+
#Hash used in all general sync communication for a given model.
|
18
|
+
def self.sync_record record, args
|
19
|
+
args.reduce({}) do |hash, option|
|
20
|
+
case option
|
21
|
+
when :all_attributes_and_associations, :all_attributes
|
22
|
+
hash.merge(record.attributes)
|
23
|
+
when ->(option){ option.is_a?(Hash) }
|
24
|
+
option[:attributes]&.reduce(hash) { |h, attr| h[attr] = record.call(attr) }
|
92
25
|
else
|
93
|
-
|
94
|
-
throw "Association #{ association_name } not found for #{ model.name }"
|
95
|
-
|
26
|
+
raise "Unknown sync record option #{option.inspect}"
|
96
27
|
end
|
97
28
|
end
|
98
29
|
end
|
99
30
|
|
100
|
-
def self.
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
record.send( model_association[:name] ).pluck(:id)
|
112
|
-
else
|
113
|
-
record.send( model_association[:name] ).id
|
31
|
+
def self.sync_associations record, args
|
32
|
+
args.reduce([]) do |associations, option|
|
33
|
+
case option
|
34
|
+
when :all_attributes_and_associations, :all_attributes
|
35
|
+
associations + record.class.reflect_on_all_associations.map { |a| a.name }
|
36
|
+
when ->(option){ option.is_a?(Hash) }
|
37
|
+
associations + option[:associations]
|
38
|
+
else
|
39
|
+
raise "Unknown sync associations option #{option.inspect}"
|
40
|
+
end
|
41
|
+
associations
|
114
42
|
end
|
115
43
|
end
|
116
|
-
|
117
|
-
def self.get_model_association model, association_name
|
118
|
-
@@model_descriptions[ model.name ][:associations].find{ |a| a[:name] == association_name }
|
119
|
-
end
|
120
|
-
|
121
|
-
def self.first_key obj
|
122
|
-
obj.respond_to?( :keys ) ? obj.keys.first : nil
|
123
|
-
end
|
124
44
|
end
|
125
45
|
end
|
data/config/routes.rb
CHANGED
data/lib/active_sync/engine.rb
CHANGED
data/lib/active_sync/version.rb
CHANGED
@@ -1,68 +1,95 @@
|
|
1
|
-
import ActionCable from 'actioncable'
|
2
1
|
import Model from './model.js'
|
2
|
+
import CamelCase from 'camelcase'
|
3
|
+
import Pluralize from 'pluralize'
|
4
|
+
import SnakeCase from "snake-case";
|
3
5
|
|
4
6
|
export default class ActiveSync {
|
5
|
-
constructor( args ){
|
6
|
-
|
7
|
-
this._cable = ActionCable.createConsumer()
|
8
|
-
this._models = []
|
9
|
-
this._customModels = args.customModels || []
|
10
7
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
8
|
+
static models = []
|
9
|
+
|
10
|
+
// ActiveSync dynamically creates model classes when a new instance of it is created
|
11
|
+
// Valid arguments are:
|
12
|
+
// models: Used to define custom model. Pass an array of classes in and they will be set up as ActiveSync models.
|
13
|
+
// It's expected that models passed in extend ActiveSync's Model class. They will then receive extra functions
|
14
|
+
// as per anything defined in the modelDescriptions argument (such as associations).
|
15
|
+
//
|
16
|
+
// modelDescriptions: An object that describes all the models for ActiveSync to create. If the model is not defined in
|
17
|
+
// the model then an empty model is created. Then all the described associations are added to their
|
18
|
+
// respective models.
|
19
|
+
// IE { Customer: { hasMany: ['sites']}, Site: { belongsTo: 'customer' }
|
20
|
+
// Will add a 'sites' method to the Customer class and a 'customer' method to Site.
|
21
|
+
//
|
15
22
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
this.setupModel( modelDescriptions[modelName] )
|
21
|
-
})
|
23
|
+
constructor( args ){
|
24
|
+
this._models = args.models || [];
|
25
|
+
this.buildModels(args.modelDescriptions)
|
26
|
+
}
|
22
27
|
|
23
|
-
|
24
|
-
|
28
|
+
install( vue ){
|
29
|
+
this._models.forEach((model)=> vue.prototype["$" + model.className] = model)
|
30
|
+
}
|
25
31
|
|
32
|
+
models(){
|
33
|
+
return this._models
|
26
34
|
}
|
27
35
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
if( records[ id ] ){
|
32
|
-
Object.keys( record ).forEach( (key) => Vue.set( records[ id ], key, record[key] ) )
|
33
|
-
} else {
|
34
|
-
Vue.set( records, id, record )
|
35
|
-
}
|
36
|
-
},
|
36
|
+
// Creates the models from the modelDescription arg passed in to the constructor
|
37
|
+
buildModels(modelDescriptions){
|
38
|
+
let modelNames = Object.keys(modelDescriptions || {});
|
37
39
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
40
|
+
modelNames.forEach((modelName) => {
|
41
|
+
if(!this._models.find((model) => model.className === modelName)){
|
42
|
+
this._models.push(this.createModel(modelName))
|
43
|
+
}
|
44
|
+
})
|
43
45
|
|
44
|
-
|
46
|
+
this._models.forEach((model) => {
|
47
|
+
this.addBelongsToModel(modelDescriptions[model.className], model)
|
48
|
+
this.addHasManyToModel(modelDescriptions[model.className], model)
|
45
49
|
})
|
46
50
|
}
|
47
51
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
xmlHttp.send( null )
|
52
|
-
return JSON.parse(xmlHttp.responseText)
|
52
|
+
createModel(name){
|
53
|
+
let modelClass = Model
|
54
|
+
return eval(`(class ${name} extends modelClass { static className = '${name}' })`)
|
53
55
|
}
|
56
|
+
|
57
|
+
addBelongsToModel(modelDescription, model){
|
58
|
+
((modelDescription || {}).belongsTo || []).forEach((association) => {
|
59
|
+
let associatedModel = this._models.find((model) => model.className === association)
|
60
|
+
model[association] = function () {
|
61
|
+
return associatedModel.find(this[association + '_id'])
|
62
|
+
}
|
63
|
+
});
|
64
|
+
}
|
65
|
+
|
66
|
+
addHasManyToModel(modelDescription, model){
|
67
|
+
((modelDescription || {}).hasMany || []).forEach((association) => {
|
68
|
+
let associatedModel = this._models.find((model) => model.className === CamelCase(Pluralize.singular(association), {pascalCase: true}))
|
69
|
+
model.prototype[association] = function () {
|
70
|
+
let associationQuery = {}
|
71
|
+
associationQuery[SnakeCase(model.className) + '_id'] = this.id
|
72
|
+
return associatedModel.where(associationQuery)
|
73
|
+
}
|
74
|
+
});
|
75
|
+
}
|
76
|
+
}
|
54
77
|
|
55
|
-
setupModel( modelDescription ){
|
56
|
-
var CustomModel = this._customModels.find(( m ) => m.name == modelDescription.name )
|
57
|
-
|
58
|
-
if( CustomModel ){
|
59
|
-
|
60
|
-
this._models.push( new CustomModel( modelDescription, this._cable, this._modelOptions ) )
|
61
78
|
|
62
|
-
} else {
|
63
79
|
|
64
|
-
|
80
|
+
// Code for dynamically requesting Model names and associations.
|
81
|
+
//
|
82
|
+
// Object.keys( modelDescriptions ).forEach( ( modelName ) =>{
|
83
|
+
// modelDescriptions[modelName].name = modelName
|
84
|
+
// this.setupModel( modelDescriptions[modelName] )
|
85
|
+
// })
|
65
86
|
|
66
|
-
|
67
|
-
|
68
|
-
|
87
|
+
// var modelDescriptions = this.requestModelDescriptions()
|
88
|
+
// this._models.forEach( ( model ) => model.setAssociatedModels( this._models))
|
89
|
+
// args.afterSetup( this._models )
|
90
|
+
// requestModelDescriptions(){
|
91
|
+
// var xmlHttp = new XMLHttpRequest()
|
92
|
+
// xmlHttp.open( "GET", 'active_sync/models', false ) // false for synchronous request
|
93
|
+
// xmlHttp.send( null )
|
94
|
+
// return JSON.parse(xmlHttp.responseText)
|
95
|
+
// }
|
data/lib/javascript/model.js
CHANGED
@@ -1,63 +1,128 @@
|
|
1
|
-
import
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
this._name = description.name
|
6
|
-
this._afterFind = options.afterFind || (( record ) => {})
|
7
|
-
this._records = new Records( {
|
8
|
-
cable: cable,
|
9
|
-
modelName: description.name,
|
10
|
-
addNewRecord: options.addNewRecord,
|
11
|
-
references: new Records( {
|
12
|
-
cable: cable,
|
13
|
-
addNewRecord: options.addNewRecord
|
14
|
-
}),
|
15
|
-
associations: description.associations
|
16
|
-
})
|
17
|
-
}
|
1
|
+
import Axios from 'axios'
|
2
|
+
import SnakeCase from 'snake-case'
|
3
|
+
import CamelCase from 'camelcase'
|
4
|
+
import Pluralize from 'pluralize'
|
18
5
|
|
19
|
-
|
20
|
-
|
21
|
-
|
6
|
+
import Actioncable from "actioncable"
|
7
|
+
|
8
|
+
import Util from './util'
|
22
9
|
|
23
|
-
|
24
|
-
|
10
|
+
export default class Model {
|
11
|
+
|
12
|
+
// static records = {}
|
13
|
+
static recordsLoaded = false
|
14
|
+
static urlPathBase = 'active_sync/'
|
15
|
+
|
16
|
+
static consumer = Actioncable.createConsumer()
|
17
|
+
|
18
|
+
constructor(args){
|
19
|
+
if(!args.id) throw 'Can not create record without an id'
|
20
|
+
if(this.constructor.records[args.id]){
|
21
|
+
this.constructor.records[args.id].setProperties(args)
|
22
|
+
} else {
|
23
|
+
this.setProperties(args)
|
24
|
+
this.constructor.records[this.id] = this
|
25
|
+
}
|
25
26
|
}
|
26
27
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
this._records.forEach( ( record ) => {
|
31
|
-
allRecords.push( this._records.getRecord( record.id ) )
|
32
|
-
})
|
28
|
+
setProperties(args){
|
29
|
+
Object.keys(args).forEach((property)=>{
|
30
|
+
this[property] = args[property]
|
33
31
|
})
|
34
|
-
|
35
|
-
return allRecords
|
36
32
|
}
|
37
33
|
|
38
|
-
|
34
|
+
static get records(){
|
35
|
+
if(!this.recordsObject) this.recordsObject = {}
|
36
|
+
return this.recordsObject
|
37
|
+
}
|
39
38
|
|
40
|
-
|
39
|
+
static get all(){
|
40
|
+
return this.loadOrSearch()
|
41
|
+
}
|
41
42
|
|
42
|
-
|
43
|
-
|
44
|
-
|
43
|
+
static modelUrlPath(singular = false){
|
44
|
+
if(singular) {
|
45
|
+
return this.urlPathBase + SnakeCase(this.className)
|
46
|
+
} else {
|
47
|
+
return this.urlPathBase + SnakeCase(Pluralize(this.className))
|
45
48
|
}
|
49
|
+
}
|
46
50
|
|
51
|
+
static find(id){
|
52
|
+
return new Promise((resolve,reject)=>{
|
53
|
+
if(!this.records[id]){
|
54
|
+
resolve(this.loadRecords({ id: id }).then(()=> this.records[id]))
|
55
|
+
} else {
|
56
|
+
resolve(this.records[id])
|
57
|
+
}
|
58
|
+
})
|
59
|
+
}
|
60
|
+
|
61
|
+
static where(args){
|
62
|
+
return this.loadOrSearch(Util.snakeCaseKeys(args))
|
63
|
+
}
|
47
64
|
|
48
|
-
|
65
|
+
static through(model, args){
|
66
|
+
return model.loadRecords(args).then(()=>{
|
67
|
+
var linkingIds = [...new Set(model.searchRecords(args).map((record)=> record[CamelCase(this.className)+'Id']))]
|
68
|
+
return this.where({id: linkingIds})
|
69
|
+
})
|
49
70
|
}
|
50
71
|
|
51
|
-
|
72
|
+
static create(data){
|
73
|
+
return Axios.post(this.modelUrlPath(), Util.snakeCaseKeys(data))
|
74
|
+
.then((response) => {
|
75
|
+
new this(response.data)
|
76
|
+
return response
|
77
|
+
})
|
78
|
+
}
|
52
79
|
|
53
|
-
|
80
|
+
static update(data){
|
81
|
+
return Axios.put( `${this.modelUrlPath(true)}/${data.id}`, Util.snakeCaseKeys(data))
|
82
|
+
.then((response) => {
|
83
|
+
// new this(response.data)
|
84
|
+
return response
|
85
|
+
})
|
86
|
+
}
|
54
87
|
|
55
|
-
|
56
|
-
|
57
|
-
|
88
|
+
//Intended as private below here
|
89
|
+
static loadOrSearch(args={}){
|
90
|
+
let subscriptionParams = { channel: 'ModelsChannel', model: this.className, filter: args }
|
91
|
+
if(this.consumer.subscriptions.findAll(JSON.stringify(subscriptionParams)).length === 0){
|
92
|
+
return this.loadRecords(subscriptionParams)
|
93
|
+
} else {
|
94
|
+
return new Promise((resolve, reject) => { resolve(this.searchRecords(args)) } )
|
95
|
+
}
|
96
|
+
}
|
97
|
+
|
98
|
+
static loadRecords(args){
|
99
|
+
return new Promise((resolve, reject) => {
|
100
|
+
this.consumer.subscriptions.create(args, {
|
101
|
+
received: (data) => {
|
102
|
+
let records = []
|
103
|
+
data.forEach((datum) => {
|
104
|
+
records.push(new this(datum))
|
105
|
+
})
|
106
|
+
resolve(records)
|
107
|
+
}
|
58
108
|
})
|
59
109
|
})
|
110
|
+
}
|
60
111
|
|
61
|
-
|
112
|
+
static searchRecords(args){
|
113
|
+
var results = []
|
114
|
+
Object.keys(this.records).forEach((id)=>{
|
115
|
+
// Its a match if none of the keys don't match (IE terminates when a property doesn't match), if a property is an array it still matches if the corresponding arg is within it.
|
116
|
+
var match = !Object.keys(args).some((arg)=> {
|
117
|
+
return !(this.records[id][arg] == args[arg] ||
|
118
|
+
(Array.isArray(this.records[id][arg]) && this.records[id][arg].some((i)=> typeof i == 'object' && i.id == args[arg])) ||
|
119
|
+
(Array.isArray(args[arg]) && args[arg].some((a)=> typeof a == 'object' && a.id == this.records[id][arg] || a == this.records[id][arg]))
|
120
|
+
)
|
121
|
+
})
|
122
|
+
if(match) {
|
123
|
+
results.push(this.records[id])
|
124
|
+
}
|
125
|
+
})
|
126
|
+
return results
|
62
127
|
}
|
63
|
-
}
|
128
|
+
}
|
data/lib/javascript/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "rails-active-sync",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.2.0",
|
4
4
|
"description": "Javascript for interacting with active-sync ruby gem.",
|
5
5
|
"main": "active-sync.js",
|
6
6
|
"scripts": {
|
@@ -20,7 +20,8 @@
|
|
20
20
|
"dependencies": {
|
21
21
|
"actioncable": "^5.2.2-1",
|
22
22
|
"camelcase": "^5.2.0",
|
23
|
-
"snake-case": "^2.1.0"
|
23
|
+
"snake-case": "^2.1.0",
|
24
|
+
"axios": "^0.21.1"
|
24
25
|
},
|
25
26
|
"author": "Crammaman",
|
26
27
|
"license": "MIT"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active-sync
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- crammaman
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-12-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -25,47 +25,61 @@ dependencies:
|
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: 5.1.3
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: webpacker
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
34
|
-
type: :
|
33
|
+
version: 3.5.5
|
34
|
+
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: 3.5.5
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: sqlite3
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.4'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.4'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: foreman
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
44
58
|
requirements:
|
45
59
|
- - ">="
|
46
60
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
61
|
+
version: '0'
|
48
62
|
type: :development
|
49
63
|
prerelease: false
|
50
64
|
version_requirements: !ruby/object:Gem::Requirement
|
51
65
|
requirements:
|
52
66
|
- - ">="
|
53
67
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
68
|
+
version: '0'
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
70
|
+
name: puma
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
58
72
|
requirements:
|
59
|
-
- - "
|
73
|
+
- - ">="
|
60
74
|
- !ruby/object:Gem::Version
|
61
|
-
version:
|
75
|
+
version: '0'
|
62
76
|
type: :development
|
63
77
|
prerelease: false
|
64
78
|
version_requirements: !ruby/object:Gem::Requirement
|
65
79
|
requirements:
|
66
|
-
- - "
|
80
|
+
- - ">="
|
67
81
|
- !ruby/object:Gem::Version
|
68
|
-
version:
|
82
|
+
version: '0'
|
69
83
|
description: With minimal set up ActiveSync presents limited rails model interfaces
|
70
84
|
within the JS font end. Records accessed are kept updated through action cable.
|
71
85
|
email:
|
@@ -77,11 +91,13 @@ files:
|
|
77
91
|
- MIT-LICENSE
|
78
92
|
- README.md
|
79
93
|
- Rakefile
|
80
|
-
- app/channels/
|
94
|
+
- app/channels/models_channel.rb
|
81
95
|
- app/controllers/active_sync/application_controller.rb
|
82
96
|
- app/controllers/active_sync/models_controller.rb
|
83
97
|
- app/helpers/active_sync/models_helper.rb
|
84
|
-
- app/
|
98
|
+
- app/jobs/broadcast_change_job.rb
|
99
|
+
- app/models/active_sync/broadcaster.rb
|
100
|
+
- app/models/active_sync/model.rb
|
85
101
|
- app/models/active_sync/sync.rb
|
86
102
|
- config/routes.rb
|
87
103
|
- lib/active-sync.rb
|
@@ -90,7 +106,7 @@ files:
|
|
90
106
|
- lib/javascript/active-sync.js
|
91
107
|
- lib/javascript/model.js
|
92
108
|
- lib/javascript/package.json
|
93
|
-
- lib/javascript/
|
109
|
+
- lib/javascript/util.js
|
94
110
|
- lib/tasks/rails_sync_tasks.rake
|
95
111
|
homepage: https://github.com/Crammaman/rails-sync
|
96
112
|
licenses:
|
@@ -111,8 +127,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
111
127
|
- !ruby/object:Gem::Version
|
112
128
|
version: '0'
|
113
129
|
requirements: []
|
114
|
-
|
115
|
-
rubygems_version: 2.6.10
|
130
|
+
rubygems_version: 3.1.6
|
116
131
|
signing_key:
|
117
132
|
specification_version: 4
|
118
133
|
summary: Live updated JS objects for use in reactive JS frameworks
|
@@ -1,89 +0,0 @@
|
|
1
|
-
# Rails currently doesn't allow namespacing channels in an engine
|
2
|
-
# module ActiveSync
|
3
|
-
class ActiveSyncChannel < ActionCable::Channel::Base
|
4
|
-
# For providing DashData with data from rails models
|
5
|
-
# To change the data sent (like reducing how much is sent)
|
6
|
-
# implement broadcast_model in the respective modelc
|
7
|
-
|
8
|
-
def subscribed
|
9
|
-
|
10
|
-
if filter && filter[:IsReference]
|
11
|
-
|
12
|
-
subscribe_references
|
13
|
-
|
14
|
-
else
|
15
|
-
|
16
|
-
subscribe_models
|
17
|
-
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
def unsubscribed
|
22
|
-
# Any cleanup needed when channel is unsubscribed
|
23
|
-
end
|
24
|
-
|
25
|
-
private
|
26
|
-
def subscribe_models
|
27
|
-
if filter.nil?
|
28
|
-
|
29
|
-
stream_from "#{subscription_model.name}_All"
|
30
|
-
transmit( subscription_model.sync_all )
|
31
|
-
|
32
|
-
else
|
33
|
-
|
34
|
-
subscription_model.register_sync_subscription "#{subscription_model.name}_#{checksum}", filter
|
35
|
-
stream_from "#{subscription_model.name}_#{checksum}"
|
36
|
-
|
37
|
-
# TODO ensure that params are safe to pass to the model then register for syncing to.
|
38
|
-
transmit( subscription_model.sync_filtered( filter.to_h ) )
|
39
|
-
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
def subscribe_references
|
44
|
-
|
45
|
-
record = subscription_model.find( filter[:record_id] )
|
46
|
-
|
47
|
-
if model_association
|
48
|
-
|
49
|
-
transmit( ActiveSync::Sync.association_record( model_association, record) )
|
50
|
-
|
51
|
-
else
|
52
|
-
|
53
|
-
raise "#{subscription_model} does not reference #{ filter[:association_name] }"
|
54
|
-
|
55
|
-
end
|
56
|
-
|
57
|
-
subscription_model.register_sync_subscription "#{subscription_model.name}_#{checksum}", filter.merge( subscribed_model: subscription_model )
|
58
|
-
eval( model_association[:class] ).register_sync_subscription "#{subscription_model.name}_#{checksum}", filter.merge( subscribed_model: subscription_model )
|
59
|
-
stream_from "#{subscription_model.name}_#{checksum}"
|
60
|
-
|
61
|
-
end
|
62
|
-
|
63
|
-
def subscription_model
|
64
|
-
|
65
|
-
if ActiveSync::Sync.is_sync_model?( params[:model] )
|
66
|
-
|
67
|
-
eval( params[:model] )
|
68
|
-
|
69
|
-
else
|
70
|
-
|
71
|
-
raise "Model parameter: #{params[:model]} is not a registered sync model"
|
72
|
-
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
def model_association
|
77
|
-
ActiveSync::Sync.get_model_association( subscription_model, filter[:association_name] )
|
78
|
-
end
|
79
|
-
|
80
|
-
def filter
|
81
|
-
params[:filter]
|
82
|
-
end
|
83
|
-
|
84
|
-
def checksum
|
85
|
-
# A checksum is generated and used in the stream name so all of the same filtered subscriptions should be on the same Stream
|
86
|
-
Digest::MD5.hexdigest( Marshal::dump( filter ) )
|
87
|
-
end
|
88
|
-
end
|
89
|
-
# end
|
@@ -1,110 +0,0 @@
|
|
1
|
-
module ActiveSync
|
2
|
-
module ActiveRecordExtension
|
3
|
-
extend ActiveSupport::Concern
|
4
|
-
|
5
|
-
included do
|
6
|
-
# after_update :sync_update
|
7
|
-
after_commit :sync_change
|
8
|
-
|
9
|
-
@@sync_record_subscriptions = {}
|
10
|
-
end
|
11
|
-
|
12
|
-
def sync_update
|
13
|
-
sync_change if saved_changes?
|
14
|
-
end
|
15
|
-
|
16
|
-
def sync_change
|
17
|
-
if ActiveSync::Sync.is_sync_model? self.class
|
18
|
-
#TODO properly accommodate multi process environment, since sync sync_subscriptions
|
19
|
-
# exists only in one process, if one process has a filter sub but another does not
|
20
|
-
# not all users will necessarily get broadcast to.
|
21
|
-
ActionCable.server.broadcast("#{self.class}_All", ActiveSync::Sync.sync_record( self ) )
|
22
|
-
self.class.sync_record_subscriptions.each do | stream, filter |
|
23
|
-
unless filter[:IsReference]
|
24
|
-
|
25
|
-
match = true
|
26
|
-
filter.each do | key, value |
|
27
|
-
unless self.send( key ) == value
|
28
|
-
match = false
|
29
|
-
break
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
ActionCable.server.broadcast( stream, ActiveSync::Sync.sync_record( self ) ) if match
|
34
|
-
|
35
|
-
else
|
36
|
-
|
37
|
-
model_association = ActiveSync::Sync.get_model_association( filter[:subscribed_model], filter[:association_name] )
|
38
|
-
|
39
|
-
record = filter[:subscribed_model].find( filter[:record_id] )
|
40
|
-
|
41
|
-
if defined? record.send( model_association[:name] ).pluck
|
42
|
-
|
43
|
-
referenced = record.send( model_association[:name] ).pluck(:id).include? id
|
44
|
-
else
|
45
|
-
referenced = record.send( model_association[:name] ).id == id
|
46
|
-
end
|
47
|
-
|
48
|
-
if referenced
|
49
|
-
ActionCable.server.broadcast( stream, ActiveSync::Sync.association_record( model_association, record ))
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
|
57
|
-
class_methods do
|
58
|
-
|
59
|
-
def register_sync_subscription stream, filter
|
60
|
-
@@sync_record_subscriptions[ self.name ] = {} if @@sync_record_subscriptions[ self.name ].nil?
|
61
|
-
@@sync_record_subscriptions[ self.name ][ stream ] = filter
|
62
|
-
end
|
63
|
-
|
64
|
-
def sync_record_subscriptions
|
65
|
-
@@sync_record_subscriptions[ self.name ] || {}
|
66
|
-
end
|
67
|
-
|
68
|
-
# Sync configures the data that is used in general sync communication
|
69
|
-
# This can be passed the following options:
|
70
|
-
#
|
71
|
-
# Example use in Model:
|
72
|
-
# sync :all_references, associations: [ :sites ]
|
73
|
-
|
74
|
-
# ATTRIBUTE OPTIONS
|
75
|
-
# Attributes are data that is sent in the actual sync data (this will always include the ID)
|
76
|
-
|
77
|
-
# :all_attributes - sync data will have all attributes
|
78
|
-
# :attributes - an array of symbols that will be called on the record and sent as attributes
|
79
|
-
|
80
|
-
# ASSOCIATION OPTIONS
|
81
|
-
# Associations are lazy loaded, data will not go with the record but the front end will be told that
|
82
|
-
# there is an association to load the data of when accessed.
|
83
|
-
|
84
|
-
# :all_associations - sync data will be associated
|
85
|
-
# :associations - an array of symbols
|
86
|
-
|
87
|
-
def sync *attributes
|
88
|
-
|
89
|
-
ActiveSync::Sync.configure_model_description self, attributes
|
90
|
-
|
91
|
-
end
|
92
|
-
|
93
|
-
# Sync hash for all of self records
|
94
|
-
def sync_all
|
95
|
-
|
96
|
-
self.all.map do |record|
|
97
|
-
ActiveSync::Sync.sync_record record
|
98
|
-
end
|
99
|
-
|
100
|
-
end
|
101
|
-
|
102
|
-
def sync_filtered filter
|
103
|
-
|
104
|
-
self.where( filter ).map do |record|
|
105
|
-
ActiveSync::Sync.sync_record record
|
106
|
-
end
|
107
|
-
end
|
108
|
-
end
|
109
|
-
end
|
110
|
-
end
|
data/lib/javascript/records.js
DELETED
@@ -1,216 +0,0 @@
|
|
1
|
-
import CamelCase from 'camelcase'
|
2
|
-
import SnakeCase from 'snake-case'
|
3
|
-
|
4
|
-
export default class Records {
|
5
|
-
constructor( args = {} ){
|
6
|
-
this._records = {}
|
7
|
-
this._addNewRecord = args.addNewRecord || this.addNewRecord
|
8
|
-
this._associations = args.associations || []
|
9
|
-
this._cable = args.cable
|
10
|
-
this._modelName = args.modelName
|
11
|
-
this._references = args.references
|
12
|
-
// A subscription is just it's filter{}
|
13
|
-
this._subscriptions = []
|
14
|
-
// An ugly object of { filter{}: boolean }
|
15
|
-
this._dataLoading = {}
|
16
|
-
}
|
17
|
-
|
18
|
-
setAssociatedModels( models ){
|
19
|
-
this._associations.forEach( ( association ) => {
|
20
|
-
var model = models.find( ( model ) => model.name == association.class )
|
21
|
-
if( model ){
|
22
|
-
association.model = model
|
23
|
-
} else {
|
24
|
-
throw `Model ${this._modelName} is set up for association with ${association.class} but ${association.class} is not available through Active Sync`
|
25
|
-
}
|
26
|
-
} )
|
27
|
-
}
|
28
|
-
|
29
|
-
getRecord( id ){
|
30
|
-
return this._records[id]
|
31
|
-
}
|
32
|
-
|
33
|
-
push( record ){
|
34
|
-
|
35
|
-
this._associations.forEach( ( association ) => {
|
36
|
-
switch(association.type){
|
37
|
-
case 'ActiveRecord::Associations::HasManyAssociation':
|
38
|
-
case 'ActiveRecord::Associations::HasManyThroughAssociation':
|
39
|
-
record[ association.name ] = [1]
|
40
|
-
break
|
41
|
-
case 'ActiveRecord::Associations::BelongsToAssociation':
|
42
|
-
var standIn = {}
|
43
|
-
Object.defineProperty(standIn, "$count", {
|
44
|
-
enumerable: false,
|
45
|
-
writable: true
|
46
|
-
})
|
47
|
-
standIn.$count = 1
|
48
|
-
//this._addNewRecord( record, association.name, standIn )
|
49
|
-
record[ association.name ] = standIn
|
50
|
-
break
|
51
|
-
|
52
|
-
default:
|
53
|
-
console.log('Unknown know association type ' + association.type)
|
54
|
-
}
|
55
|
-
} )
|
56
|
-
|
57
|
-
var newRecord = new Proxy( this.camelCaseKeys( record ), {
|
58
|
-
get: ( record, property )=> this.getFromRecord( record, property, this )
|
59
|
-
} )
|
60
|
-
|
61
|
-
this._addNewRecord( this._records, newRecord.id, newRecord )
|
62
|
-
}
|
63
|
-
|
64
|
-
addNewRecord( records, recordId, record ){
|
65
|
-
if( records[ recordId ] ){
|
66
|
-
Object.keys( record ).forEach( (key) => records[ recordId ][ key ] = record[ key ] )
|
67
|
-
} else {
|
68
|
-
records[ recordId ] = record
|
69
|
-
}
|
70
|
-
}
|
71
|
-
|
72
|
-
forEach( func ){
|
73
|
-
Object.keys( this._records ).forEach(( id ) => func( this._records[id] ) )
|
74
|
-
}
|
75
|
-
|
76
|
-
camelCaseKeys( record ){
|
77
|
-
return Object.keys( record ).reduce( ( camelRecord, key ) => {
|
78
|
-
camelRecord[ CamelCase(key) ] = record[key]
|
79
|
-
return camelRecord
|
80
|
-
}, {} )
|
81
|
-
}
|
82
|
-
snakeCaseKeys( record ){
|
83
|
-
return Object.keys( record ).reduce( ( camelRecord, key ) => {
|
84
|
-
camelRecord[ SnakeCase(key) ] = record[key]
|
85
|
-
return camelRecord
|
86
|
-
}, {} )
|
87
|
-
}
|
88
|
-
|
89
|
-
getFromRecord( record, property, self ){
|
90
|
-
self.loadIfAssociation( record, property )
|
91
|
-
return record[ property ]
|
92
|
-
}
|
93
|
-
//If the records already exist it will return an instantaneously resolving promise
|
94
|
-
loadRecords( filter = null ){
|
95
|
-
if( !this.isSubscribed( filter ) ){
|
96
|
-
|
97
|
-
this._subscriptions.push( filter ? filter : 'all' )
|
98
|
-
this.subscribeToRecords( filter )
|
99
|
-
|
100
|
-
}
|
101
|
-
|
102
|
-
return new Promise( (resolve, reject)=> this.awaitData(resolve, reject, filter) )
|
103
|
-
}
|
104
|
-
|
105
|
-
awaitData( resolve, reject, filter ){
|
106
|
-
setTimeout( ()=>{
|
107
|
-
if( this._dataLoading[ filter ] ){
|
108
|
-
this.awaitData( resolve, reject, filter )
|
109
|
-
} else {
|
110
|
-
resolve()
|
111
|
-
}
|
112
|
-
//TODO there's something wrong with _dataLoading not working so this wait time has been cranked up.
|
113
|
-
}, 200 )
|
114
|
-
}
|
115
|
-
|
116
|
-
// Adds records that match properties into records
|
117
|
-
forEachMatch( properties, func ){
|
118
|
-
|
119
|
-
let records = []
|
120
|
-
|
121
|
-
this.forEach( ( record ) => {
|
122
|
-
|
123
|
-
var match = true
|
124
|
-
Object.keys( properties ).forEach( ( property ) => {
|
125
|
-
if( properties[ property ] != record[ property ] ){
|
126
|
-
match = false
|
127
|
-
}
|
128
|
-
})
|
129
|
-
|
130
|
-
if( match ){
|
131
|
-
func(record)
|
132
|
-
}
|
133
|
-
})
|
134
|
-
|
135
|
-
return records
|
136
|
-
}
|
137
|
-
|
138
|
-
isSubscribed( filter ){
|
139
|
-
filter = filter ? filter : 'all'
|
140
|
-
if( this._subscriptions.includes( 'all' ) && !filter.IsReference ) {
|
141
|
-
return true
|
142
|
-
} else {
|
143
|
-
return !!this._subscriptions.find( ( sub ) => JSON.stringify(sub) == JSON.stringify(filter) )
|
144
|
-
}
|
145
|
-
}
|
146
|
-
|
147
|
-
// Subscribing to a record is the source of all data communication. With no filter
|
148
|
-
// all records are subscribed to, this is done without checking for existing subscriptions
|
149
|
-
// so that needs to be done before getting here.
|
150
|
-
subscribeToRecords( filter = null ){
|
151
|
-
|
152
|
-
let subscriptionParameters = { channel: 'ActiveSyncChannel', model: this._modelName }
|
153
|
-
this._dataLoading[ filter ] = true
|
154
|
-
|
155
|
-
if ( filter !== null ) {
|
156
|
-
subscriptionParameters.filter = filter.IsReference ? filter : this.snakeCaseKeys(filter)
|
157
|
-
}
|
158
|
-
|
159
|
-
this._cable.subscriptions.create( subscriptionParameters ,{
|
160
|
-
received: (data) => {
|
161
|
-
var records = data.IsReference ? this._references : this
|
162
|
-
// Will find records and update them, if not found will add them to
|
163
|
-
// _records or references.
|
164
|
-
if( data.length > 0){
|
165
|
-
|
166
|
-
// data is a promise so might not have anything at this point,
|
167
|
-
// adding with a forEach allows promises to be handled (is there a better way?)
|
168
|
-
data.forEach((addModel) => {
|
169
|
-
records.push(addModel)
|
170
|
-
})
|
171
|
-
|
172
|
-
|
173
|
-
} else {
|
174
|
-
|
175
|
-
records.push( data )
|
176
|
-
|
177
|
-
}
|
178
|
-
|
179
|
-
this._dataLoading[ filter ] = false
|
180
|
-
|
181
|
-
}
|
182
|
-
})
|
183
|
-
}
|
184
|
-
|
185
|
-
loadIfAssociation( record, property ){
|
186
|
-
var association = this._associations.find( ( a ) => a.name == property )
|
187
|
-
|
188
|
-
if( association ){
|
189
|
-
|
190
|
-
var referencedRecords = []
|
191
|
-
|
192
|
-
if( record[property][0] == 1 ){
|
193
|
-
record[property].pop()
|
194
|
-
} else if( record[property].$count > 0 ){
|
195
|
-
record[property].$count--
|
196
|
-
} else {
|
197
|
-
this.loadRecords({ IsReference: true, record_id: record.id, association_name: property })
|
198
|
-
.then( () => {
|
199
|
-
var references = this._references.getRecord( record.id )[ property ]
|
200
|
-
|
201
|
-
if( references.length > 0 && references.length !== record[property].length ){
|
202
|
-
record[property] = []
|
203
|
-
references.forEach( ( reference ) => {
|
204
|
-
record[property].push( association.model.find( reference ))
|
205
|
-
} )
|
206
|
-
|
207
|
-
} else if( typeof references.length === 'undefined' && record[property].$count == 0 ) {
|
208
|
-
|
209
|
-
this._addNewRecord(record,property, association.model.find( references ))
|
210
|
-
|
211
|
-
}
|
212
|
-
})
|
213
|
-
}
|
214
|
-
}
|
215
|
-
}
|
216
|
-
}
|