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.
- checksums.yaml +4 -4
- data/app/controllers/wcc/contentful/webhook_controller.rb +2 -0
- data/app/jobs/wcc/contentful/webhook_enable_job.rb +1 -1
- data/lib/tasks/download_schema.rake +1 -1
- data/lib/wcc/contentful/active_record_shim.rb +2 -2
- data/lib/wcc/contentful/configuration.rb +31 -18
- data/lib/wcc/contentful/content_type_indexer.rb +2 -0
- data/lib/wcc/contentful/downloads_schema.rb +5 -4
- data/lib/wcc/contentful/engine.rb +2 -4
- data/lib/wcc/contentful/event.rb +4 -11
- data/lib/wcc/contentful/exceptions.rb +2 -3
- data/lib/wcc/contentful/indexed_representation.rb +3 -6
- data/lib/wcc/contentful/instrumentation.rb +2 -1
- data/lib/wcc/contentful/link.rb +1 -3
- data/lib/wcc/contentful/link_visitor.rb +2 -4
- data/lib/wcc/contentful/middleware/store/caching_middleware.rb +5 -9
- data/lib/wcc/contentful/middleware/store.rb +4 -6
- data/lib/wcc/contentful/model.rb +0 -6
- data/lib/wcc/contentful/model_api.rb +17 -12
- data/lib/wcc/contentful/model_builder.rb +8 -4
- data/lib/wcc/contentful/model_methods.rb +10 -12
- data/lib/wcc/contentful/model_singleton_methods.rb +4 -4
- data/lib/wcc/contentful/rich_text/node.rb +60 -0
- data/lib/wcc/contentful/rich_text.rb +105 -0
- data/lib/wcc/contentful/rspec.rb +1 -3
- data/lib/wcc/contentful/simple_client/response.rb +28 -34
- data/lib/wcc/contentful/simple_client.rb +11 -14
- data/lib/wcc/contentful/store/base.rb +5 -5
- data/lib/wcc/contentful/store/cdn_adapter.rb +11 -7
- data/lib/wcc/contentful/store/factory.rb +1 -1
- data/lib/wcc/contentful/store/memory_store.rb +2 -2
- data/lib/wcc/contentful/store/postgres_store.rb +15 -22
- data/lib/wcc/contentful/store/query.rb +2 -2
- data/lib/wcc/contentful/store/rspec_examples/include_param.rb +6 -4
- data/lib/wcc/contentful/store/rspec_examples/operators.rb +1 -1
- data/lib/wcc/contentful/sync_engine.rb +31 -15
- data/lib/wcc/contentful/sys.rb +2 -1
- data/lib/wcc/contentful/test/double.rb +3 -5
- data/lib/wcc/contentful/test/factory.rb +4 -6
- data/lib/wcc/contentful/version.rb +1 -1
- data/lib/wcc/contentful.rb +6 -8
- data/wcc-contentful.gemspec +6 -6
- 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
|
data/lib/wcc/contentful/rspec.rb
CHANGED
@@ -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
|
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
|
-
|
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
|
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
|
-
|
148
|
-
|
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
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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
|
-
|
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
|
-
|
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')) &&
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
-
#
|
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.
|
75
|
+
return response.each_page.flat_map(&:page_items) unless @options[:include]
|
76
76
|
|
77
|
-
response.
|
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 =
|
178
|
+
return val unless included = includes[val.dig('sys', 'id')]
|
175
179
|
|
176
180
|
included
|
177
181
|
end
|
@@ -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).
|
45
|
-
raise ArgumentError, "Operator :#{
|
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
|
-
|
50
|
-
|
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(
|
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
|
-
|
252
|
-
|
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
|
-
"
|
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
|
-
|
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
|
-
|
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
|
-
|
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', :
|
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' => {
|
19
|
-
|
20
|
-
|
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(
|
42
|
-
@
|
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
|
-
|
148
|
-
|
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
|
-
|
166
|
-
|
167
|
-
|
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
|
-
#
|
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.
|
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
|