Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
active-sync (0.1.1)
active-sync (0.2.0)
rails (>= 5.1.3)

GEM
Expand Down Expand Up @@ -89,6 +89,8 @@ GEM
nokogiri (1.11.3)
mini_portile2 (~> 2.5.0)
racc (~> 1.4)
puma (5.5.2)
nio4r (~> 2.0)
racc (1.5.2)
rack (2.2.3)
rack-proxy (0.6.5)
Expand Down Expand Up @@ -139,7 +141,7 @@ GEM
rack-proxy (>= 0.6.1)
railties (>= 5.2)
semantic_range (>= 2.3.0)
websocket-driver (0.7.3)
websocket-driver (0.7.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
zeitwerk (2.4.2)
Expand All @@ -150,6 +152,7 @@ PLATFORMS
DEPENDENCIES
active-sync!
foreman
puma
sqlite3 (~> 1.4)
webpacker (>= 3.5.5)

Expand Down
1 change: 1 addition & 0 deletions active_sync.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ Gem::Specification.new do |s|
s.add_development_dependency "webpacker", ">= 3.5.5"
s.add_development_dependency "sqlite3", "~> 1.4"
s.add_development_dependency "foreman"
s.add_development_dependency "puma"
end
41 changes: 10 additions & 31 deletions app/channels/models_channel.rb
Original file line number Diff line number Diff line change
@@ -1,48 +1,27 @@
# Rails currently doesn't allow namespacing channels in an engine
# module ActiveSync
class ModelsChannel < ActionCable::Channel::Base
# For providing DashData with data from rails models
# To change the data sent (like reducing how much is sent)
# implement broadcast_model in the respective modelc
# To change the data sent implement sync_record in the respective model

def subscribed
subscribe_models
transmit(subscription_model.where(params[:filter]).map(&:sync_record))
stream_from params[:model], coder: ActiveSupport::JSON do |message|
if (params[:filter].to_a - message.to_a).blank?
transmit([message])
end
end
end

def unsubscribed
# Any cleanup needed when channel is unsubscribed
end

private
def subscribe_models
if filter.nil?

stream_from "#{subscription_model.name}_All"
transmit( subscription_model.sync_all )

else

subscription_model.register_sync_subscription "#{subscription_model.name}_#{checksum}", filter
stream_from "#{subscription_model.name}_#{checksum}"

# TODO ensure that params are safe to pass to the model then register for syncing to.
transmit( subscription_model.sync_filtered( filter.to_h ) )

end
end

def subscription_model
model = params[:model].camelize.constantize
model.sync_model? ? model : raise "Model '#{params[:model]}' is not a registered sync model"
end

def model_association
ActiveSync::Sync.get_model_association( subscription_model, filter[:association_name] )
end

def checksum
# A checksum is generated and used in the stream name so all of the same filtered subscriptions should be on the same Stream
Digest::MD5.hexdigest( Marshal::dump( filter ) )
model = params[:model].singularize.camelize.constantize
raise "Model '#{params[:model]}' is not set up for syncing model" unless model.sync_model?
model
end
end
# end
2 changes: 1 addition & 1 deletion app/controllers/active_sync/application_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module ActiveSync
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
# protect_from_forgery with: :exception
end
end
16 changes: 11 additions & 5 deletions app/controllers/active_sync/models_controller.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
module ActiveSync
class ModelsController < ApplicationController

def index
render json: model.sync_filtered(properties)
def update
#TODO some oversite on what can be edited for sync records
model.find(params[:id]).update(params.permit(model.sync_attributes))
head :no_content
end

def properties
params.permit(ActiveSync::Sync.model_descriptions[model.name][:attributes])
def create
#TODO some oversite on what can be created for sync records
render json: model.create(params.permit(model.sync_attributes)).id
end

private
def model
params[:model].singularize.camelize.safe_constantize || params[:model].camelize.safe_constantize
m = params[:model].singularize.camelize.safe_constantize || params[:model].camelize.safe_constantize
raise "Cannot edit #{params[:model]} as it is not a sync model" unless m.sync_model?
m
end
end
end
19 changes: 4 additions & 15 deletions app/jobs/broadcast_change_job.rb
Original file line number Diff line number Diff line change
@@ -1,20 +1,9 @@
class BroadcastChangeJob < ApplicationJob
queue_as :default
queue_as :active_sync

def perform record
SyncSubscriptions.all.each do | subscription |
unless filter[:IsReference]

match = true
filter.each do | key, value |
unless self.send( key ) == value
match = false
break
end
end
include ActionCable::Channel::Broadcasting

ActionCable.server.broadcast( stream, ActiveSync::Sync.sync_record( self ) ) if match
end
end
def perform record
ActionCable.server.broadcast(record.class.name, record.sync_record)
end
end
25 changes: 3 additions & 22 deletions app/models/active_sync/model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,6 @@ def sync_model?
true
end

def register_sync_subscription stream, filter
@@sync_record_subscriptions[ self.name ] = {} if @@sync_record_subscriptions[ self.name ].nil?
@@sync_record_subscriptions[ self.name ][ stream ] = filter
end

def sync_record_subscriptions
@@sync_record_subscriptions[ self.name ] || {}
end

# #sync sets the #sync_record method that renders the hash to create the JSON object that is broadcast and sets
# #sync_associations which returns a list of associations that are permitted to be broadcast for this model.
# define these methods directly in your model if the record sent to the font end needs to be different to what's
Expand All @@ -57,26 +48,16 @@ def sync_record_subscriptions
# :associations - an array of symbols

def sync *attributes
self.class.define_method(:sync_attributes) do
ActiveSync::Sync.sync_attributes(self, attributes)
end
define_method(:sync_record) do
ActiveSync::Sync.sync_record(self, attributes)
end
define_method(:sync_associations) do
ActiveSync::Sync.sync_associations(self, attributes)
end
end

# Sync hash for all of self records
def sync_all
self.all.map do |record|
ActiveSync::Sync.sync_record record
end
end

def sync_filtered filter
self.where( filter ).map do |record|
ActiveSync::Sync.sync_record record
end
end
end
end
end
19 changes: 16 additions & 3 deletions app/models/active_sync/sync.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
module ActiveSync
class Sync

def self.sync_attributes model, args
@@sync_attributes ||= args.reduce([]) do |array, option|
case option
when :all_attributes_and_associations, :all_attributes
array + model.column_names.map(&:to_sym)
when ->(option){ option.is_a?(Hash) }
array + option[:attributes]
else
raise "Unknown sync record option #{option.inspect}"
end
end
end

#Hash used in all general sync communication for a given model.
def self.sync_record record, args
args.reduce({}) do |hash, option|
case option
when :all_attributes_and_associations, :all_attributes
hash.merge(model.attributes)
hash.merge(record.attributes)
when ->(option){ option.is_a?(Hash) }
option[:attributes]&.each { |attribute| hash[attribute] = record.call(attribute) }
option[:attributes]&.reduce(hash) { |h, attr| h[attr] = record.call(attr) }
else
raise "Unknown sync record option #{option.inspect}"
end
hash
end
end

Expand Down
4 changes: 2 additions & 2 deletions config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ActiveSync::Engine.routes.draw do
get '/:model', to: 'models#index'
get '/:model/:id', to: 'models#show'
put '/:model/:id', to: 'models#update'
post '/:model', to: 'models#create'
end
2 changes: 1 addition & 1 deletion lib/active_sync/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module ActiveSync
VERSION = '0.1.1'
VERSION = '0.2.0'
end
42 changes: 24 additions & 18 deletions lib/javascript/active-sync.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Model from './model.js'
import CamelCase from 'camelcase'
import Pluralize from 'pluralize'
import SnakeCase from "snake-case";

export default class ActiveSync {

Expand All @@ -19,8 +20,6 @@ export default class ActiveSync {
// Will add a 'sites' method to the Customer class and a 'customer' method to Site.
//



constructor( args ){
this._models = args.models || [];
this.buildModels(args.modelDescriptions)
Expand All @@ -34,7 +33,7 @@ export default class ActiveSync {
return this._models
}

// Creates the models from the modelDescription arg passed int to the constructor
// Creates the models from the modelDescription arg passed in to the constructor
buildModels(modelDescriptions){
let modelNames = Object.keys(modelDescriptions || {});

Expand All @@ -45,28 +44,35 @@ export default class ActiveSync {
})

this._models.forEach((model) => {
((modelDescriptions[model.className] || {}).belongsTo || []).forEach((association) => {
let associatedModel = this._models.find((model) => model.className === CamelCase(association, {pascalCase: true}))
model[association] = function () {
return associatedModel.find(this[association + 'Id'])
}
});

((modelDescriptions[model.className] || {}).hasMany || []).forEach((association) => {
let associatedModel = this._models.find((model) => model.className === CamelCase(Pluralize.singular(association), {pascalCase: true}))
model.prototype[association] = function () {
let associationQuery = {}
associationQuery[CamelCase(model.className) + 'Id'] = this.id
return associatedModel.where(associationQuery)
}
});
this.addBelongsToModel(modelDescriptions[model.className], model)
this.addHasManyToModel(modelDescriptions[model.className], model)
})
}

createModel(name){
let modelClass = Model
return eval(`(class ${name} extends modelClass { static className = '${name}' })`)
}

addBelongsToModel(modelDescription, model){
((modelDescription || {}).belongsTo || []).forEach((association) => {
let associatedModel = this._models.find((model) => model.className === association)
model[association] = function () {
return associatedModel.find(this[association + '_id'])
}
});
}

addHasManyToModel(modelDescription, model){
((modelDescription || {}).hasMany || []).forEach((association) => {
let associatedModel = this._models.find((model) => model.className === CamelCase(Pluralize.singular(association), {pascalCase: true}))
model.prototype[association] = function () {
let associationQuery = {}
associationQuery[SnakeCase(model.className) + '_id'] = this.id
return associatedModel.where(associationQuery)
}
});
}
}


Expand Down
Loading