active-sync 0.1.1 → 0.2.0
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 +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
|
-
}
|