Class: SyncableRecord

Inherits:
ActiveRecord::Base
  • Object
show all
Defined in:
lib/syncer/syncable_record.rb

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.fix_associated_ids(remote_record) ⇒ Object

finds models associated with temp ids and assigns their real id to the record



44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/syncer/syncable_record.rb', line 44

def self.fix_associated_ids(remote_record)
  remote_record.keys.each do |key|
    # if the key is of the form xxx_tmp_id
    next unless /(?<foreign_key_prefix>.*)_tmp_id$/ =~ key
    # then find the xxx class
    next unless model_class = foreign_key_prefix.capitalize.to_class
    # find the class model that matches the tmp_id
    next unless model = model_class.find_by_tmp_id(remote_record["#{foreign_key_prefix}_tmp_id"])      
    # save the actual model id 
    remote_record["#{foreign_key_prefix}_id"] = model.id
    # remove the xxx_tmp_id key & value
    remote_record.delete(key)   
  end
end

.sync(remote_state) ⇒ Object

syncs a remote data source with server data state remote state should be provided as a Hash returns a Hash response with state changes for the remote data site



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/syncer/syncable_record.rb', line 62

def self.sync(remote_state)
  # TODO we do not currently check for id's per user
  # TODO check both when creating and when accessing
  # TODO we should at some point delete the tmp_ids

  remote_saved = remote_state['saved'] || []
  remote_created = remote_state['created'] || []
  remote_deleted = remote_state['deleted'] || []
  remote_updated = remote_state['updated'] || []
  
  # build list of all the server records known to remote
  remote_existing_records = remote_saved + remote_updated + remote_deleted
  
  # group server records by new, updated and deleted as relates to remote state
  all_records = self.all
  new_records = all_records.find_all do |record|
    !remote_existing_records.detect { |remote_record| record.matches?(remote_record) }
  end
  update_records = all_records.find_all do |record|
    remote_existing_records.detect { |remote_record| record.updated?(remote_record) }
  end
  delete_records = remote_existing_records.find_all do |remote_record|
    !all_records.detect { |record| record.matches?(remote_record) }
  end    
  
  # created
  created = new_records
  
  # handle special case where we have an updated record that was deleted remotely
  # in that case we must recreate it
  special_records = []
  update_records.each do |record|
    match = remote_deleted.detect { |remote_record| record.updated?(remote_record) }
    if match
      created << record
      special_records << record
    end
  end
  
  # cleanup by removing these special case records
  # no need to remove from remote_deleted as that we will confirm
  special_records.each { |record| update_records.delete(record) }
   
  # create all new remote records and add to created list
  if remote_created
    remote_created.each do |remote_record|
      fix_associated_ids(remote_record)
      rec = self.create(remote_record)
      if rec.persisted?
        response = rec.to_rjson
        created << response
      end
    end
  end
  
  # updated
  updated = update_records
  
  # update all updated remote records and add to updated list
  if remote_updated
    remote_updated.each do |remote_record|
      fix_associated_ids(remote_record)
      rec = self.find_by_id(remote_record['id'])
      # update the record only if found and if it has not been updated on the server already        
      if rec && !rec.updated?(remote_record) 
        attrs = remote_record.dup
        attrs.delete("id")      # can't update id
        attrs.delete("version") # can't update version
        updated << rec if rec.update_attributes(attrs)
      end
    end
  end
  
  # deleted
  deleted = delete_records.map{ |r| {:id => r['id']} if r['id'] }
  deleted.delete(nil)
  
  # destroy all deleted remote records and add to deleted list
  if remote_deleted
    remote_deleted.each do |remote_record|
      rec = self.find_by_id(remote_record['id'])
      # destroy the record only if found and if it has not been updated on the server already
      if rec && !rec.updated?(remote_record)         
        rec.destroy
        deleted << { :id => remote_record['id'] } if remote_record['id']
      end
    end
  end

  response = Hash.new
  response[:create] = created unless created.empty?
  response[:update] = updated unless updated.empty?
  response[:delete] = deleted unless deleted.empty?

  response
  
rescue => error
  { :error => "#{error}" }
end

Instance Method Details

#increment_versionObject



24
25
26
# File 'lib/syncer/syncable_record.rb', line 24

def increment_version
  increment!(:version)
end

#matches?(remote_record) ⇒ Boolean

helper to match record with remote record info if remote record has version use it otherwise only id matches (for delete records)

Returns:

  • (Boolean)


30
31
32
33
34
35
36
# File 'lib/syncer/syncable_record.rb', line 30

def matches?(remote_record)
  if remote_record['version']
    id == remote_record['id'] && version >= remote_record['version']
  else
    id == remote_record['id']
  end
end

#updated?(remote_record) ⇒ Boolean

helper that checks if remote record has been updated

Returns:

  • (Boolean)


39
40
41
# File 'lib/syncer/syncable_record.rb', line 39

def updated?(remote_record)
  id == remote_record['id'] && version > remote_record['version']
end

#zero_versionObject



20
21
22
# File 'lib/syncer/syncable_record.rb', line 20

def zero_version
  self.version = 0
end