synced 1.0.9 → 1.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 +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:
|