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 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: []