wcc-contentful 1.1.1 → 1.2.1

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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/wcc/contentful/webhook_controller.rb +2 -0
  3. data/app/jobs/wcc/contentful/webhook_enable_job.rb +1 -1
  4. data/lib/tasks/download_schema.rake +1 -1
  5. data/lib/wcc/contentful/active_record_shim.rb +2 -2
  6. data/lib/wcc/contentful/configuration.rb +31 -18
  7. data/lib/wcc/contentful/content_type_indexer.rb +2 -0
  8. data/lib/wcc/contentful/downloads_schema.rb +5 -4
  9. data/lib/wcc/contentful/engine.rb +2 -4
  10. data/lib/wcc/contentful/event.rb +4 -11
  11. data/lib/wcc/contentful/exceptions.rb +2 -3
  12. data/lib/wcc/contentful/indexed_representation.rb +3 -6
  13. data/lib/wcc/contentful/instrumentation.rb +2 -1
  14. data/lib/wcc/contentful/link.rb +1 -3
  15. data/lib/wcc/contentful/link_visitor.rb +2 -4
  16. data/lib/wcc/contentful/middleware/store/caching_middleware.rb +5 -9
  17. data/lib/wcc/contentful/middleware/store.rb +4 -6
  18. data/lib/wcc/contentful/model.rb +0 -6
  19. data/lib/wcc/contentful/model_api.rb +17 -12
  20. data/lib/wcc/contentful/model_builder.rb +8 -4
  21. data/lib/wcc/contentful/model_methods.rb +10 -12
  22. data/lib/wcc/contentful/model_singleton_methods.rb +4 -4
  23. data/lib/wcc/contentful/rich_text/node.rb +60 -0
  24. data/lib/wcc/contentful/rich_text.rb +105 -0
  25. data/lib/wcc/contentful/rspec.rb +1 -3
  26. data/lib/wcc/contentful/simple_client/response.rb +28 -34
  27. data/lib/wcc/contentful/simple_client.rb +11 -14
  28. data/lib/wcc/contentful/store/base.rb +5 -5
  29. data/lib/wcc/contentful/store/cdn_adapter.rb +11 -7
  30. data/lib/wcc/contentful/store/factory.rb +1 -1
  31. data/lib/wcc/contentful/store/memory_store.rb +2 -2
  32. data/lib/wcc/contentful/store/postgres_store.rb +15 -22
  33. data/lib/wcc/contentful/store/query.rb +2 -2
  34. data/lib/wcc/contentful/store/rspec_examples/include_param.rb +6 -4
  35. data/lib/wcc/contentful/store/rspec_examples/operators.rb +1 -1
  36. data/lib/wcc/contentful/sync_engine.rb +31 -15
  37. data/lib/wcc/contentful/sys.rb +2 -1
  38. data/lib/wcc/contentful/test/double.rb +3 -5
  39. data/lib/wcc/contentful/test/factory.rb +4 -6
  40. data/lib/wcc/contentful/version.rb +1 -1
  41. data/lib/wcc/contentful.rb +6 -8
  42. data/wcc-contentful.gemspec +6 -6
  43. metadata +13 -25
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WCC::Contentful::RichText
4
+ module Node
5
+ extend ActiveSupport::Concern
6
+
7
+ def keys
8
+ members.map(&:to_s)
9
+ end
10
+
11
+ included do
12
+ include Enumerable
13
+
14
+ alias_method :node_type, :nodeType
15
+
16
+ # Make the nodes read-only
17
+ undef_method :[]=
18
+ members.each do |member|
19
+ undef_method("#{member}=")
20
+ end
21
+
22
+ # Override each so it has a Hash-like interface rather than Struct-like.
23
+ # The goal being to mimic a JSON-parsed hash representation of the raw
24
+ def each
25
+ members.map do |key|
26
+ tuple = [key.to_s, self.[](key)]
27
+ yield tuple if block_given?
28
+ tuple
29
+ end
30
+ end
31
+ end
32
+
33
+ class_methods do
34
+ # Default value for node_type covers most cases
35
+ def node_type
36
+ name.demodulize.underscore.dasherize
37
+ end
38
+
39
+ def tokenize(raw, context = nil)
40
+ raise ArgumentError, "Expected '#{node_type}', got '#{raw['nodeType']}'" unless raw['nodeType'] == node_type
41
+
42
+ values =
43
+ members.map do |symbol|
44
+ val = raw[symbol.to_s]
45
+
46
+ case symbol
47
+ when :content
48
+ WCC::Contentful::RichText.tokenize(val, context)
49
+ # when :data
50
+ # TODO: resolve links...
51
+ else
52
+ val
53
+ end
54
+ end
55
+
56
+ new(*values)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './rich_text/node'
4
+
5
+ ##
6
+ # This module contains a number of structs representing nodes in a Contentful
7
+ # rich text field. When the Model layer parses a Rich Text field from
8
+ # Contentful, it is turned into a WCC::Contentful::RichText::Document node.
9
+ # The {WCC::Contentful::RichText::Document#content content} method of this
10
+ # node is an Array containing paragraph, blockquote, entry, and other nodes.
11
+ #
12
+ # The various structs in the RichText object model are designed to mimic the
13
+ # Hash interface, so that the indexing operator `#[]` and the `#dig` method
14
+ # can be used to traverse the data. The data can also be accessed by the
15
+ # attribute reader methods defined on the structs. Both of these are considered
16
+ # part of the public API of the model and will not change.
17
+ #
18
+ # In a future release we plan to implement automatic link resolution. When that
19
+ # happens, the `.data` attribute of embedded entries and assets will return a
20
+ # new class that is able to resolve the `.target` automatically into a full
21
+ # entry or asset. This future class will still respect the hash accessor methods
22
+ # `#[]`, `#dig`, `#keys`, and `#each`, so it is safe to use those.
23
+ module WCC::Contentful::RichText
24
+ ##
25
+ # Recursively converts a raw JSON-parsed hash into the RichText object model.
26
+ def self.tokenize(raw, context = nil)
27
+ return unless raw
28
+ return raw.map { |c| tokenize(c, context) } if raw.is_a?(Array)
29
+
30
+ klass =
31
+ case raw['nodeType']
32
+ when 'document'
33
+ Document
34
+ when 'paragraph'
35
+ Paragraph
36
+ when 'blockquote'
37
+ Blockquote
38
+ when 'text'
39
+ Text
40
+ when 'embedded-entry-inline'
41
+ EmbeddedEntryInline
42
+ when 'embedded-entry-block'
43
+ EmbeddedEntryBlock
44
+ when 'embedded-asset-block'
45
+ EmbeddedAssetBlock
46
+ when /heading-(\d+)/
47
+ size = Regexp.last_match(1)
48
+ const_get("Heading#{size}")
49
+ else
50
+ Unknown
51
+ end
52
+
53
+ klass.tokenize(raw, context)
54
+ end
55
+
56
+ Document =
57
+ Struct.new(:nodeType, :data, :content) do
58
+ include WCC::Contentful::RichText::Node
59
+ end
60
+
61
+ Paragraph =
62
+ Struct.new(:nodeType, :data, :content) do
63
+ include WCC::Contentful::RichText::Node
64
+ end
65
+
66
+ Blockquote =
67
+ Struct.new(:nodeType, :data, :content) do
68
+ include WCC::Contentful::RichText::Node
69
+ end
70
+
71
+ Text =
72
+ Struct.new(:nodeType, :value, :marks, :data) do
73
+ include WCC::Contentful::RichText::Node
74
+ end
75
+
76
+ EmbeddedEntryInline =
77
+ Struct.new(:nodeType, :data, :content) do
78
+ include WCC::Contentful::RichText::Node
79
+ end
80
+
81
+ EmbeddedEntryBlock =
82
+ Struct.new(:nodeType, :data, :content) do
83
+ include WCC::Contentful::RichText::Node
84
+ end
85
+
86
+ EmbeddedAssetBlock =
87
+ Struct.new(:nodeType, :data, :content) do
88
+ include WCC::Contentful::RichText::Node
89
+ end
90
+
91
+ (1..5).each do |i|
92
+ struct =
93
+ Struct.new(:nodeType, :data, :content) do
94
+ include WCC::Contentful::RichText::Node
95
+ end
96
+ sz = i
97
+ struct.define_singleton_method(:node_type) { "heading-#{sz}" }
98
+ const_set("Heading#{sz}", struct)
99
+ end
100
+
101
+ Unknown =
102
+ Struct.new(:nodeType, :data, :content) do
103
+ include WCC::Contentful::RichText::Node
104
+ end
105
+ end
@@ -13,9 +13,7 @@ module WCC::Contentful::RSpec
13
13
  # stubs the Model API to return that content type for `.find` and `.find_by`
14
14
  # query methods.
15
15
  def contentful_stub(const, **attrs)
16
- unless const.respond_to?(:content_type_definition)
17
- const = WCC::Contentful::Model.resolve_constant(const.to_s)
18
- end
16
+ const = WCC::Contentful::Model.resolve_constant(const.to_s) unless const.respond_to?(:content_type_definition)
19
17
  instance = contentful_create(const, **attrs)
20
18
 
21
19
  # mimic what's going on inside model_singleton_methods.rb
@@ -6,9 +6,7 @@ class WCC::Contentful::SimpleClient
6
6
  class Response
7
7
  include ::WCC::Contentful::Instrumentation
8
8
 
9
- attr_reader :raw_response
10
- attr_reader :client
11
- attr_reader :request
9
+ attr_reader :raw_response, :client, :request
12
10
 
13
11
  delegate :status, to: :raw_response
14
12
  alias_method :code, :status
@@ -26,7 +24,7 @@ class WCC::Contentful::SimpleClient
26
24
  def error_message
27
25
  parsed_message =
28
26
  begin
29
- raw.dig('message')
27
+ raw['message']
30
28
  rescue JSON::ParserError
31
29
  nil
32
30
  end
@@ -49,7 +47,6 @@ class WCC::Contentful::SimpleClient
49
47
 
50
48
  def next_page
51
49
  return unless next_page?
52
- return @next_page if @next_page
53
50
 
54
51
  query = (@request[:query] || {}).merge({
55
52
  skip: page_items.length + skip
@@ -58,7 +55,7 @@ class WCC::Contentful::SimpleClient
58
55
  _instrument 'page', url: @request[:url], query: query do
59
56
  @client.get(@request[:url], query)
60
57
  end
61
- @next_page = np.assert_ok!
58
+ np.assert_ok!
62
59
  end
63
60
 
64
61
  def initialize(client, request, raw_response)
@@ -77,16 +74,7 @@ class WCC::Contentful::SimpleClient
77
74
  def each_page(&block)
78
75
  raise ArgumentError, 'Not a collection response' unless page_items
79
76
 
80
- ret =
81
- Enumerator.new do |y|
82
- y << self
83
-
84
- if next_page?
85
- next_page.each_page.each do |page|
86
- y << page
87
- end
88
- end
89
- end
77
+ ret = PaginatingEnumerable.new(self)
90
78
 
91
79
  if block_given?
92
80
  ret.map(&block)
@@ -115,14 +103,9 @@ class WCC::Contentful::SimpleClient
115
103
 
116
104
  def includes
117
105
  @includes ||=
118
- raw.dig('includes')&.each_with_object({}) do |(_t, entries), h|
106
+ raw['includes']&.each_with_object({}) do |(_t, entries), h|
119
107
  entries.each { |e| h[e.dig('sys', 'id')] = e }
120
108
  end || {}
121
-
122
- return @includes unless @next_page
123
-
124
- # This could be more efficient - maybe not worth worrying about
125
- @includes.merge(@next_page.includes)
126
109
  end
127
110
  end
128
111
 
@@ -144,8 +127,8 @@ class WCC::Contentful::SimpleClient
144
127
  @client.get(url)
145
128
  end
146
129
 
147
- @next_page ||= SyncResponse.new(next_page)
148
- @next_page.assert_ok!
130
+ next_page = SyncResponse.new(next_page)
131
+ next_page.assert_ok!
149
132
  end
150
133
 
151
134
  def next_sync_token
@@ -160,16 +143,7 @@ class WCC::Contentful::SimpleClient
160
143
  def each_page
161
144
  raise ArgumentError, 'Not a collection response' unless page_items
162
145
 
163
- ret =
164
- Enumerator.new do |y|
165
- y << self
166
-
167
- if next_page?
168
- next_page.each_page.each do |page|
169
- y << page
170
- end
171
- end
172
- end
146
+ ret = PaginatingEnumerable.new(self)
173
147
 
174
148
  if block_given?
175
149
  ret.map(&block)
@@ -190,6 +164,26 @@ class WCC::Contentful::SimpleClient
190
164
  end
191
165
  end
192
166
 
167
+ class PaginatingEnumerable
168
+ include Enumerable
169
+
170
+ def initialize(initial_page)
171
+ raise ArgumentError, 'Must provide initial page' unless initial_page.present?
172
+
173
+ @initial_page = initial_page
174
+ end
175
+
176
+ def each
177
+ page = @initial_page
178
+ yield page
179
+
180
+ while page.next_page?
181
+ page = page.next_page
182
+ yield page
183
+ end
184
+ end
185
+ end
186
+
193
187
  class ApiError < StandardError
194
188
  attr_reader :response
195
189
 
@@ -24,8 +24,7 @@ module WCC::Contentful
24
24
  class SimpleClient
25
25
  include WCC::Contentful::Instrumentation
26
26
 
27
- attr_reader :api_url
28
- attr_reader :space
27
+ attr_reader :api_url, :space
29
28
 
30
29
  # Creates a new SimpleClient with the given configuration.
31
30
  #
@@ -41,7 +40,7 @@ module WCC::Contentful
41
40
  # @option options [Number] rate_limit_wait_timeout The maximum time to block the thread waiting
42
41
  # on a rate limit response. By default will wait for one 429 and then fail on the second 429.
43
42
  def initialize(api_url:, space:, access_token:, **options)
44
- @api_url = URI.join(api_url, '/spaces/', space + '/')
43
+ @api_url = URI.join(api_url, '/spaces/', "#{space}/")
45
44
  @space = space
46
45
  @access_token = access_token
47
46
 
@@ -57,7 +56,7 @@ module WCC::Contentful
57
56
 
58
57
  return unless options[:environment].present?
59
58
 
60
- @api_url = URI.join(@api_url, 'environments/', options[:environment] + '/')
59
+ @api_url = URI.join(@api_url, 'environments/', "#{options[:environment]}/")
61
60
  end
62
61
 
63
62
  # performs an HTTP GET request to the specified path within the configured
@@ -84,15 +83,13 @@ module WCC::Contentful
84
83
  case adapter
85
84
  when nil
86
85
  ADAPTERS.each do |a, spec|
87
- begin
88
- gem(*spec)
89
- return load_adapter(a)
90
- rescue Gem::LoadError
91
- next
92
- end
86
+ gem(*spec)
87
+ return load_adapter(a)
88
+ rescue Gem::LoadError
89
+ next
93
90
  end
94
- raise ArgumentError, 'Unable to load adapter! Please install one of '\
95
- "#{ADAPTERS.values.map(&:join).join(',')}"
91
+ raise ArgumentError, 'Unable to load adapter! Please install one of ' \
92
+ "#{ADAPTERS.values.map(&:join).join(',')}"
96
93
  when :faraday
97
94
  require 'faraday'
98
95
  ::Faraday.new do |faraday|
@@ -104,8 +101,8 @@ module WCC::Contentful
104
101
  TyphoeusAdapter.new
105
102
  else
106
103
  unless adapter.respond_to?(:get)
107
- raise ArgumentError, "Adapter #{adapter} is not invokeable! Please "\
108
- "pass use one of #{ADAPTERS.keys} or create a Faraday-compatible adapter"
104
+ raise ArgumentError, "Adapter #{adapter} is not invokeable! Please " \
105
+ "pass use one of #{ADAPTERS.keys} or create a Faraday-compatible adapter"
109
106
  end
110
107
  adapter
111
108
  end
@@ -61,11 +61,11 @@ module WCC::Contentful::Store
61
61
  set(json.dig('sys', 'id'), json)
62
62
  end
63
63
 
64
- if (prev_rev = prev&.dig('sys', 'revision')) && (next_rev = json.dig('sys', 'revision'))
65
- if next_rev < prev_rev
66
- # Uh oh! we overwrote an entry with a prior revision. Put the previous back.
67
- return index(prev)
68
- end
64
+ if (prev_rev = prev&.dig('sys', 'revision')) &&
65
+ (next_rev = json.dig('sys', 'revision')) &&
66
+ (next_rev < prev_rev)
67
+ # Uh oh! we overwrote an entry with a prior revision. Put the previous back.
68
+ return index(prev)
69
69
  end
70
70
 
71
71
  case type
@@ -3,7 +3,7 @@
3
3
  module WCC::Contentful::Store
4
4
  class CDNAdapter
5
5
  include WCC::Contentful::Store::Interface
6
- # Note: CDNAdapter should not instrument store events cause it's not a store.
6
+ # NOTE: CDNAdapter should not instrument store events cause it's not a store.
7
7
 
8
8
  attr_writer :client, :preview_client
9
9
 
@@ -72,9 +72,13 @@ module WCC::Contentful::Store
72
72
  delegate :count, to: :response
73
73
 
74
74
  def to_enum
75
- return response.items unless @options[:include]
75
+ return response.each_page.flat_map(&:page_items) unless @options[:include]
76
76
 
77
- response.items.map { |e| resolve_includes(e, @options[:include]) }
77
+ response.each_page
78
+ .flat_map { |page| page.page_items.each_with_object(page).to_a }
79
+ .map do |e, page|
80
+ resolve_includes(e, page.includes, depth: @options[:include])
81
+ end
78
82
  end
79
83
 
80
84
  def initialize(store, client:, relation:, options: nil, **extra)
@@ -160,18 +164,18 @@ module WCC::Contentful::Store
160
164
  end
161
165
  end
162
166
 
163
- def resolve_includes(entry, depth)
167
+ def resolve_includes(entry, includes, depth:)
164
168
  return entry unless entry && depth && depth > 0
165
169
 
166
170
  # Dig links out of response.includes and insert them into the entry
167
171
  WCC::Contentful::LinkVisitor.new(entry, :Link, depth: depth - 1).map! do |val|
168
- resolve_link(val)
172
+ resolve_link(val, includes)
169
173
  end
170
174
  end
171
175
 
172
- def resolve_link(val)
176
+ def resolve_link(val, includes)
173
177
  return val unless val.is_a?(Hash) && val.dig('sys', 'type') == 'Link'
174
- return val unless included = response.includes[val.dig('sys', 'id')]
178
+ return val unless included = includes[val.dig('sys', 'id')]
175
179
 
176
180
  included
177
181
  end
@@ -90,7 +90,7 @@ module WCC::Contentful::Store
90
90
  next if m[0].respond_to?(:call)
91
91
 
92
92
  raise ArgumentError, "The middleware '#{m[0]&.try(:name) || m[0]}' cannot be applied! " \
93
- 'It must respond to :call'
93
+ 'It must respond to :call'
94
94
  end
95
95
 
96
96
  validate_store!(store)
@@ -41,8 +41,8 @@ module WCC::Contentful::Store
41
41
  SUPPORTED_OPS = %i[eq ne in nin].freeze
42
42
 
43
43
  def execute(query)
44
- (query.conditions.map(&:op) - SUPPORTED_OPS).each do |op|
45
- raise ArgumentError, "Operator :#{op} not supported"
44
+ if bad_op = (query.conditions.map(&:op) - SUPPORTED_OPS).first
45
+ raise ArgumentError, "Operator :#{bad_op} not supported"
46
46
  end
47
47
 
48
48
  relation = mutex.with_read_lock { @hash.values }
@@ -45,12 +45,9 @@ module WCC::Contentful::Store
45
45
  JSON.parse(val) if val
46
46
  end
47
47
 
48
- if views_need_update?(value, previous_value)
49
- # mark dirty - we need to refresh the materialized view
50
- unless mutex.with_read_lock { @dirty }
51
- _instrument 'mark_dirty'
52
- mutex.with_write_lock { @dirty = true }
53
- end
48
+ if views_need_update?(value, previous_value) && !mutex.with_read_lock { @dirty }
49
+ _instrument 'mark_dirty'
50
+ mutex.with_write_lock { @dirty = true }
54
51
  end
55
52
 
56
53
  previous_value
@@ -105,7 +102,7 @@ module WCC::Contentful::Store
105
102
  end
106
103
  end
107
104
 
108
- logger&.debug('[PostgresStore] ' + statement + "\n" + params.inspect)
105
+ logger&.debug("[PostgresStore] #{statement}\n#{params.inspect}")
109
106
  _instrument 'exec' do
110
107
  @connection_pool.with { |conn| conn.exec(statement, params) }
111
108
  end
@@ -231,16 +228,12 @@ module WCC::Contentful::Store
231
228
  end
232
229
 
233
230
  statement =
234
- select_statement +
235
- " FROM #{table} AS t \n" +
236
- joins.join("\n") + "\n" +
237
- statement +
238
- (limit_statement || '')
231
+ "#{select_statement} FROM #{table} AS t \n#{joins.join("\n")}\n#{statement}#{limit_statement || ''}"
239
232
 
240
233
  [statement, params]
241
234
  end
242
235
 
243
- def _eq(path, expected, params)
236
+ def _eq(path, expected, params) # rubocop:disable Layout/LineContinuationLeadingSpace
244
237
  return " AND t.id = $#{push_param(expected, params)}" if path == %w[sys id]
245
238
 
246
239
  if path[3] == 'sys'
@@ -248,12 +241,12 @@ module WCC::Contentful::Store
248
241
  # into it to detect whether it contains `{ "sys": { "id" => expected } }`
249
242
  expected = { 'sys' => { path[4] => expected } }.to_json
250
243
  return ' AND fn_contentful_jsonb_any_to_jsonb_array(t.data->' \
251
- "#{quote_parameter_path(path.take(3))}) @> " \
252
- "jsonb_build_array($#{push_param(expected, params)}::jsonb)"
244
+ "#{quote_parameter_path(path.take(3))}) @> " \
245
+ "jsonb_build_array($#{push_param(expected, params)}::jsonb)"
253
246
  end
254
247
 
255
- " AND t.data->#{quote_parameter_path(path)}" \
256
- " @> to_jsonb($#{push_param(expected, params)})"
248
+ " AND t.data->#{quote_parameter_path(path)} " \
249
+ "@> to_jsonb($#{push_param(expected, params)})"
257
250
  end
258
251
 
259
252
  PARAM_TYPES = {
@@ -292,7 +285,7 @@ module WCC::Contentful::Store
292
285
  def push_join(_path, joins)
293
286
  table_alias = "s#{joins.length}"
294
287
  joins << "JOIN contentful_raw AS #{table_alias} ON " \
295
- "#{table_alias}.id=ANY(t.links)"
288
+ "#{table_alias}.id=ANY(t.links)"
296
289
  table_alias
297
290
  end
298
291
  end
@@ -326,8 +319,8 @@ module WCC::Contentful::Store
326
319
  end
327
320
 
328
321
  def schema_ensured?(conn)
329
- result = conn.exec('SELECT version FROM wcc_contentful_schema_version' \
330
- ' ORDER BY version DESC LIMIT 1')
322
+ result = conn.exec('SELECT version FROM wcc_contentful_schema_version ' \
323
+ 'ORDER BY version DESC LIMIT 1')
331
324
  return false if result.num_tuples == 0
332
325
 
333
326
  result[0]['version'].to_i >= EXPECTED_VERSION
@@ -339,8 +332,8 @@ module WCC::Contentful::Store
339
332
  def ensure_schema(conn)
340
333
  result =
341
334
  begin
342
- conn.exec('SELECT version FROM wcc_contentful_schema_version' \
343
- ' ORDER BY version DESC')
335
+ conn.exec('SELECT version FROM wcc_contentful_schema_version ' \
336
+ 'ORDER BY version DESC')
344
337
  rescue PG::UndefinedTable
345
338
  []
346
339
  end
@@ -36,7 +36,7 @@ module WCC::Contentful::Store
36
36
 
37
37
  FALSE_VALUES = [
38
38
  false, 0,
39
- '0', :"0",
39
+ '0', :'0',
40
40
  'f', :f,
41
41
  'F', :F,
42
42
  'false', :false, # rubocop:disable Lint/BooleanSymbol
@@ -237,7 +237,7 @@ module WCC::Contentful::Store
237
237
 
238
238
  Condition =
239
239
  Struct.new(:path, :op, :expected) do
240
- LINK_KEYS = %w[id type linkType].freeze
240
+ LINK_KEYS = %w[id type linkType].freeze # rubocop:disable Lint/ConstantDefinitionInBlock
241
241
 
242
242
  def path_tuples
243
243
  @path_tuples ||=
@@ -15,10 +15,12 @@ RSpec.shared_examples 'supports include param' do |feature_set|
15
15
  'fields' => {
16
16
  'name' => { 'en-US' => 'root' },
17
17
  'link' => { 'en-US' => make_link_to('deep1') },
18
- 'links' => { 'en-US' => [
19
- make_link_to('shallow3'),
20
- make_link_to('deep2')
21
- ] }
18
+ 'links' => {
19
+ 'en-US' => [
20
+ make_link_to('shallow3'),
21
+ make_link_to('deep2')
22
+ ]
23
+ }
22
24
  }
23
25
  }
24
26
  end
@@ -40,7 +40,7 @@ RSpec.shared_examples 'operators' do |feature_set|
40
40
  end
41
41
  end
42
42
 
43
- supported_operators.each do |op, value|
43
+ supported_operators.each do |op, value| # rubocop:disable Style/CombinableLoops
44
44
  next unless value
45
45
 
46
46
  it_behaves_like "supports :#{op} operator" do
@@ -31,15 +31,18 @@ module WCC::Contentful
31
31
  (@state&.dup || token_wrapper_factory(nil)).freeze
32
32
  end
33
33
 
34
- attr_reader :store
35
- attr_reader :client
34
+ attr_reader :store, :client, :options
36
35
 
37
36
  def should_sync?
38
37
  store&.index?
39
38
  end
40
39
 
41
- def initialize(state: nil, store: nil, client: nil, key: nil)
42
- @state_key = key || "sync:#{object_id}"
40
+ def initialize(client: nil, store: nil, state: nil, **options)
41
+ @options = {
42
+ key: "sync:#{object_id}"
43
+ }.merge!(options).freeze
44
+
45
+ @state_key = @options[:key] || "sync:#{object_id}"
43
46
  @client = client || WCC::Contentful::Services.instance.client
44
47
  @mutex = Mutex.new
45
48
 
@@ -53,9 +56,7 @@ module WCC::Contentful
53
56
  if state
54
57
  @state = token_wrapper_factory(state)
55
58
  raise ArgumentError, ':state param must be a String or Hash' unless @state.is_a? Hash
56
- unless @state.dig('sys', 'type') == 'token'
57
- raise ArgumentError, ':state param must be of sys.type = "token"'
58
- end
59
+ raise ArgumentError, ':state param must be of sys.type = "token"' unless @state.dig('sys', 'type') == 'token'
59
60
  end
60
61
  raise ArgumentError, 'either :state or :store must be provided' unless @state || @store
61
62
  end
@@ -144,8 +145,12 @@ module WCC::Contentful
144
145
  return unless sync_engine&.should_sync?
145
146
 
146
147
  up_to_id = nil
147
- up_to_id = event[:up_to_id] || event.dig('sys', 'id') if event
148
- sync!(up_to_id: up_to_id)
148
+ retry_count = 0
149
+ if event
150
+ up_to_id = event[:up_to_id] || event.dig('sys', 'id')
151
+ retry_count = event[:retry_count] if event[:retry_count]
152
+ end
153
+ sync!(up_to_id: up_to_id, retry_count: retry_count)
149
154
  end
150
155
 
151
156
  # Calls the Contentful Sync API and updates the configured store with the returned
@@ -156,21 +161,32 @@ module WCC::Contentful
156
161
  # If we don't find this ID in the sync data, then drop a job to try
157
162
  # the sync again after a few minutes.
158
163
  #
159
- def sync!(up_to_id: nil)
164
+ def sync!(up_to_id: nil, retry_count: 0)
160
165
  id_found, count = sync_engine.next(up_to_id: up_to_id)
161
166
 
162
167
  next_sync_token = sync_engine.state['token']
163
168
 
164
169
  logger.info "Synced #{count} entries. Next sync token:\n #{next_sync_token}"
165
- logger.info "Should enqueue again? [#{!id_found}]"
166
- # Passing nil to only enqueue the job 1 more time
167
- sync_later!(up_to_id: nil) unless id_found
170
+ unless id_found
171
+ if retry_count >= configuration.sync_retry_limit
172
+ logger.error "Unable to find item with id '#{up_to_id}' on the Sync API. " \
173
+ "Abandoning after #{retry_count} retries."
174
+ else
175
+ wait = (2**retry_count) * configuration.sync_retry_wait.seconds
176
+ logger.info "Unable to find item with id '#{up_to_id}' on the Sync API. " \
177
+ "Retrying after #{wait.inspect} " \
178
+ "(#{configuration.sync_retry_limit - retry_count} retries remaining)"
179
+
180
+ self.class.set(wait: wait)
181
+ .perform_later(up_to_id: up_to_id, retry_count: retry_count + 1)
182
+ end
183
+ end
168
184
  next_sync_token
169
185
  end
170
186
 
171
- # Drops an ActiveJob job to invoke WCC::Contentful.sync! after a given amount
187
+ # Enqueues an ActiveJob job to invoke WCC::Contentful.sync! after a given amount
172
188
  # of time.
173
- def sync_later!(up_to_id: nil, wait: 10.minutes)
189
+ def sync_later!(up_to_id: nil, wait: 10.seconds)
174
190
  self.class.set(wait: wait)
175
191
  .perform_later(up_to_id: up_to_id)
176
192
  end