synced 1.1.3 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1ab9aa2facc1afc7131e87f5a5d0172b6f1128bd
4
- data.tar.gz: c527f9a1459dace9a26aa4969a3fc21ed5e598a4
3
+ metadata.gz: f3293144a530b0b41b214912ec147152b207e4b2
4
+ data.tar.gz: a016b6c1cb3628884c5c465a2c84e9f167c83a36
5
5
  SHA512:
6
- metadata.gz: a1dcb83e4d53ac213cfa512b6682ceba9a8adef709aa4e2225c1bb83853bf7cc1d863f0e72cab10571b762a3d56f8fbdef15b8183a52427a01fedfff89568873
7
- data.tar.gz: eb8b8fab78c71c261662fbf005bdf978ccf23f787e467df154d360a4bd470c5eca8053a900fa32791ce1bd0b2055af2a07a39c48642b31b278fb5df7bd3c8516
6
+ metadata.gz: 9b42ca3a24e7d58cfd666a01de3c8c22e28bd67db53f74649a77c678e6212b5e689977d3158a96d202bc3167e0dd41d180ea858e01c9ea29639fd5e95ef2e9e4
7
+ data.tar.gz: e9da5291a6c4fbbb7a2e52078b6a4817e6b6cd631eb6acbaeefa519ace7db4ebc295af66235065561ac48c0766c8d5cb8a10b6144a1b81c565196b89d0338eba
data/README.md CHANGED
@@ -77,6 +77,13 @@ rental.synced_data.bedrooms # => 4
77
77
  rental.synced_data.rental_type # => "villa"
78
78
  ```
79
79
 
80
+ ## Synced strategies
81
+
82
+ There are currently 3 synced strategies: `:full`, `:updated_since` and `:check`.
83
+ - `:full` strategy fetches all available data each time, being simple but very inefficient in most cases.
84
+ - `:updated_since` is default strategy and syncs only changes since last sync. It's more efficient, but also more complex.
85
+ - `:check` strategy fetches everything like full one, but only compares the datas without updating anything.
86
+
80
87
  ## Synced database fields
81
88
 
82
89
  Option name | Default value | Description | Required |
@@ -89,8 +96,7 @@ Custom fields name can be configured in the `synced` statement of your model:
89
96
 
90
97
  ```ruby
91
98
  class Rental < ActiveRecord::Base
92
- synced id_key: :remote_id, data_key: :remote_data,
93
- synced_all_at_key: :remote_all_synced_at
99
+ synced id_key: :remote_id, data_key: :remote_data
94
100
  end
95
101
  ```
96
102
 
@@ -201,20 +207,30 @@ end
201
207
 
202
208
  This will map remote `:description` to local `:headline` attribute.
203
209
 
204
- ## Partial updates (using updated since parameter)
210
+ ## Partial updates (using `:updated_since` strategy)
205
211
 
206
212
  Partial updates mean that first synchronization will copy all of the remote
207
213
  objects into local database and next synchronizations will sync only
208
214
  added/changed and removed objects. This significantly improves synchronization
209
215
  time and saves network traffic.
210
216
 
211
- In order to enable it add timestamp column named `synced_all_at` to your
212
- database. Synced will automatically detect it.
213
-
214
217
  NOTE: In order it to work, given endpoint needs to support updated_since
215
218
  parameter. Check [API documentation](http://docs.api.bookingsync.com/reference/)
216
219
  for given endpoint.
217
220
 
221
+ ### Storing last sync timestamps
222
+
223
+ When using `:updated_since` sync strategy you need to store the timestamp of the last sync somewhere.
224
+ By default `Synced::Strategies::SyncedAllAtTimestampStrategy` strategy is used, which requires
225
+ `synced_all_at` column to be present in the synced model. This is simple solution but on large syncs it causes serious
226
+ overhead on updating the timestamps on all the records.
227
+
228
+ There is also a `Synced::Strategies::SyncedPerScopeTimestampStrategy`, that uses another model,
229
+ `Synced::Timestamp`, to store the synchronization timestamps. Migration can be copied from the synced dummy app
230
+ `spec/dummy/db/migrate/20160126082137_create_synced_timestamps.rb`.
231
+ This strategy is added to fix the problems with massive updates on `synced_all_at`. Proper cleanup of timestamp records
232
+ is needed once in a while with `Synced::Timestamp.cleanup` (cleans records older than 1 week).
233
+
218
234
  ### Forcing local objects to be re-synced with the API
219
235
 
220
236
  When you add a new column or change something in the synced attributes and you
@@ -392,7 +408,6 @@ Option name | Default value | Description
392
408
  ---------------------|------------------|-----------------------------------------------------------------------------------|--------|-------------|
393
409
  `:id_key` | `:synced_id` | ID of the object fetched from the API | YES | NO |
394
410
  `:data_key` | `:synced_data` | Object fetched from the API | YES | NO |
395
- `:synced_all_at_key` | `:synced_all_at` | Time of the last synchronization | YES | NO |
396
411
  `:associations` | `[]` | [Sync remote associations to local ones](#associations) | YES | NO |
397
412
  `:local_attributes` | `[]` | [Sync remote attributes to local ones](#local-attributes) | YES | NO |
398
413
  `:mapper` | `nil` | [Module used for mapping remote objects](#local-attributes-with-mapping-modules) | YES | NO |
@@ -6,11 +6,10 @@ module Synced
6
6
  #
7
7
  # @param options [Hash] Configuration options for synced. They are inherited
8
8
  # by subclasses, but can be overwritten in the subclass.
9
+ # @option options [Symbol] strategy: synchronization strategy, one of :full, :updated_since, :check.
10
+ # Defaults to :updated_since
9
11
  # @option options [Symbol] id_key: attribute name under which
10
12
  # remote object's ID is stored, default is :synced_id.
11
- # @option options [Symbol] synced_all_at_key: attribute name under which
12
- # last synchronization time is stored, default is :synced_all_at. It's only
13
- # used when only_updated option is enabled.
14
13
  # @option options [Boolean] only_updated: If true requests to API will take
15
14
  # advantage of updated_since param and fetch only created/changed/deleted
16
15
  # remote objects
@@ -34,28 +33,25 @@ module Synced
34
33
  # Works only for partial (updated_since param) synchronizations.
35
34
  # @option options [Array|Hash] delegate_attributes: Given attributes will be defined
36
35
  # on synchronized object and delegated to synced_data Hash
37
- # @option options [Hash] search_params: Given attributes and their values
36
+ # @option options [Hash] query_params: Given attributes and their values
38
37
  # which will be passed to api client to perform search
39
- def synced(options = {})
40
- options.symbolize_keys!
38
+ def synced(strategy: :updated_since, **options)
41
39
  options.assert_valid_keys(:associations, :data_key, :fields,
42
40
  :globalized_attributes, :id_key, :include, :initial_sync_since,
43
- :local_attributes, :mapper, :only_updated, :remove, :synced_all_at_key,
44
- :delegate_attributes, :search_params)
45
- class_attribute :synced_id_key, :synced_all_at_key, :synced_data_key,
41
+ :local_attributes, :mapper, :only_updated, :remove,
42
+ :delegate_attributes, :query_params, :timestamp_strategy)
43
+ class_attribute :synced_id_key, :synced_data_key,
46
44
  :synced_local_attributes, :synced_associations, :synced_only_updated,
47
45
  :synced_mapper, :synced_remove, :synced_include, :synced_fields,
48
46
  :synced_globalized_attributes, :synced_initial_sync_since, :synced_delegate_attributes,
49
- :synced_search_params
47
+ :synced_query_params, :synced_timestamp_strategy, :synced_strategy
48
+ self.synced_strategy = strategy
50
49
  self.synced_id_key = options.fetch(:id_key, :synced_id)
51
- self.synced_all_at_key = options.fetch(:synced_all_at_key,
52
- synced_column_presence(:synced_all_at))
53
50
  self.synced_data_key = options.fetch(:data_key,
54
51
  synced_column_presence(:synced_data))
55
52
  self.synced_local_attributes = options.fetch(:local_attributes, [])
56
53
  self.synced_associations = options.fetch(:associations, [])
57
- self.synced_only_updated = options.fetch(:only_updated,
58
- column_names.include?(synced_all_at_key.to_s))
54
+ self.synced_only_updated = options.fetch(:only_updated, synced_strategy == :updated_since)
59
55
  self.synced_mapper = options.fetch(:mapper, nil)
60
56
  self.synced_remove = options.fetch(:remove, false)
61
57
  self.synced_include = options.fetch(:include, [])
@@ -65,7 +61,8 @@ module Synced
65
61
  self.synced_initial_sync_since = options.fetch(:initial_sync_since,
66
62
  nil)
67
63
  self.synced_delegate_attributes = options.fetch(:delegate_attributes, [])
68
- self.synced_search_params = options.fetch(:search_params, {})
64
+ self.synced_query_params = options.fetch(:query_params, {})
65
+ self.synced_timestamp_strategy = options.fetch(:timestamp_strategy, nil)
69
66
  include Synced::DelegateAttributes
70
67
  include Synced::HasSyncedData
71
68
  end
@@ -77,7 +74,7 @@ module Synced
77
74
  # @param model_class [Class] - ActiveRecord model class to which remote objects
78
75
  # will be synchronized.
79
76
  # @param scope [ActiveRecord::Base] - Within this object scope local objects
80
- # will be synchronized. By default it's model_class.
77
+ # will be synchronized. By default it's model_class. Can be infered from active record association scope.
81
78
  # @param remove [Boolean] - If it's true all local objects within
82
79
  # current scope which are not present in the remote array will be destroyed.
83
80
  # If only_updated is enabled, ids of objects to be deleted will be taken
@@ -97,42 +94,55 @@ module Synced
97
94
  # create/remove/update rentals only within website.
98
95
  # It requires relation website.rentals to exist.
99
96
  #
100
- # Rental.synchronize(remote: remote_rentals, scope: website)
97
+ # website.rentals.synchronize(remote: remote_rentals)
101
98
  #
102
- def synchronize(options = {})
103
- options.symbolize_keys!
104
- options.assert_valid_keys(:api, :fields, :include, :remote, :remove,
105
- :scope, :strategy, :search_params, :association_sync)
99
+ def synchronize(scope: scope_from_relation, strategy: synced_strategy, **options)
100
+ options.assert_valid_keys(:api, :fields, :include, :remote, :remove, :query_params, :association_sync)
106
101
  options[:remove] = synced_remove unless options.has_key?(:remove)
107
102
  options[:include] = Array.wrap(synced_include) unless options.has_key?(:include)
108
103
  options[:fields] = Array.wrap(synced_fields) unless options.has_key?(:fields)
109
- options[:search_params] = synced_search_params unless options.has_key?(:search_params)
104
+ options[:query_params] = synced_query_params unless options.has_key?(:query_params)
110
105
  options.merge!({
106
+ scope: scope,
107
+ strategy: strategy,
111
108
  id_key: synced_id_key,
112
109
  synced_data_key: synced_data_key,
113
- synced_all_at_key: synced_all_at_key,
114
110
  data_key: synced_data_key,
115
111
  local_attributes: synced_local_attributes,
116
112
  associations: synced_associations,
117
113
  only_updated: synced_only_updated,
118
114
  mapper: synced_mapper,
119
115
  globalized_attributes: synced_globalized_attributes,
120
- initial_sync_since: synced_initial_sync_since
116
+ initial_sync_since: synced_initial_sync_since,
117
+ timestamp_strategy: synced_timestamp_strategy
121
118
  })
122
119
  Synced::Synchronizer.new(self, options).perform
123
120
  end
124
121
 
125
- # Reset synced_all_at for given scope, this forces synced to sync
122
+ # Reset last sync timestamp for given scope, this forces synced to sync
126
123
  # all the records on the next sync. Useful for cases when you add
127
124
  # a new column to be synced and you use updated since strategy for faster
128
125
  # synchronization.
129
- def reset_synced
130
- return unless synced_only_updated
131
- update_all(synced_all_at_key => nil)
126
+ def reset_synced(scope: scope_from_relation)
127
+ options = {
128
+ scope: scope,
129
+ strategy: synced_strategy,
130
+ only_updated: synced_only_updated,
131
+ initial_sync_since: synced_initial_sync_since,
132
+ timestamp_strategy: synced_timestamp_strategy
133
+ }
134
+ Synced::Synchronizer.new(self, options).reset_synced
132
135
  end
133
136
 
134
137
  private
135
138
 
139
+ # attempt to get scope from association reflection, so you could do:
140
+ # account.bookings.synchronize
141
+ # and the scope would be account
142
+ def scope_from_relation
143
+ all.proxy_association.owner if all.respond_to?(:proxy_association)
144
+ end
145
+
136
146
  def synced_column_presence(name)
137
147
  name if column_names.include?(name.to_s)
138
148
  end
@@ -21,8 +21,6 @@ module Synced
21
21
  # will be synchronized. By default it's model_class.
22
22
  # @option options [Symbol] id_key: attribute name under which
23
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
24
  # @option options [Symbol] data_key: attribute name under which remote
27
25
  # object's data is stored.
28
26
  # @option options [Array] local_attributes: Array of attributes in the remote
@@ -48,7 +46,6 @@ module Synced
48
46
  @model_class = model_class
49
47
  @scope = options[:scope]
50
48
  @id_key = options[:id_key]
51
- @synced_all_at_key = options[:synced_all_at_key]
52
49
  @data_key = options[:data_key]
53
50
  @remove = options[:remove]
54
51
  @only_updated = options[:only_updated]
@@ -64,7 +61,7 @@ module Synced
64
61
  @perform_request = options[:remote].nil? && !@association_sync
65
62
  @remote_objects = Array.wrap(options[:remote]) unless @perform_request
66
63
  @globalized_attributes = synced_attributes_as_hash(options[:globalized_attributes])
67
- @search_params = options[:search_params]
64
+ @query_params = options[:query_params]
68
65
  end
69
66
 
70
67
  def perform
@@ -93,6 +90,10 @@ module Synced
93
90
  end
94
91
  end
95
92
 
93
+ def reset_synced
94
+ RuntimeError.new("Full strategy does not support reset_synced functionality")
95
+ end
96
+
96
97
  private
97
98
 
98
99
  def synchronize_associations(remote, local_object)
@@ -184,11 +185,11 @@ module Synced
184
185
  end
185
186
  options[:fields] = @fields if @fields.present?
186
187
  options[:auto_paginate] = true
187
- end.merge(search_params)
188
+ end.merge(query_params)
188
189
  end
189
190
 
190
- def search_params
191
- Hash[@search_params.map do |param, value|
191
+ def query_params
192
+ Hash[@query_params.map do |param, value|
192
193
  final_value = value.respond_to?(:call) ? search_param_value_for_lambda(value) : value
193
194
  [param, final_value]
194
195
  end]
@@ -0,0 +1,31 @@
1
+ module Synced
2
+ module Strategies
3
+ # This is a strategy for UpdatedSince defining how to store and update synced timestamps.
4
+ # It uses synced_all_at column on model to store update time.
5
+ class SyncedAllAtTimestampStrategy
6
+ attr_reader :relation_scope
7
+
8
+ def initialize(relation_scope:, **_options)
9
+ @relation_scope = relation_scope
10
+ end
11
+
12
+ def last_synced_at
13
+ relation_scope.minimum(synced_all_at_key)
14
+ end
15
+
16
+ def update(timestamp)
17
+ relation_scope.update_all(synced_all_at_key => timestamp)
18
+ end
19
+
20
+ def reset
21
+ relation_scope.update_all(synced_all_at_key => nil)
22
+ end
23
+
24
+ private
25
+
26
+ def synced_all_at_key
27
+ :synced_all_at
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,26 @@
1
+ require 'synced/timestamp'
2
+
3
+ module Synced
4
+ module Strategies
5
+ # This is a strategy for UpdatedSince defining how to store and update synced timestamps.
6
+ # It uses a separate timestamps table to track when different models were synced in specific scopes.
7
+ class SyncedPerScopeTimestampStrategy
8
+ def initialize(scope:, model_class:, **_options)
9
+ @scope = scope
10
+ @model_class = model_class
11
+ end
12
+
13
+ def last_synced_at
14
+ Synced::Timestamp.with_scope_and_model(@scope, @model_class).last_synced_at
15
+ end
16
+
17
+ def update(timestamp)
18
+ Synced::Timestamp.with_scope_and_model(@scope, @model_class).create!(synced_at: timestamp)
19
+ end
20
+
21
+ def reset
22
+ Synced::Timestamp.with_scope_and_model(@scope, @model_class).delete_all
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,3 +1,6 @@
1
+ require "synced/strategies/synced_all_at_timestamp_strategy"
2
+ require "synced/strategies/synced_per_scope_timestamp_strategy"
3
+
1
4
  module Synced
2
5
  module Strategies
3
6
  # This strategy performs partial synchronization.
@@ -8,18 +11,24 @@ module Synced
8
11
  def initialize(model_class, options = {})
9
12
  super
10
13
  @initial_sync_since = options[:initial_sync_since]
14
+ timestampt_strategy_class = options[:timestamp_strategy] || Synced::Strategies::SyncedAllAtTimestampStrategy
15
+ @timestamp_strategy = timestampt_strategy_class.new(relation_scope: relation_scope, scope: @scope, model_class: model_class)
11
16
  end
12
17
 
13
18
  def perform
14
19
  super.tap do |local_objects|
15
- instrument("update_synced_all_at_perform.synced", model: @model_class) do
20
+ instrument("update_synced_timestamp_perform.synced", model: @model_class) do
16
21
  # TODO: it can't be Time.now. this value has to be fetched from the API as well
17
22
  # https://github.com/BookingSync/synced/issues/29
18
- relation_scope.update_all(@synced_all_at_key => Time.now)
23
+ @timestamp_strategy.update(Time.now)
19
24
  end
20
25
  end
21
26
  end
22
27
 
28
+ def reset_synced
29
+ @timestamp_strategy.reset
30
+ end
31
+
23
32
  private
24
33
 
25
34
  def api_request_options
@@ -37,7 +46,7 @@ module Synced
37
46
 
38
47
  def updated_since
39
48
  instrument("updated_since.synced") do
40
- [relation_scope.minimum(@synced_all_at_key), initial_sync_since].compact.max
49
+ [@timestamp_strategy.last_synced_at, initial_sync_since].compact.max
41
50
  end
42
51
  end
43
52
 
@@ -22,8 +22,6 @@ module Synced
22
22
  # will be synchronized. By default it's model_class.
23
23
  # @option options [Symbol] id_key: attribute name under which
24
24
  # remote object's ID is stored, default is :synced_id.
25
- # @option options [Symbol] synced_all_at_key: attribute name under which
26
- # remote object's sync time is stored, default is :synced_all_at
27
25
  # @option options [Symbol] data_key: attribute name under which remote
28
26
  # object's data is stored.
29
27
  # @option options [Array] local_attributes: Array of attributes in the remote
@@ -46,29 +44,31 @@ module Synced
46
44
  # @option options [Array|Hash] globalized_attributes: A list of attributes
47
45
  # which will be mapped with their translations.
48
46
  # @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.
51
- def initialize(model_class, options = {})
47
+ # process, possible values are :full, :updated_since, :check.
48
+ def initialize(model_class, strategy:, **options)
52
49
  @model_class = model_class
53
- @synced_all_at_key = options[:synced_all_at_key]
54
50
  @only_updated = options[:only_updated]
55
- @perform_request = options[:remote].nil?
56
- @strategy = strategy_class(options[:strategy]).new(model_class, options)
51
+ @remote = options[:remote]
52
+ @strategy = strategy_class(strategy).new(model_class, options)
57
53
  end
58
54
 
59
55
  def perform
60
56
  @strategy.perform
61
57
  end
62
58
 
59
+ def reset_synced
60
+ @strategy.reset_synced
61
+ end
62
+
63
63
  private
64
64
 
65
65
  def strategy_class(name)
66
- name ||= updated_since? ? :updated_since : :full
66
+ name = :full if force_full_strategy?
67
67
  "Synced::Strategies::#{name.to_s.classify}".constantize
68
68
  end
69
69
 
70
- def updated_since?
71
- @only_updated && @synced_all_at_key && @perform_request
70
+ def force_full_strategy?
71
+ @remote
72
72
  end
73
73
  end
74
74
  end
@@ -0,0 +1,19 @@
1
+ class Synced::Timestamp < ActiveRecord::Base
2
+ self.table_name = 'synced_timestamps'
3
+ belongs_to :parent_scope, polymorphic: true
4
+ scope :with_scope_and_model, ->(parent_scope, model_class) { where(parent_scope: parent_scope, model_class: model_class.to_s) }
5
+ validates :parent_scope, :model_class, :synced_at, presence: true
6
+ scope :old, -> { where('synced_at < ?', 1.week.ago) }
7
+
8
+ def model_class=(value)
9
+ write_attribute(:model_class, value.to_s)
10
+ end
11
+
12
+ def self.last_synced_at
13
+ maximum(:synced_at)
14
+ end
15
+
16
+ def self.cleanup
17
+ old.delete_all
18
+ end
19
+ end
@@ -1,3 +1,3 @@
1
1
  module Synced
2
- VERSION = "1.1.3"
2
+ VERSION = "1.2.0"
3
3
  end
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.1.3
4
+ version: 1.2.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-07 00:00:00.000000000 Z
12
+ date: 2016-02-01 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -184,8 +184,11 @@ files:
184
184
  - lib/synced/result_presenter.rb
185
185
  - lib/synced/strategies/check.rb
186
186
  - lib/synced/strategies/full.rb
187
+ - lib/synced/strategies/synced_all_at_timestamp_strategy.rb
188
+ - lib/synced/strategies/synced_per_scope_timestamp_strategy.rb
187
189
  - lib/synced/strategies/updated_since.rb
188
190
  - lib/synced/synchronizer.rb
191
+ - lib/synced/timestamp.rb
189
192
  - lib/synced/version.rb
190
193
  homepage: https://github.com/BookingSync/synced
191
194
  licenses:
@@ -207,7 +210,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
207
210
  version: '0'
208
211
  requirements: []
209
212
  rubyforge_project:
210
- rubygems_version: 2.4.8
213
+ rubygems_version: 2.4.3
211
214
  signing_key:
212
215
  specification_version: 4
213
216
  summary: Keep your BookingSync Application synced with BookingSync.