wcc-contentful 0.1.0 → 0.2.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/.circleci/config.yml +1 -1
- data/.gitignore +5 -0
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +8 -1
- data/Guardfile +23 -1
- data/app/controllers/wcc/contentful/application_controller.rb +7 -0
- data/app/controllers/wcc/contentful/webhook_controller.rb +30 -0
- data/app/jobs/wcc/contentful/delayed_sync_job.rb +14 -0
- data/bin/rails +14 -0
- data/config/initializers/mime_types.rb +3 -0
- data/config/routes.rb +5 -0
- data/lib/generators/wcc/menu_generator.rb +4 -4
- data/lib/generators/wcc/templates/contentful_shell_wrapper +109 -68
- data/lib/generators/wcc/templates/menu/menu.rb +4 -6
- data/lib/generators/wcc/templates/menu/menu_button.rb +4 -6
- data/lib/generators/wcc/templates/release +2 -2
- data/lib/generators/wcc/templates/wcc_contentful.rb +0 -1
- data/lib/wcc/contentful/client_ext.rb +1 -1
- data/lib/wcc/contentful/configuration.rb +76 -35
- data/lib/wcc/contentful/engine.rb +13 -0
- data/lib/wcc/contentful/exceptions.rb +6 -0
- data/lib/wcc/contentful/graphql/builder.rb +8 -3
- data/lib/wcc/contentful/helpers.rb +6 -0
- data/lib/wcc/contentful/indexed_representation.rb +31 -0
- data/lib/wcc/contentful/model.rb +82 -1
- data/lib/wcc/contentful/model_builder.rb +18 -8
- data/lib/wcc/contentful/model_validators.rb +69 -18
- data/lib/wcc/contentful/simple_client/http_adapter.rb +15 -0
- data/lib/wcc/contentful/simple_client/typhoeus_adapter.rb +30 -0
- data/lib/wcc/contentful/simple_client.rb +67 -18
- data/lib/wcc/contentful/store/base.rb +89 -0
- data/lib/wcc/contentful/store/cdn_adapter.rb +13 -19
- data/lib/wcc/contentful/store/lazy_cache_store.rb +76 -0
- data/lib/wcc/contentful/store/memory_store.rb +17 -23
- data/lib/wcc/contentful/store/postgres_store.rb +32 -19
- data/lib/wcc/contentful/store.rb +62 -0
- data/lib/wcc/contentful/version.rb +1 -1
- data/lib/wcc/contentful.rb +113 -24
- data/wcc-contentful.gemspec +4 -0
- metadata +75 -2
@@ -1,17 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'http'
|
4
|
-
|
5
3
|
require_relative 'simple_client/response'
|
6
4
|
|
7
5
|
module WCC::Contentful
|
6
|
+
##
|
7
|
+
# The SimpleClient accesses the Contentful CDN to get JSON responses,
|
8
|
+
# returning the raw JSON data as a parsed hash.
|
9
|
+
# It can be configured to access any API url and exposes only a single method,
|
10
|
+
# `get`. This method returns a WCC::Contentful::SimpleClient::Response
|
11
|
+
# that handles paging automatically.
|
12
|
+
#
|
13
|
+
# The SimpleClient by default uses 'http' to perform the gets, but any HTTP
|
14
|
+
# client can be injected by passing a proc as the `adapter:` option.
|
8
15
|
class SimpleClient
|
9
16
|
def initialize(api_url:, space:, access_token:, **options)
|
10
17
|
@api_url = URI.join(api_url, '/spaces/', space + '/')
|
11
18
|
@space = space
|
12
19
|
@access_token = access_token
|
13
20
|
|
14
|
-
@get_http = options[:
|
21
|
+
@get_http = SimpleClient.load_adapter(options[:adapter])
|
15
22
|
|
16
23
|
@options = options
|
17
24
|
@query_defaults = {}
|
@@ -26,6 +33,39 @@ module WCC::Contentful
|
|
26
33
|
get_http(url, query))
|
27
34
|
end
|
28
35
|
|
36
|
+
ADAPTERS = {
|
37
|
+
http: ['http', '> 1.0', '< 3.0'],
|
38
|
+
typhoeus: ['typhoeus', '~> 1.0']
|
39
|
+
}.freeze
|
40
|
+
|
41
|
+
def self.load_adapter(adapter)
|
42
|
+
case adapter
|
43
|
+
when nil
|
44
|
+
ADAPTERS.each do |a, spec|
|
45
|
+
begin
|
46
|
+
gem(*spec)
|
47
|
+
return load_adapter(a)
|
48
|
+
rescue Gem::LoadError
|
49
|
+
next
|
50
|
+
end
|
51
|
+
end
|
52
|
+
raise ArgumentError, 'Unable to load adapter! Please install one of '\
|
53
|
+
"#{ADAPTERS.values.map(&:join).join(',')}"
|
54
|
+
when :http
|
55
|
+
require_relative 'simple_client/http_adapter'
|
56
|
+
HttpAdapter.new
|
57
|
+
when :typhoeus
|
58
|
+
require_relative 'simple_client/typhoeus_adapter'
|
59
|
+
TyphoeusAdapter.new
|
60
|
+
else
|
61
|
+
unless adapter.respond_to?(:call)
|
62
|
+
raise ArgumentError, "Adapter #{adapter} is not invokeable! Please "\
|
63
|
+
"pass a proc or use one of #{ADAPTERS.keys}"
|
64
|
+
end
|
65
|
+
adapter
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
29
69
|
private
|
30
70
|
|
31
71
|
def get_http(url, query, headers = {}, proxy = {})
|
@@ -36,27 +76,19 @@ module WCC::Contentful
|
|
36
76
|
q = @query_defaults.dup
|
37
77
|
q = q.merge(query) if query
|
38
78
|
|
39
|
-
resp =
|
40
|
-
|
41
|
-
@get_http.call(url, q, headers, proxy)
|
42
|
-
else
|
43
|
-
default_get_http(url, q, headers, proxy)
|
44
|
-
end
|
79
|
+
resp = @get_http.call(url, q, headers, proxy)
|
80
|
+
|
45
81
|
if [301, 302, 307].include?(resp.code) && !@options[:no_follow_redirects]
|
46
82
|
resp = get_http(resp.headers['location'], nil, headers, proxy)
|
47
83
|
end
|
48
84
|
resp
|
49
85
|
end
|
50
86
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
HTTP[headers].get(url, params: query)
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
87
|
+
##
|
88
|
+
# The CDN SimpleClient accesses 'https://cdn.contentful.com' to get raw
|
89
|
+
# JSON responses. It exposes methods to query entries, assets, and content_types.
|
90
|
+
# The responses are instances of WCC::Contentful::SimpleClient::Response
|
91
|
+
# which handles paging automatically.
|
60
92
|
class Cdn < SimpleClient
|
61
93
|
def initialize(space:, access_token:, **options)
|
62
94
|
super(
|
@@ -67,31 +99,48 @@ module WCC::Contentful
|
|
67
99
|
)
|
68
100
|
end
|
69
101
|
|
102
|
+
##
|
103
|
+
# Gets an entry by ID
|
70
104
|
def entry(key, query = {})
|
71
105
|
resp = get("entries/#{key}", query)
|
72
106
|
resp.assert_ok!
|
73
107
|
end
|
74
108
|
|
109
|
+
##
|
110
|
+
# Queries entries with optional query parameters
|
75
111
|
def entries(query = {})
|
76
112
|
resp = get('entries', query)
|
77
113
|
resp.assert_ok!
|
78
114
|
end
|
79
115
|
|
116
|
+
##
|
117
|
+
# Gets an asset by ID
|
80
118
|
def asset(key, query = {})
|
81
119
|
resp = get("assets/#{key}", query)
|
82
120
|
resp.assert_ok!
|
83
121
|
end
|
84
122
|
|
123
|
+
##
|
124
|
+
# Queries assets with optional query parameters
|
85
125
|
def assets(query = {})
|
86
126
|
resp = get('assets', query)
|
87
127
|
resp.assert_ok!
|
88
128
|
end
|
89
129
|
|
130
|
+
##
|
131
|
+
# Queries content types with optional query parameters
|
90
132
|
def content_types(query = {})
|
91
133
|
resp = get('content_types', query)
|
92
134
|
resp.assert_ok!
|
93
135
|
end
|
94
136
|
|
137
|
+
##
|
138
|
+
# Accesses the Sync API to get a list of items that have changed since
|
139
|
+
# the last sync.
|
140
|
+
#
|
141
|
+
# If `sync_token` is nil, an initial sync is performed.
|
142
|
+
# Returns a WCC::Contentful::SimpleClient::SyncResponse
|
143
|
+
# which handles paging automatically.
|
95
144
|
def sync(sync_token: nil, **query)
|
96
145
|
sync_token =
|
97
146
|
if sync_token
|
@@ -0,0 +1,89 @@
|
|
1
|
+
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module WCC::Contentful::Store
|
5
|
+
class Base
|
6
|
+
def find(_id)
|
7
|
+
raise NotImplementedError, "#{self.class} does not implement #find"
|
8
|
+
end
|
9
|
+
|
10
|
+
def set(_id, _value)
|
11
|
+
raise NotImplementedError, "#{self.class} does not implement #set"
|
12
|
+
end
|
13
|
+
|
14
|
+
def delete(_id)
|
15
|
+
raise NotImplementedError, "#{self.class} does not implement #delete"
|
16
|
+
end
|
17
|
+
|
18
|
+
def index(json)
|
19
|
+
# Subclasses can override to do this in a more performant thread-safe way.
|
20
|
+
# Example: postgres_store could do this in a stored procedure for speed
|
21
|
+
mutex.with_write_lock do
|
22
|
+
prev =
|
23
|
+
case type = json.dig('sys', 'type')
|
24
|
+
when 'DeletedEntry', 'DeletedAsset'
|
25
|
+
delete(json.dig('sys', 'id'))
|
26
|
+
else
|
27
|
+
set(json.dig('sys', 'id'), json)
|
28
|
+
end
|
29
|
+
|
30
|
+
if (prev_rev = prev&.dig('sys', 'revision')) && (next_rev = json.dig('sys', 'revision'))
|
31
|
+
if next_rev < prev_rev
|
32
|
+
# Uh oh! we overwrote an entry with a prior revision. Put the previous back.
|
33
|
+
return index(prev)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
case type
|
38
|
+
when 'DeletedEntry', 'DeletedAsset'
|
39
|
+
nil
|
40
|
+
else
|
41
|
+
json
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def find_by(content_type:, filter: nil)
|
47
|
+
# default implementation - can be overridden
|
48
|
+
q = find_all(content_type: content_type)
|
49
|
+
q = q.apply(filter) if filter
|
50
|
+
q.first
|
51
|
+
end
|
52
|
+
|
53
|
+
# rubocop:disable Lint/UnusedMethodArgument
|
54
|
+
def find_all(content_type:)
|
55
|
+
raise NotImplementedError, "#{self.class} does not implement find_all"
|
56
|
+
end
|
57
|
+
# rubocop:enable Lint/UnusedMethodArgument
|
58
|
+
|
59
|
+
def initialize
|
60
|
+
@mutex = Concurrent::ReentrantReadWriteLock.new
|
61
|
+
end
|
62
|
+
|
63
|
+
protected
|
64
|
+
|
65
|
+
attr_reader :mutex
|
66
|
+
|
67
|
+
class Query
|
68
|
+
delegate :first, to: :result
|
69
|
+
delegate :map, to: :result
|
70
|
+
delegate :count, to: :result
|
71
|
+
|
72
|
+
def result
|
73
|
+
raise NotImplementedError
|
74
|
+
end
|
75
|
+
|
76
|
+
def apply(filter, context = nil)
|
77
|
+
filter.reduce(self) do |query, (field, value)|
|
78
|
+
if value.is_a?(Hash)
|
79
|
+
k = value.keys.first
|
80
|
+
raise ArgumentError, "Filter not implemented: #{value}" unless query.respond_to?(k)
|
81
|
+
query.public_send(k, field, value[k], context)
|
82
|
+
else
|
83
|
+
query.eq(field.to_s, value)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -4,7 +4,10 @@ module WCC::Contentful::Store
|
|
4
4
|
class CDNAdapter
|
5
5
|
attr_reader :client
|
6
6
|
|
7
|
+
# Intentionally not implementing write methods
|
8
|
+
|
7
9
|
def initialize(client)
|
10
|
+
super()
|
8
11
|
@client = client
|
9
12
|
end
|
10
13
|
|
@@ -16,29 +19,26 @@ module WCC::Contentful::Store
|
|
16
19
|
client.asset(key, locale: '*')
|
17
20
|
end
|
18
21
|
entry&.raw
|
22
|
+
rescue WCC::Contentful::SimpleClient::NotFoundError
|
23
|
+
nil
|
19
24
|
end
|
20
25
|
|
21
|
-
def
|
22
|
-
|
26
|
+
def find_by(content_type:, filter: nil)
|
27
|
+
# default implementation - can be overridden
|
28
|
+
q = find_all(content_type: content_type)
|
29
|
+
q = q.apply(filter) if filter
|
30
|
+
q.first
|
23
31
|
end
|
24
32
|
|
25
|
-
def
|
33
|
+
def find_all(content_type:)
|
26
34
|
Query.new(@client, content_type: content_type)
|
27
35
|
end
|
28
36
|
|
29
|
-
class Query
|
37
|
+
class Query < Base::Query
|
30
38
|
delegate :count, to: :resolve
|
31
39
|
|
32
|
-
def first
|
33
|
-
resolve.items.first
|
34
|
-
end
|
35
|
-
|
36
|
-
def map(&block)
|
37
|
-
resolve.items.map(&block)
|
38
|
-
end
|
39
|
-
|
40
40
|
def result
|
41
|
-
|
41
|
+
resolve.items
|
42
42
|
end
|
43
43
|
|
44
44
|
def initialize(client, relation)
|
@@ -48,12 +48,6 @@ module WCC::Contentful::Store
|
|
48
48
|
@relation = relation
|
49
49
|
end
|
50
50
|
|
51
|
-
def apply(filter, context = nil)
|
52
|
-
return eq(filter[:field], filter[:eq], context) if filter[:eq]
|
53
|
-
|
54
|
-
raise ArgumentError, "Filter not implemented: #{filter}"
|
55
|
-
end
|
56
|
-
|
57
51
|
def eq(field, expected, context = nil)
|
58
52
|
locale = context[:locale] if context.present?
|
59
53
|
locale ||= 'en-US'
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WCC::Contentful::Store
|
4
|
+
class LazyCacheStore
|
5
|
+
delegate :find_all, to: :@cdn
|
6
|
+
|
7
|
+
# TODO: https://zube.io/watermarkchurch/development/c/2265
|
8
|
+
# figure out how to cache the results of a find_by query, ex:
|
9
|
+
# `find_by('slug' => '/about')`
|
10
|
+
delegate :find_by, to: :@cdn
|
11
|
+
|
12
|
+
def initialize(client, cache: nil)
|
13
|
+
@cdn = CDNAdapter.new(client)
|
14
|
+
@cache = cache || ActiveSupport::Cache::MemoryStore.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def find(key)
|
18
|
+
found =
|
19
|
+
@cache.fetch(key) do
|
20
|
+
# if it's not a contentful ID don't hit the API.
|
21
|
+
# Store a nil object if we can't find the object on the CDN.
|
22
|
+
(@cdn.find(key) || nil_obj(key)) if key =~ /^\w+$/
|
23
|
+
end
|
24
|
+
|
25
|
+
case found.try(:dig, 'sys', 'type')
|
26
|
+
when 'Nil', 'DeletedEntry', 'DeletedAsset'
|
27
|
+
nil
|
28
|
+
else
|
29
|
+
found
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# #index is called whenever the sync API comes back with more data.
|
34
|
+
def index(json)
|
35
|
+
id = json.dig('sys', 'id')
|
36
|
+
return unless prev = @cache.read(id)
|
37
|
+
|
38
|
+
if (prev_rev = prev&.dig('sys', 'revision')) && (next_rev = json.dig('sys', 'revision'))
|
39
|
+
return prev if next_rev < prev_rev
|
40
|
+
end
|
41
|
+
|
42
|
+
# we also set deletes in the cache - no need to go hit the API when we know
|
43
|
+
# this is a nil object
|
44
|
+
@cache.write(id, json)
|
45
|
+
|
46
|
+
case json.dig('sys', 'type')
|
47
|
+
when 'DeletedEntry', 'DeletedAsset'
|
48
|
+
nil
|
49
|
+
else
|
50
|
+
json
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def set(key, value)
|
55
|
+
old = @cache.read(key)
|
56
|
+
@cache.write(key, value)
|
57
|
+
old
|
58
|
+
end
|
59
|
+
|
60
|
+
def delete(key)
|
61
|
+
old = @cache.read(key)
|
62
|
+
@cache.delete(key)
|
63
|
+
old
|
64
|
+
end
|
65
|
+
|
66
|
+
def nil_obj(id)
|
67
|
+
{
|
68
|
+
'sys' => {
|
69
|
+
'id' => id,
|
70
|
+
'type' => 'Nil',
|
71
|
+
'revision' => 1
|
72
|
+
}
|
73
|
+
}
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -1,35 +1,39 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module WCC::Contentful::Store
|
4
|
-
class MemoryStore
|
4
|
+
class MemoryStore < Base
|
5
5
|
def initialize
|
6
|
+
super
|
6
7
|
@hash = {}
|
7
|
-
@mutex = Mutex.new
|
8
8
|
end
|
9
9
|
|
10
|
-
def
|
10
|
+
def set(key, value)
|
11
11
|
value = value.deep_dup.freeze
|
12
|
-
|
12
|
+
mutex.with_write_lock do
|
13
|
+
old = @hash[key]
|
13
14
|
@hash[key] = value
|
15
|
+
old
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def delete(key)
|
20
|
+
mutex.with_write_lock do
|
21
|
+
@hash.delete(key)
|
14
22
|
end
|
15
23
|
end
|
16
24
|
|
17
25
|
def keys
|
18
|
-
|
26
|
+
mutex.with_read_lock { @hash.keys }
|
19
27
|
end
|
20
28
|
|
21
29
|
def find(key)
|
22
|
-
|
30
|
+
mutex.with_read_lock do
|
23
31
|
@hash[key]
|
24
32
|
end
|
25
33
|
end
|
26
34
|
|
27
|
-
def find_all
|
28
|
-
|
29
|
-
end
|
30
|
-
|
31
|
-
def find_by(content_type:)
|
32
|
-
relation = @mutex.synchronize { @hash.values }
|
35
|
+
def find_all(content_type:)
|
36
|
+
relation = mutex.with_read_lock { @hash.values }
|
33
37
|
|
34
38
|
relation =
|
35
39
|
relation.reject do |v|
|
@@ -39,11 +43,7 @@ module WCC::Contentful::Store
|
|
39
43
|
Query.new(relation)
|
40
44
|
end
|
41
45
|
|
42
|
-
class Query
|
43
|
-
delegate :first, to: :@relation
|
44
|
-
delegate :map, to: :@relation
|
45
|
-
delegate :count, to: :@relation
|
46
|
-
|
46
|
+
class Query < Base::Query
|
47
47
|
def result
|
48
48
|
@relation.dup
|
49
49
|
end
|
@@ -52,12 +52,6 @@ module WCC::Contentful::Store
|
|
52
52
|
@relation = relation
|
53
53
|
end
|
54
54
|
|
55
|
-
def apply(filter, context = nil)
|
56
|
-
return eq(filter[:field], filter[:eq], context) if filter[:eq]
|
57
|
-
|
58
|
-
raise ArgumentError, "Filter not implemented: #{filter}"
|
59
|
-
end
|
60
|
-
|
61
55
|
def eq(field, expected, context = nil)
|
62
56
|
locale = context[:locale] if context.present?
|
63
57
|
locale ||= 'en-US'
|
@@ -4,16 +4,19 @@ gem 'pg', '~> 1.0'
|
|
4
4
|
require 'pg'
|
5
5
|
|
6
6
|
module WCC::Contentful::Store
|
7
|
-
class PostgresStore
|
7
|
+
class PostgresStore < Base
|
8
8
|
def initialize(connection_options = nil)
|
9
|
+
super()
|
9
10
|
connection_options ||= { dbname: 'postgres' }
|
10
11
|
@conn = PG.connect(connection_options)
|
11
12
|
PostgresStore.ensure_schema(@conn)
|
12
13
|
end
|
13
14
|
|
14
|
-
def
|
15
|
-
@conn.exec_prepared('
|
16
|
-
|
15
|
+
def set(key, value)
|
16
|
+
result = @conn.exec_prepared('upsert_entry', [key, value.to_json])
|
17
|
+
return if result.num_tuples == 0
|
18
|
+
val = result.getvalue(0, 0)
|
19
|
+
JSON.parse(val) if val
|
17
20
|
end
|
18
21
|
|
19
22
|
def keys
|
@@ -23,17 +26,19 @@ module WCC::Contentful::Store
|
|
23
26
|
arr
|
24
27
|
end
|
25
28
|
|
26
|
-
def
|
27
|
-
result = @conn.exec_prepared('
|
29
|
+
def delete(key)
|
30
|
+
result = @conn.exec_prepared('delete_by_id', [key])
|
28
31
|
return if result.num_tuples == 0
|
29
32
|
JSON.parse(result.getvalue(0, 1))
|
30
33
|
end
|
31
34
|
|
32
|
-
def
|
33
|
-
|
35
|
+
def find(key)
|
36
|
+
result = @conn.exec_prepared('select_entry', [key])
|
37
|
+
return if result.num_tuples == 0
|
38
|
+
JSON.parse(result.getvalue(0, 1))
|
34
39
|
end
|
35
40
|
|
36
|
-
def
|
41
|
+
def find_all(content_type:)
|
37
42
|
statement = "WHERE data->'sys'->'contentType'->'sys'->>'id' = $1"
|
38
43
|
Query.new(
|
39
44
|
@conn,
|
@@ -42,7 +47,7 @@ module WCC::Contentful::Store
|
|
42
47
|
)
|
43
48
|
end
|
44
49
|
|
45
|
-
class Query
|
50
|
+
class Query < Base::Query
|
46
51
|
def initialize(conn, statement = nil, params = nil)
|
47
52
|
@conn = conn
|
48
53
|
@statement = statement ||
|
@@ -50,12 +55,6 @@ module WCC::Contentful::Store
|
|
50
55
|
@params = params || []
|
51
56
|
end
|
52
57
|
|
53
|
-
def apply(filter, context = nil)
|
54
|
-
return eq(filter[:field], filter[:eq], context) if filter[:eq]
|
55
|
-
|
56
|
-
raise ArgumentError, "Filter not implemented: #{filter}"
|
57
|
-
end
|
58
|
-
|
59
58
|
def eq(field, expected, context = nil)
|
60
59
|
locale = context[:locale] if context.present?
|
61
60
|
locale ||= 'en-US'
|
@@ -115,18 +114,32 @@ module WCC::Contentful::Store
|
|
115
114
|
def self.ensure_schema(conn)
|
116
115
|
conn.exec(<<~HEREDOC
|
117
116
|
CREATE TABLE IF NOT EXISTS contentful_raw (
|
118
|
-
id
|
117
|
+
id varchar PRIMARY KEY,
|
119
118
|
data jsonb
|
120
119
|
);
|
121
120
|
CREATE INDEX IF NOT EXISTS contentful_raw_value_type ON contentful_raw ((data->'sys'->>'type'));
|
122
121
|
CREATE INDEX IF NOT EXISTS contentful_raw_value_content_type ON contentful_raw ((data->'sys'->'contentType'->'sys'->>'id'));
|
122
|
+
|
123
|
+
DROP FUNCTION IF EXISTS "upsert_entry";
|
124
|
+
CREATE FUNCTION "upsert_entry"(_id varchar, _data jsonb) RETURNS jsonb AS $$
|
125
|
+
DECLARE
|
126
|
+
prev jsonb;
|
127
|
+
BEGIN
|
128
|
+
SELECT data FROM contentful_raw WHERE id = _id INTO prev;
|
129
|
+
INSERT INTO contentful_raw (id, data) values (_id, _data)
|
130
|
+
ON CONFLICT (id) DO
|
131
|
+
UPDATE
|
132
|
+
SET data = _data;
|
133
|
+
RETURN prev;
|
134
|
+
END;
|
135
|
+
$$ LANGUAGE 'plpgsql';
|
123
136
|
HEREDOC
|
124
137
|
)
|
125
138
|
|
126
|
-
conn.prepare('
|
127
|
-
'ON CONFLICT (id) DO UPDATE SET data = $2')
|
139
|
+
conn.prepare('upsert_entry', 'SELECT * FROM upsert_entry($1,$2)')
|
128
140
|
conn.prepare('select_entry', 'SELECT * FROM contentful_raw WHERE id = $1')
|
129
141
|
conn.prepare('select_ids', 'SELECT id FROM contentful_raw')
|
142
|
+
conn.prepare('delete_by_id', 'DELETE FROM contentful_raw WHERE id = $1 RETURNING *')
|
130
143
|
end
|
131
144
|
end
|
132
145
|
end
|
data/lib/wcc/contentful/store.rb
CHANGED
@@ -1,8 +1,70 @@
|
|
1
1
|
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
+
require_relative 'store/base'
|
4
5
|
require_relative 'store/memory_store'
|
6
|
+
require_relative 'store/lazy_cache_store'
|
5
7
|
require_relative 'store/cdn_adapter'
|
6
8
|
|
7
9
|
# required dynamically if they select the 'postgres' store option
|
8
10
|
# require_relative 'store/postgres_store'
|
11
|
+
|
12
|
+
module WCC::Contentful::Store
|
13
|
+
SYNC_STORES = {
|
14
|
+
memory: ->(_config) { WCC::Contentful::Store::MemoryStore.new },
|
15
|
+
postgres: ->(_config) {
|
16
|
+
require_relative 'store/postgres_store'
|
17
|
+
WCC::Contentful::Store::PostgresStore.new(ENV['POSTGRES_CONNECTION'])
|
18
|
+
}
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
CDN_METHODS = %i[
|
22
|
+
eager_sync
|
23
|
+
lazy_sync
|
24
|
+
direct
|
25
|
+
].freeze
|
26
|
+
|
27
|
+
Factory =
|
28
|
+
Struct.new(:config, :cdn_method, :content_delivery_params) do
|
29
|
+
def build_sync_store
|
30
|
+
unless respond_to?("build_#{cdn_method}")
|
31
|
+
raise ArgumentError, "Don't know how to build content delivery method #{cdn_method}"
|
32
|
+
end
|
33
|
+
|
34
|
+
public_send("build_#{cdn_method}", config, *content_delivery_params)
|
35
|
+
end
|
36
|
+
|
37
|
+
def validate!
|
38
|
+
unless CDN_METHODS.include?(cdn_method)
|
39
|
+
raise ArgumentError, "Please use one of #{CDN_METHODS} for 'content_delivery'"
|
40
|
+
end
|
41
|
+
|
42
|
+
return unless respond_to?("validate_#{cdn_method}")
|
43
|
+
public_send("validate_#{cdn_method}", config, *content_delivery_params)
|
44
|
+
end
|
45
|
+
|
46
|
+
def build_eager_sync(config, store = nil, *_options)
|
47
|
+
puts "store: #{store}"
|
48
|
+
store = SYNC_STORES[store].call(config) if store.is_a?(Symbol)
|
49
|
+
store || MemoryStore.new
|
50
|
+
end
|
51
|
+
|
52
|
+
def build_lazy_sync(config, *options)
|
53
|
+
WCC::Contentful::Store::LazyCacheStore.new(
|
54
|
+
config.client,
|
55
|
+
cache: ActiveSupport::Cache.lookup_store(*options)
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
def build_direct(config, *_options)
|
60
|
+
CDNAdapter.new(config.client)
|
61
|
+
end
|
62
|
+
|
63
|
+
def validate_eager_sync(_config, store = nil, *_options)
|
64
|
+
return unless store.is_a?(Symbol)
|
65
|
+
|
66
|
+
return if SYNC_STORES.keys.include?(store)
|
67
|
+
raise ArgumentError, "Please use one of #{SYNC_STORES.keys}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|