synced 1.0.9 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/synced/model.rb +1 -1
- data/lib/synced/strategies/check.rb +54 -0
- data/lib/synced/strategies/full.rb +229 -0
- data/lib/synced/strategies/updated_since.rb +75 -0
- data/lib/synced/synchronizer.rb +19 -207
- data/lib/synced/version.rb +1 -1
- metadata +7 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4220ff2ed647669303e132040f72a13835a2ea11
|
4
|
+
data.tar.gz: 575e7b3ba5985f389655252f040b279ec80976fc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1168823d0315ebbeff7524139d86fe17defd940d2371ff6fa634471d7033d5d28eb38cca898871e009b0fe47b5e482eb215d799a0b7bc1721046ba2eea4147b5
|
7
|
+
data.tar.gz: 0f6eaf9fac8a72b73f2b001afef26521485b88fa3e3279b64dce0054b0eb33aed927379843d20de869c0daf82222b80e475a6128bd1e82199c576c7a28510202
|
data/lib/synced/model.rb
CHANGED
@@ -98,7 +98,7 @@ module Synced
|
|
98
98
|
def synchronize(options = {})
|
99
99
|
options.symbolize_keys!
|
100
100
|
options.assert_valid_keys(:api, :fields, :include, :remote, :remove,
|
101
|
-
:scope)
|
101
|
+
:scope, :strategy)
|
102
102
|
options[:remove] = synced_remove unless options.has_key?(:remove)
|
103
103
|
options[:include] = Array(synced_include) unless options.has_key?(:include)
|
104
104
|
options[:fields] = Array(synced_fields) unless options.has_key?(:fields)
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Synced
|
2
|
+
module Strategies
|
3
|
+
# This strategy doesn't do any synchronization it simply verifies if local objects are in sync
|
4
|
+
# with the remote ones (taken from the API).
|
5
|
+
class Check < Full
|
6
|
+
attr_reader :result
|
7
|
+
|
8
|
+
def initialize(model_class, options = {})
|
9
|
+
super
|
10
|
+
@result = Result.new
|
11
|
+
end
|
12
|
+
|
13
|
+
# Makes a DRY run of full synchronization. It checks and collects objects which
|
14
|
+
# * are present in the local database, but not in the API. Local AR object is
|
15
|
+
# returned - additional objects
|
16
|
+
# * are present in the API, but not in the local database, remote object is
|
17
|
+
# returned - missing objects
|
18
|
+
# * are changed in the API, but not in the local database,
|
19
|
+
# ActiveRecord::Model #changes hash is returned - changed objects
|
20
|
+
# @return [Synced::Strategies::Check::Result] Integrity check result
|
21
|
+
def perform
|
22
|
+
result.additional = remove_relation.to_a
|
23
|
+
remote_objects.map do |remote|
|
24
|
+
if local_object = local_object_by_remote_id(remote.id)
|
25
|
+
remote.extend(@mapper) if @mapper
|
26
|
+
local_object.attributes = default_attributes_mapping(remote)
|
27
|
+
local_object.attributes = local_attributes_mapping(remote)
|
28
|
+
if @globalized_attributes.present?
|
29
|
+
local_object.attributes = globalized_attributes_mapping(remote,
|
30
|
+
local_object.translations.translated_locales)
|
31
|
+
end
|
32
|
+
result.changed << local_object.changes if local_object.changed?
|
33
|
+
else
|
34
|
+
result.missing << remote
|
35
|
+
end
|
36
|
+
end
|
37
|
+
result
|
38
|
+
end
|
39
|
+
|
40
|
+
# Represents result of synchronization integrity check
|
41
|
+
class Result
|
42
|
+
attr_accessor :changed, :missing, :additional
|
43
|
+
|
44
|
+
def initialize
|
45
|
+
@changed, @missing, @additional = [], [], []
|
46
|
+
end
|
47
|
+
|
48
|
+
def passed?
|
49
|
+
changed.empty? && missing.empty? && additional.empty?
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,229 @@
|
|
1
|
+
module Synced
|
2
|
+
module Strategies
|
3
|
+
# This strategy performs full synchronization.
|
4
|
+
# It takes all the objects from the API and
|
5
|
+
# - creates missing in the local database
|
6
|
+
# - removes local objects which are missing the API
|
7
|
+
# - updates local objects which are changed in the API
|
8
|
+
# This is the base synchronization strategy.
|
9
|
+
class Full
|
10
|
+
include AttributesAsHash
|
11
|
+
|
12
|
+
# Initializes new Full sync strategy
|
13
|
+
#
|
14
|
+
# @param remote_objects [Array|NilClass] Array of objects to be synchronized
|
15
|
+
# with local database. Objects need to respond to at least :id message.
|
16
|
+
# If it's nil, then synchronizer will fetch the remote objects on it's own from the API.
|
17
|
+
# @param model_class [Class] ActiveRecord model class from which local objects
|
18
|
+
# will be created.
|
19
|
+
# @param options [Hash]
|
20
|
+
# @option options [Symbol] scope: Within this object scope local objects
|
21
|
+
# will be synchronized. By default it's model_class.
|
22
|
+
# @option options [Symbol] id_key: attribute name under which
|
23
|
+
# remote object's ID is stored, default is :synced_id.
|
24
|
+
# @option options [Symbol] synced_all_at_key: attribute name under which
|
25
|
+
# remote object's sync time is stored, default is :synced_all_at
|
26
|
+
# @option options [Symbol] data_key: attribute name under which remote
|
27
|
+
# object's data is stored.
|
28
|
+
# @option options [Array] local_attributes: Array of attributes in the remote
|
29
|
+
# object which will be mapped to local object attributes.
|
30
|
+
# @option options [Boolean] remove: If it's true all local objects within
|
31
|
+
# current scope which are not present in the remote array will be destroyed.
|
32
|
+
# If only_updated is enabled, ids of objects to be deleted will be taken
|
33
|
+
# from the meta part. By default if cancel_at column is present, all
|
34
|
+
# missing local objects will be canceled with cancel_all,
|
35
|
+
# if it's missing, all will be destroyed with destroy_all.
|
36
|
+
# You can also force method to remove local objects by passing it
|
37
|
+
# to remove: :mark_as_missing.
|
38
|
+
# @param api [BookingSync::API::Client] - API client to be used for fetching
|
39
|
+
# remote objects
|
40
|
+
# @option options [Boolean] only_updated: If true requests to API will take
|
41
|
+
# advantage of updated_since param and fetch only created/changed/deleted
|
42
|
+
# remote objects
|
43
|
+
# @option options [Module] mapper: Module class which will be used for
|
44
|
+
# mapping remote objects attributes into local object attributes
|
45
|
+
# @option options [Array|Hash] globalized_attributes: A list of attributes
|
46
|
+
# which will be mapped with their translations.
|
47
|
+
def initialize(model_class, options = {})
|
48
|
+
@model_class = model_class
|
49
|
+
@scope = options[:scope]
|
50
|
+
@id_key = options[:id_key]
|
51
|
+
@synced_all_at_key = options[:synced_all_at_key]
|
52
|
+
@data_key = options[:data_key]
|
53
|
+
@remove = options[:remove]
|
54
|
+
@only_updated = options[:only_updated]
|
55
|
+
@include = options[:include]
|
56
|
+
@local_attributes = synced_attributes_as_hash(options[:local_attributes])
|
57
|
+
@api = options[:api]
|
58
|
+
@mapper = options[:mapper].respond_to?(:call) ?
|
59
|
+
options[:mapper].call : options[:mapper]
|
60
|
+
@fields = options[:fields]
|
61
|
+
@remove = options[:remove]
|
62
|
+
@associations = Array(options[:associations])
|
63
|
+
@perform_request = options[:remote].nil?
|
64
|
+
@remote_objects = Array(options[:remote]) unless @perform_request
|
65
|
+
@globalized_attributes = synced_attributes_as_hash(options[:globalized_attributes])
|
66
|
+
end
|
67
|
+
|
68
|
+
def perform
|
69
|
+
instrument("perform.synced", model: @model_class) do
|
70
|
+
relation_scope.transaction do
|
71
|
+
instrument("remove_perform.synced", model: @model_class) do
|
72
|
+
remove_relation.send(remove_strategy) if @remove
|
73
|
+
end
|
74
|
+
instrument("sync_perform.synced", model: @model_class) do
|
75
|
+
remote_objects.map do |remote|
|
76
|
+
remote.extend(@mapper) if @mapper
|
77
|
+
local_object = local_object_by_remote_id(remote.id) || relation_scope.new
|
78
|
+
local_object.attributes = default_attributes_mapping(remote)
|
79
|
+
local_object.attributes = local_attributes_mapping(remote)
|
80
|
+
if @globalized_attributes.present?
|
81
|
+
local_object.attributes = globalized_attributes_mapping(remote,
|
82
|
+
local_object.translations.translated_locales)
|
83
|
+
end
|
84
|
+
local_object.save! if local_object.changed?
|
85
|
+
local_object.tap do |local_object|
|
86
|
+
synchronize_associations(remote, local_object)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def synchronize_associations(remote, local_object)
|
97
|
+
@associations.each do |association|
|
98
|
+
klass = association.to_s.classify.constantize
|
99
|
+
klass.synchronize(remote: remote[association], scope: local_object, remove: @remove)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def local_attributes_mapping(remote)
|
104
|
+
Hash[@local_attributes.map do |k, v|
|
105
|
+
[k, v.respond_to?(:call) ? v.call(remote) : remote.send(v)]
|
106
|
+
end]
|
107
|
+
end
|
108
|
+
|
109
|
+
def default_attributes_mapping(remote)
|
110
|
+
{}.tap do |attributes|
|
111
|
+
attributes[@id_key] = remote.id
|
112
|
+
attributes[@data_key] = remote if @data_key
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def globalized_attributes_mapping(remote, used_locales)
|
117
|
+
empty = Hash[used_locales.map { |locale| [locale.to_s, nil] }]
|
118
|
+
{}.tap do |attributes|
|
119
|
+
@globalized_attributes.each do |local_attr, remote_attr|
|
120
|
+
translations = empty.merge(remote.send(remote_attr) || {})
|
121
|
+
attributes["#{local_attr}_translations"] = translations
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Returns relation within which local objects are created/edited and removed
|
127
|
+
# If no scope is provided, the relation_scope will be class on which
|
128
|
+
# .synchronize method is called.
|
129
|
+
# If scope is provided, like: account, then relation_scope will be a relation
|
130
|
+
# account.rentals (given we run .synchronize on Rental class)
|
131
|
+
#
|
132
|
+
# @return [ActiveRecord::Relation|Class]
|
133
|
+
def relation_scope
|
134
|
+
if @scope
|
135
|
+
@model_class.unscoped { @scope.send(resource_name).scope }
|
136
|
+
else
|
137
|
+
@model_class.unscoped
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Returns api client from the closest possible source.
|
142
|
+
#
|
143
|
+
# @raise [BookingSync::API::Unauthorized] - On unauthorized user
|
144
|
+
# @return [BookingSync::API::Client] BookingSync API client
|
145
|
+
def api
|
146
|
+
return @api if @api
|
147
|
+
closest = [@scope, @scope.class, @model_class].find do |object|
|
148
|
+
object.respond_to?(:api)
|
149
|
+
end
|
150
|
+
closest.try(:api) || raise(MissingAPIClient.new(@scope, @model_class))
|
151
|
+
end
|
152
|
+
|
153
|
+
def local_object_by_remote_id(remote_id)
|
154
|
+
local_objects.find { |l| l.public_send(@id_key) == remote_id }
|
155
|
+
end
|
156
|
+
|
157
|
+
def local_objects
|
158
|
+
@local_objects ||= relation_scope.where(@id_key => remote_objects_ids).to_a
|
159
|
+
end
|
160
|
+
|
161
|
+
def remote_objects_ids
|
162
|
+
@remote_objects_ids ||= remote_objects.map(&:id)
|
163
|
+
end
|
164
|
+
|
165
|
+
def remote_objects
|
166
|
+
@remote_objects ||= @perform_request ? fetch_remote_objects : nil
|
167
|
+
end
|
168
|
+
|
169
|
+
def fetch_remote_objects
|
170
|
+
instrument("fetch_remote_objects.synced", model: @model_class) do
|
171
|
+
api.paginate(resource_name, api_request_options)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def api_request_options
|
176
|
+
{}.tap do |options|
|
177
|
+
options[:include] = @associations if @associations.present?
|
178
|
+
if @include.present?
|
179
|
+
options[:include] ||= []
|
180
|
+
options[:include] += @include
|
181
|
+
end
|
182
|
+
options[:fields] = @fields if @fields.present?
|
183
|
+
options[:auto_paginate] = true
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def resource_name
|
188
|
+
@model_class.to_s.tableize
|
189
|
+
end
|
190
|
+
|
191
|
+
def remove_strategy
|
192
|
+
@remove == true ? default_remove_strategy : @remove
|
193
|
+
end
|
194
|
+
|
195
|
+
def default_remove_strategy
|
196
|
+
if @model_class.column_names.include?("canceled_at")
|
197
|
+
:cancel_all
|
198
|
+
else
|
199
|
+
:destroy_all
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# Remove all local objects which are not present in the remote objects
|
204
|
+
def remove_relation
|
205
|
+
relation_scope.where.not(@id_key => remote_objects_ids)
|
206
|
+
end
|
207
|
+
|
208
|
+
def instrument(*args, &block)
|
209
|
+
Synced.instrumenter.instrument(*args, &block)
|
210
|
+
end
|
211
|
+
|
212
|
+
class MissingAPIClient < StandardError
|
213
|
+
def initialize(scope, model_class)
|
214
|
+
@scope = scope
|
215
|
+
@model_class = model_class
|
216
|
+
end
|
217
|
+
|
218
|
+
def message
|
219
|
+
if @scope
|
220
|
+
%Q{Missing BookingSync API client in #{@scope} object or \
|
221
|
+
#{@scope.class} class when synchronizing #{@model_class} model}
|
222
|
+
else
|
223
|
+
%Q{Missing BookingSync API client in #{@model_class} class}
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Synced
|
2
|
+
module Strategies
|
3
|
+
# This strategy performs partial synchronization.
|
4
|
+
# It fetches only changes (additions, modifications and deletions) from the API.
|
5
|
+
class UpdatedSince < Full
|
6
|
+
# @option options [Time|Proc] initial_sync_since: A point in time from which
|
7
|
+
# objects will be synchronized on first synchronization.
|
8
|
+
def initialize(model_class, options = {})
|
9
|
+
super
|
10
|
+
@initial_sync_since = options[:initial_sync_since]
|
11
|
+
end
|
12
|
+
|
13
|
+
def perform
|
14
|
+
super.tap do |local_objects|
|
15
|
+
instrument("update_synced_all_at_perform.synced", model: @model_class) do
|
16
|
+
# TODO: it can't be Time.now. this value has to be fetched from the API as well
|
17
|
+
# https://github.com/BookingSync/synced/issues/29
|
18
|
+
relation_scope.update_all(@synced_all_at_key => Time.now)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def api_request_options
|
26
|
+
super.merge(updated_since: updated_since)
|
27
|
+
end
|
28
|
+
|
29
|
+
def initial_sync_since
|
30
|
+
if @initial_sync_since.respond_to?(:call)
|
31
|
+
@initial_sync_since.arity == 0 ? @initial_sync_since.call :
|
32
|
+
@initial_sync_since.call(@scope)
|
33
|
+
else
|
34
|
+
@initial_sync_since
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def updated_since
|
39
|
+
instrument("updated_since.synced") do
|
40
|
+
[relation_scope.minimum(@synced_all_at_key), initial_sync_since].compact.max
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def deleted_remote_objects_ids
|
45
|
+
meta && meta[:deleted_ids] or raise CannotDeleteDueToNoDeletedIdsError.new(@model_class)
|
46
|
+
end
|
47
|
+
|
48
|
+
def meta
|
49
|
+
remote_objects
|
50
|
+
@meta ||= api.last_response.meta
|
51
|
+
end
|
52
|
+
|
53
|
+
# Remove all objects with ids from deleted_ids field in the meta key
|
54
|
+
def remove_relation
|
55
|
+
relation_scope.where(@id_key => deleted_remote_objects_ids)
|
56
|
+
end
|
57
|
+
|
58
|
+
class CannotDeleteDueToNoDeletedIdsError < StandardError
|
59
|
+
def initialize(model_class)
|
60
|
+
@model_class = model_class
|
61
|
+
end
|
62
|
+
|
63
|
+
def message
|
64
|
+
"Cannot delete #{pluralized_model_class}. No deleted_ids were returned in API response."
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def pluralized_model_class
|
70
|
+
@model_class.to_s.pluralize
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/lib/synced/synchronizer.rb
CHANGED
@@ -1,17 +1,20 @@
|
|
1
1
|
require 'synced/delegate_attributes'
|
2
2
|
require 'synced/attributes_as_hash'
|
3
|
+
require 'synced/strategies/full'
|
4
|
+
require 'synced/strategies/check'
|
5
|
+
require 'synced/strategies/updated_since'
|
6
|
+
|
3
7
|
# Synchronizer class which performs actual synchronization between
|
4
8
|
# local database and given array of remote objects
|
5
9
|
module Synced
|
6
10
|
class Synchronizer
|
7
|
-
|
8
|
-
attr_reader :id_key
|
11
|
+
attr_reader :strategy
|
9
12
|
|
10
13
|
# Initializes a new Synchronizer
|
11
14
|
#
|
12
15
|
# @param remote_objects [Array|NilClass] Array of objects to be synchronized
|
13
16
|
# with local database. Objects need to respond to at least :id message.
|
14
|
-
# If it's nil, then synchronizer will fetch the remote objects on it's own.
|
17
|
+
# If it's nil, then synchronizer will fetch the remote objects on it's own from the API.
|
15
18
|
# @param model_class [Class] ActiveRecord model class from which local objects
|
16
19
|
# will be created.
|
17
20
|
# @param options [Hash]
|
@@ -42,221 +45,30 @@ module Synced
|
|
42
45
|
# mapping remote objects attributes into local object attributes
|
43
46
|
# @option options [Array|Hash] globalized_attributes: A list of attributes
|
44
47
|
# which will be mapped with their translations.
|
45
|
-
# @option options [
|
46
|
-
#
|
47
|
-
#
|
48
|
+
# @option options [Symbol] strategy: Strategy to be used for synchronization
|
49
|
+
# process, possible values are :full, :updated_since, :check and nil. Default
|
50
|
+
# is nil, so strategy will be chosen automatically.
|
48
51
|
def initialize(model_class, options = {})
|
49
|
-
@model_class
|
50
|
-
@
|
51
|
-
@
|
52
|
-
@
|
53
|
-
@
|
54
|
-
@remove = options[:remove]
|
55
|
-
@only_updated = options[:only_updated]
|
56
|
-
@include = options[:include]
|
57
|
-
@local_attributes = synced_attributes_as_hash(options[:local_attributes])
|
58
|
-
@api = options[:api]
|
59
|
-
@mapper = options[:mapper].respond_to?(:call) ?
|
60
|
-
options[:mapper].call : options[:mapper]
|
61
|
-
@fields = options[:fields]
|
62
|
-
@remove = options[:remove]
|
63
|
-
@associations = Array(options[:associations])
|
64
|
-
@perform_request = options[:remote].nil?
|
65
|
-
@remote_objects = Array(options[:remote]) unless @perform_request
|
66
|
-
@globalized_attributes = synced_attributes_as_hash(options[:globalized_attributes])
|
67
|
-
@initial_sync_since = options[:initial_sync_since]
|
52
|
+
@model_class = model_class
|
53
|
+
@synced_all_at_key = options[:synced_all_at_key]
|
54
|
+
@only_updated = options[:only_updated]
|
55
|
+
@perform_request = options[:remote].nil?
|
56
|
+
@strategy = strategy_class(options[:strategy]).new(model_class, options)
|
68
57
|
end
|
69
58
|
|
70
59
|
def perform
|
71
|
-
|
72
|
-
relation_scope.transaction do
|
73
|
-
instrument("remove_perform.synced", model: @model_class) do
|
74
|
-
remove_relation.send(remove_strategy) if @remove
|
75
|
-
end
|
76
|
-
instrument("sync_perform.synced", model: @model_class) do
|
77
|
-
remote_objects.map do |remote|
|
78
|
-
remote.extend(@mapper) if @mapper
|
79
|
-
local_object = local_object_by_remote_id(remote.id) || relation_scope.new
|
80
|
-
local_object.attributes = default_attributes_mapping(remote)
|
81
|
-
local_object.attributes = local_attributes_mapping(remote)
|
82
|
-
if @globalized_attributes.present?
|
83
|
-
local_object.attributes = globalized_attributes_mapping(remote,
|
84
|
-
local_object.translations.translated_locales)
|
85
|
-
end
|
86
|
-
local_object.save! if local_object.changed?
|
87
|
-
local_object.tap do |local_object|
|
88
|
-
@associations.each do |association|
|
89
|
-
klass = association.to_s.classify.constantize
|
90
|
-
klass.synchronize(remote: remote[association], scope: local_object,
|
91
|
-
remove: @remove)
|
92
|
-
end
|
93
|
-
end
|
94
|
-
end
|
95
|
-
end.tap do |local_objects|
|
96
|
-
if updated_since_enabled?
|
97
|
-
instrument("update_synced_all_at_perform.synced", model: @model_class) do
|
98
|
-
relation_scope.update_all(@synced_all_at_key => Time.now)
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|
102
|
-
end
|
103
|
-
end
|
60
|
+
@strategy.perform
|
104
61
|
end
|
105
62
|
|
106
63
|
private
|
107
64
|
|
108
|
-
def
|
109
|
-
|
110
|
-
|
111
|
-
end]
|
112
|
-
end
|
113
|
-
|
114
|
-
def default_attributes_mapping(remote)
|
115
|
-
{}.tap do |attributes|
|
116
|
-
attributes[@id_key] = remote.id
|
117
|
-
attributes[@data_key] = remote if @data_key
|
118
|
-
end
|
119
|
-
end
|
120
|
-
|
121
|
-
def globalized_attributes_mapping(remote, used_locales)
|
122
|
-
empty = Hash[used_locales.map { |locale| [locale.to_s, nil] }]
|
123
|
-
{}.tap do |attributes|
|
124
|
-
@globalized_attributes.each do |local_attr, remote_attr|
|
125
|
-
translations = empty.merge(remote.send(remote_attr) || {})
|
126
|
-
attributes["#{local_attr}_translations"] = translations
|
127
|
-
end
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
# Returns relation within which local objects are created/edited and removed
|
132
|
-
# If no scope is provided, the relation_scope will be class on which
|
133
|
-
# .synchronize method is called.
|
134
|
-
# If scope is provided, like: account, then relation_scope will be a relation
|
135
|
-
# account.rentals (given we run .synchronize on Rental class)
|
136
|
-
#
|
137
|
-
# @return [ActiveRecord::Relation|Class]
|
138
|
-
def relation_scope
|
139
|
-
if @scope
|
140
|
-
@model_class.unscoped { @scope.send(resource_name).scope }
|
141
|
-
else
|
142
|
-
@model_class.unscoped
|
143
|
-
end
|
144
|
-
end
|
145
|
-
|
146
|
-
# Returns api client from the closest possible source.
|
147
|
-
#
|
148
|
-
# @raise [BookingSync::API::Unauthorized] - On unauthorized user
|
149
|
-
# @return [BookingSync::API::Client] BookingSync API client
|
150
|
-
def api
|
151
|
-
return @api if @api
|
152
|
-
closest = [@scope, @scope.class, @model_class].detect do |o|
|
153
|
-
o.respond_to?(:api)
|
154
|
-
end
|
155
|
-
closest && closest.api || raise(MissingAPIClient.new(@scope, @model_class))
|
156
|
-
end
|
157
|
-
|
158
|
-
def local_object_by_remote_id(remote_id)
|
159
|
-
local_objects.find { |l| l.public_send(id_key) == remote_id }
|
160
|
-
end
|
161
|
-
|
162
|
-
def local_objects
|
163
|
-
@local_objects ||= relation_scope.where(id_key => remote_objects_ids).to_a
|
164
|
-
end
|
165
|
-
|
166
|
-
def remote_objects_ids
|
167
|
-
@remote_objects_ids ||= remote_objects.map(&:id)
|
168
|
-
end
|
169
|
-
|
170
|
-
def remote_objects
|
171
|
-
@remote_objects ||= @perform_request ? fetch_remote_objects : nil
|
172
|
-
end
|
173
|
-
|
174
|
-
def deleted_remote_objects_ids
|
175
|
-
remote_objects
|
176
|
-
api.last_response.meta[:deleted_ids]
|
65
|
+
def strategy_class(name)
|
66
|
+
name ||= updated_since? ? :updated_since : :full
|
67
|
+
"Synced::Strategies::#{name.to_s.classify}".constantize
|
177
68
|
end
|
178
69
|
|
179
|
-
def
|
180
|
-
instrument("fetch_remote_objects.synced", model: @model_class) do
|
181
|
-
api.paginate(resource_name, api_request_options)
|
182
|
-
end
|
183
|
-
end
|
184
|
-
|
185
|
-
def api_request_options
|
186
|
-
{}.tap do |options|
|
187
|
-
options[:include] = @associations if @associations.present?
|
188
|
-
if @include.present?
|
189
|
-
options[:include] ||= []
|
190
|
-
options[:include] += @include
|
191
|
-
end
|
192
|
-
options[:fields] = @fields if @fields.present?
|
193
|
-
options[:updated_since] = updated_since if updated_since_enabled?
|
194
|
-
options[:auto_paginate] = true
|
195
|
-
end
|
196
|
-
end
|
197
|
-
|
198
|
-
def updated_since
|
199
|
-
instrument("updated_since.synced") do
|
200
|
-
[relation_scope.minimum(@synced_all_at_key),
|
201
|
-
initial_sync_since].compact.max
|
202
|
-
end
|
203
|
-
end
|
204
|
-
|
205
|
-
def initial_sync_since
|
206
|
-
if @initial_sync_since.respond_to?(:call)
|
207
|
-
@initial_sync_since.arity == 0 ? @initial_sync_since.call :
|
208
|
-
@initial_sync_since.call(@scope)
|
209
|
-
else
|
210
|
-
@initial_sync_since
|
211
|
-
end
|
212
|
-
end
|
213
|
-
|
214
|
-
def updated_since_enabled?
|
70
|
+
def updated_since?
|
215
71
|
@only_updated && @synced_all_at_key && @perform_request
|
216
72
|
end
|
217
|
-
|
218
|
-
def resource_name
|
219
|
-
@model_class.to_s.tableize
|
220
|
-
end
|
221
|
-
|
222
|
-
def remove_strategy
|
223
|
-
@remove == true ? default_remove_strategy : @remove
|
224
|
-
end
|
225
|
-
|
226
|
-
def default_remove_strategy
|
227
|
-
if @model_class.column_names.include?("canceled_at")
|
228
|
-
:cancel_all
|
229
|
-
else
|
230
|
-
:destroy_all
|
231
|
-
end
|
232
|
-
end
|
233
|
-
|
234
|
-
def remove_relation
|
235
|
-
if updated_since_enabled?
|
236
|
-
relation_scope.where(id_key => deleted_remote_objects_ids)
|
237
|
-
else
|
238
|
-
relation_scope.where.not(id_key => remote_objects_ids)
|
239
|
-
end
|
240
|
-
end
|
241
|
-
|
242
|
-
def instrument(*args, &block)
|
243
|
-
Synced.instrumenter.instrument(*args, &block)
|
244
|
-
end
|
245
|
-
|
246
|
-
class MissingAPIClient < StandardError
|
247
|
-
def initialize(scope, model_class)
|
248
|
-
@scope = scope
|
249
|
-
@model_class = model_class
|
250
|
-
end
|
251
|
-
|
252
|
-
def message
|
253
|
-
if @scope
|
254
|
-
%Q{Missing BookingSync API client in #{@scope} object or \
|
255
|
-
#{@scope.class} class when synchronizing #{@model_class} model}
|
256
|
-
else
|
257
|
-
%Q{Missing BookingSync API client in #{@model_class} class}
|
258
|
-
end
|
259
|
-
end
|
260
|
-
end
|
261
73
|
end
|
262
74
|
end
|
data/lib/synced/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: synced
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sebastien Grosjean
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2015-
|
12
|
+
date: 2015-06-25 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
@@ -167,6 +167,9 @@ files:
|
|
167
167
|
- lib/synced/has_synced_data.rb
|
168
168
|
- lib/synced/model.rb
|
169
169
|
- lib/synced/rails.rb
|
170
|
+
- lib/synced/strategies/check.rb
|
171
|
+
- lib/synced/strategies/full.rb
|
172
|
+
- lib/synced/strategies/updated_since.rb
|
170
173
|
- lib/synced/synchronizer.rb
|
171
174
|
- lib/synced/version.rb
|
172
175
|
homepage: https://github.com/BookingSync/synced
|
@@ -189,8 +192,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
189
192
|
version: '0'
|
190
193
|
requirements: []
|
191
194
|
rubyforge_project:
|
192
|
-
rubygems_version: 2.4.
|
195
|
+
rubygems_version: 2.4.5
|
193
196
|
signing_key:
|
194
197
|
specification_version: 4
|
195
198
|
summary: Keep your BookingSync Application synced with BookingSync.
|
196
199
|
test_files: []
|
200
|
+
has_rdoc:
|