active-sync 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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,5 @@
1
+ module ActiveSync
2
+ class ApplicationController < ActionController::Base
3
+ protect_from_forgery with: :exception
4
+ end
5
+ end
@@ -0,0 +1,10 @@
1
+ module ActiveSync
2
+ class ModelsController < ApplicationController
3
+
4
+ def index
5
+
6
+ render json: ActiveSync::Sync.model_descriptions
7
+
8
+ end
9
+ end
10
+ 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
@@ -0,0 +1,3 @@
1
+ ActiveSync::Engine.routes.draw do
2
+ resources :models, only: [ :index ]
3
+ end
@@ -0,0 +1,4 @@
1
+ require "active_sync/engine"
2
+ module ActiveSync
3
+ # Your code goes here...
4
+ end
@@ -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,3 @@
1
+ module ActiveSync
2
+ VERSION = '0.1.0'
3
+ 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
+ }
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :active_sync do
3
+ # # Task goes here
4
+ # end
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: []