active-sync 0.1.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 +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: []
|