active-sync 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +36 -0
- data/app/channels/active_sync_channel.rb +89 -0
- data/app/controllers/active_sync/application_controller.rb +5 -0
- data/app/controllers/active_sync/models_controller.rb +10 -0
- data/app/helpers/active_sync/models_helper.rb +15 -0
- data/app/models/active_sync/active_record_extension.rb +110 -0
- data/app/models/active_sync/sync.rb +125 -0
- data/config/routes.rb +3 -0
- data/lib/active-sync.rb +4 -0
- data/lib/active_sync/engine.rb +13 -0
- data/lib/active_sync/version.rb +3 -0
- data/lib/javascript/active-sync.js +68 -0
- data/lib/javascript/model.js +63 -0
- data/lib/javascript/records.js +216 -0
- data/lib/tasks/rails_sync_tasks.rake +4 -0
- metadata +118 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 4af6276356d5f3f9e62fee94ac86418a141aeb0c
|
4
|
+
data.tar.gz: e04dd3659a3265137f315405fd4fbe7c6ba095b5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bbb474bbb05eebf6aa6be7c700c2b6d94f960d0587316f9dcf70bcc08d1b135029daecd3cc32dd9eb247a4b3b3e170ab51e4b56fb23659dfa351d29b28c9a835
|
7
|
+
data.tar.gz: 0b3d134b824cf89dbd9eef943c5677b197d5dcb1b1ff36c08520fd64d30545778fbb276a2ce0382a637c5678752a95841c93de50821760a3b024e75f83ef9052
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2019 crammaman
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# ActiveSync
|
2
|
+
Short description and motivation.
|
3
|
+
|
4
|
+
## Usage
|
5
|
+
How to use my plugin.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem 'active_sync'
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
```bash
|
16
|
+
$ bundle
|
17
|
+
```
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
```bash
|
21
|
+
$ gem install active_sync
|
22
|
+
```
|
23
|
+
|
24
|
+
## Contributing
|
25
|
+
Contribution directions go here.
|
26
|
+
|
27
|
+
## License
|
28
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'ActiveSync'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
|
18
|
+
load 'rails/tasks/engine.rake'
|
19
|
+
|
20
|
+
|
21
|
+
load 'rails/tasks/statistics.rake'
|
22
|
+
|
23
|
+
|
24
|
+
|
25
|
+
require 'bundler/gem_tasks'
|
26
|
+
|
27
|
+
require 'rake/testtask'
|
28
|
+
|
29
|
+
Rake::TestTask.new(:test) do |t|
|
30
|
+
t.libs << 'test'
|
31
|
+
t.pattern = 'test/**/*_test.rb'
|
32
|
+
t.verbose = false
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
task default: :test
|
@@ -0,0 +1,89 @@
|
|
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
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module ActiveSync
|
2
|
+
module ModelsHelper
|
3
|
+
def self.model_descriptions
|
4
|
+
Rails.application.eager_load! unless Rails.application.config.cache_classes
|
5
|
+
ActiveRecord::Base.subclasses[1].descendants.map do |model|
|
6
|
+
{
|
7
|
+
name: model.name,
|
8
|
+
associations: model.reflect_on_all_associations.map do |a|
|
9
|
+
{ name: a.name, class: a.class_name, type: a.association_class.name }
|
10
|
+
end
|
11
|
+
}
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,110 @@
|
|
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
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module ActiveSync
|
2
|
+
class Sync
|
3
|
+
|
4
|
+
# Describes what of each model should be sent as the sync records,
|
5
|
+
# this is populated through calls to 'sync' in the model class
|
6
|
+
@@model_descriptions = {}
|
7
|
+
@@loaded = false
|
8
|
+
|
9
|
+
def self.model_descriptions
|
10
|
+
( Rails.application.eager_load! && @@loaded = true ) unless Rails.application.config.cache_classes || @@loaded
|
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
|
+
|
62
|
+
else
|
63
|
+
|
64
|
+
throw "Unknown sync option: #{option}"
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.add_attributes_to_description model, attributes
|
71
|
+
|
72
|
+
attributes.each{ |attribute| @@model_descriptions[ model.name ][ :attributes ] << attribute.to_s }
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.add_associations_to_description model, association_names
|
77
|
+
association_names.each do |association_name|
|
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
|
+
|
92
|
+
else
|
93
|
+
|
94
|
+
throw "Association #{ association_name } not found for #{ model.name }"
|
95
|
+
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.association_record model_association, record
|
101
|
+
{
|
102
|
+
IsReference: true,
|
103
|
+
id: record.id,
|
104
|
+
model_association[:name] => associated_ids( record, model_association )
|
105
|
+
}
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.associated_ids record, model_association
|
109
|
+
if defined? record.send( model_association[:name] ).pluck
|
110
|
+
|
111
|
+
record.send( model_association[:name] ).pluck(:id)
|
112
|
+
else
|
113
|
+
record.send( model_association[:name] ).id
|
114
|
+
end
|
115
|
+
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
|
+
end
|
125
|
+
end
|
data/config/routes.rb
ADDED
data/lib/active-sync.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
module ActiveSync
|
2
|
+
class Engine < ::Rails::Engine
|
3
|
+
isolate_namespace ActiveSync
|
4
|
+
|
5
|
+
initializer "active_sync", before: :load_config_initializers do |app|
|
6
|
+
Rails.application.routes.append do
|
7
|
+
mount ActiveSync::Engine, at: "/active_sync"
|
8
|
+
end
|
9
|
+
|
10
|
+
ActiveRecord::Base.class_eval { include ActiveSync::ActiveRecordExtension }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
import ActionCable from 'actioncable'
|
2
|
+
import Model from 'model'
|
3
|
+
|
4
|
+
export default class ActiveSync {
|
5
|
+
constructor( args ){
|
6
|
+
|
7
|
+
this._cable = ActionCable.createConsumer()
|
8
|
+
this._models = []
|
9
|
+
this._customModels = args.customModels || []
|
10
|
+
|
11
|
+
this._modelOptions = {
|
12
|
+
addNewRecord: args.addNewRecord,
|
13
|
+
afterFind: args.afterFind
|
14
|
+
}
|
15
|
+
|
16
|
+
var modelDescriptions = this.requestModelDescriptions()
|
17
|
+
|
18
|
+
Object.keys( modelDescriptions ).forEach( ( modelName ) =>{
|
19
|
+
modelDescriptions[modelName].name = modelName
|
20
|
+
this.setupModel( modelDescriptions[modelName] )
|
21
|
+
})
|
22
|
+
|
23
|
+
this._models.forEach( ( model ) => model.setAssociatedModels( this._models))
|
24
|
+
args.afterSetup( this._models )
|
25
|
+
|
26
|
+
}
|
27
|
+
|
28
|
+
static install( Vue, options ){
|
29
|
+
var rs = new ActiveSync({
|
30
|
+
addNewRecord: ( records, id, record ) => {
|
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
|
+
},
|
37
|
+
|
38
|
+
afterSetup: ( models ) => {
|
39
|
+
models.forEach( ( model ) => {
|
40
|
+
Vue.prototype[ '$' + model.name ] = model
|
41
|
+
})
|
42
|
+
},
|
43
|
+
|
44
|
+
customModels: (options || {}).customModels
|
45
|
+
})
|
46
|
+
}
|
47
|
+
|
48
|
+
requestModelDescriptions(){
|
49
|
+
var xmlHttp = new XMLHttpRequest()
|
50
|
+
xmlHttp.open( "GET", 'active_sync/models', false ) // false for synchronous request
|
51
|
+
xmlHttp.send( null )
|
52
|
+
return JSON.parse(xmlHttp.responseText)
|
53
|
+
}
|
54
|
+
|
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
|
+
|
62
|
+
} else {
|
63
|
+
|
64
|
+
this._models.push( new Model( modelDescription, this._cable, this._modelOptions ) )
|
65
|
+
|
66
|
+
}
|
67
|
+
}
|
68
|
+
}
|
@@ -0,0 +1,63 @@
|
|
1
|
+
import Records from './records.js'
|
2
|
+
|
3
|
+
export default class Model{
|
4
|
+
constructor( description, cable, options ){
|
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
|
+
}
|
18
|
+
|
19
|
+
setAssociatedModels( models ){
|
20
|
+
this._records.setAssociatedModels( models )
|
21
|
+
}
|
22
|
+
|
23
|
+
get name(){
|
24
|
+
return this._name
|
25
|
+
}
|
26
|
+
|
27
|
+
get all() {
|
28
|
+
var allRecords = []
|
29
|
+
this._records.loadRecords().then( () => {
|
30
|
+
this._records.forEach( ( record ) => {
|
31
|
+
allRecords.push( this._records.getRecord( record.id ) )
|
32
|
+
})
|
33
|
+
})
|
34
|
+
|
35
|
+
return allRecords
|
36
|
+
}
|
37
|
+
|
38
|
+
find( id ){
|
39
|
+
|
40
|
+
if( !this._records.getRecord( id ) ){
|
41
|
+
|
42
|
+
this._records.push( { id: id } )
|
43
|
+
this._records.loadRecords( { id: id } )
|
44
|
+
// .then(()=> this._afterFind( this._records.getRecord( id ) ))
|
45
|
+
}
|
46
|
+
|
47
|
+
|
48
|
+
return this._records.getRecord( id )
|
49
|
+
}
|
50
|
+
|
51
|
+
where( properties ){
|
52
|
+
|
53
|
+
var records = []
|
54
|
+
|
55
|
+
this._records.loadRecords( properties ).then( () => {
|
56
|
+
this._records.forEachMatch( properties, (record) => {
|
57
|
+
records.push( this._records.getRecord( record.id ) )
|
58
|
+
})
|
59
|
+
})
|
60
|
+
|
61
|
+
return records
|
62
|
+
}
|
63
|
+
}
|
@@ -0,0 +1,216 @@
|
|
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
|
+
}
|
metadata
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: active-sync
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- crammaman
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-04-16 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 5.1.3
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 5.1.3
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: puma
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.11'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.11'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: webpacker
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 3.5.5
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 3.5.5
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: sqlite3
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.3.6
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.3.6
|
69
|
+
description: With minimal set up ActiveSync presents limited rails model interfaces
|
70
|
+
within the JS font end. Records accessed are kept updated through action cable.
|
71
|
+
email:
|
72
|
+
- smadams00@gmail.com
|
73
|
+
executables: []
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- MIT-LICENSE
|
78
|
+
- README.md
|
79
|
+
- Rakefile
|
80
|
+
- app/channels/active_sync_channel.rb
|
81
|
+
- app/controllers/active_sync/application_controller.rb
|
82
|
+
- app/controllers/active_sync/models_controller.rb
|
83
|
+
- app/helpers/active_sync/models_helper.rb
|
84
|
+
- app/models/active_sync/active_record_extension.rb
|
85
|
+
- app/models/active_sync/sync.rb
|
86
|
+
- config/routes.rb
|
87
|
+
- lib/active-sync.rb
|
88
|
+
- lib/active_sync/engine.rb
|
89
|
+
- lib/active_sync/version.rb
|
90
|
+
- lib/javascript/active-sync.js
|
91
|
+
- lib/javascript/model.js
|
92
|
+
- lib/javascript/records.js
|
93
|
+
- lib/tasks/rails_sync_tasks.rake
|
94
|
+
homepage: https://github.com/Crammaman/rails-sync
|
95
|
+
licenses:
|
96
|
+
- MIT
|
97
|
+
metadata: {}
|
98
|
+
post_install_message:
|
99
|
+
rdoc_options: []
|
100
|
+
require_paths:
|
101
|
+
- lib
|
102
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
107
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - ">="
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '0'
|
112
|
+
requirements: []
|
113
|
+
rubyforge_project:
|
114
|
+
rubygems_version: 2.5.2.1
|
115
|
+
signing_key:
|
116
|
+
specification_version: 4
|
117
|
+
summary: Live updated JS objects for use in reactive JS frameworks
|
118
|
+
test_files: []
|