wcc-contentful 0.0.3 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.circleci/config.yml +51 -0
- data/.gitignore +8 -0
- data/.rspec +1 -0
- data/.rubocop.yml +240 -0
- data/.rubocop_todo.yml +13 -0
- data/CHANGELOG.md +7 -1
- data/Gemfile +4 -2
- data/Guardfile +36 -0
- data/README.md +1 -1
- data/Rakefile +5 -3
- data/bin/rspec +3 -0
- data/lib/generators/wcc/USAGE +24 -0
- data/lib/generators/wcc/menu_generator.rb +67 -0
- data/lib/generators/wcc/templates/.keep +0 -0
- data/lib/generators/wcc/templates/Procfile +3 -0
- data/lib/generators/wcc/templates/contentful_shell_wrapper +342 -0
- data/lib/generators/wcc/templates/menu/generated_add_menus.ts +85 -0
- data/lib/generators/wcc/templates/menu/menu.rb +25 -0
- data/lib/generators/wcc/templates/menu/menu_button.rb +25 -0
- data/lib/generators/wcc/templates/release +9 -0
- data/lib/generators/wcc/templates/wcc_contentful.rb +18 -0
- data/lib/wcc/contentful.rb +93 -26
- data/lib/wcc/contentful/client_ext.rb +15 -0
- data/lib/wcc/contentful/configuration.rb +93 -0
- data/lib/wcc/contentful/content_type_indexer.rb +153 -0
- data/lib/wcc/contentful/exceptions.rb +34 -0
- data/lib/wcc/contentful/graphql.rb +15 -0
- data/lib/wcc/contentful/graphql/builder.rb +172 -0
- data/lib/wcc/contentful/graphql/types.rb +54 -0
- data/lib/wcc/contentful/helpers.rb +28 -0
- data/lib/wcc/contentful/indexed_representation.rb +111 -0
- data/lib/wcc/contentful/model.rb +24 -0
- data/lib/wcc/contentful/model/menu.rb +7 -0
- data/lib/wcc/contentful/model/menu_button.rb +15 -0
- data/lib/wcc/contentful/model_builder.rb +151 -0
- data/lib/wcc/contentful/model_validators.rb +64 -0
- data/lib/wcc/contentful/model_validators/dsl.rb +165 -0
- data/lib/wcc/contentful/simple_client.rb +127 -0
- data/lib/wcc/contentful/simple_client/response.rb +160 -0
- data/lib/wcc/contentful/store.rb +8 -0
- data/lib/wcc/contentful/store/cdn_adapter.rb +79 -0
- data/lib/wcc/contentful/store/memory_store.rb +75 -0
- data/lib/wcc/contentful/store/postgres_store.rb +132 -0
- data/lib/wcc/contentful/version.rb +3 -1
- data/wcc-contentful.gemspec +49 -24
- metadata +261 -16
- data/lib/wcc/contentful/redirect.rb +0 -33
@@ -0,0 +1,160 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class WCC::Contentful::SimpleClient
|
4
|
+
class Response
|
5
|
+
attr_reader :raw_response
|
6
|
+
attr_reader :client
|
7
|
+
attr_reader :request
|
8
|
+
|
9
|
+
delegate :code, to: :raw_response
|
10
|
+
delegate :headers, to: :raw_response
|
11
|
+
|
12
|
+
def body
|
13
|
+
@body ||= raw_response.body.to_s
|
14
|
+
end
|
15
|
+
|
16
|
+
def raw
|
17
|
+
@raw ||= JSON.parse(body)
|
18
|
+
end
|
19
|
+
alias_method :to_json, :raw
|
20
|
+
|
21
|
+
def error_message
|
22
|
+
raw.dig('message') || "#{code}: #{raw_response.message}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(client, request, raw_response)
|
26
|
+
@client = client
|
27
|
+
@request = request
|
28
|
+
@raw_response = raw_response
|
29
|
+
@body = raw_response.body.to_s
|
30
|
+
end
|
31
|
+
|
32
|
+
def assert_ok!
|
33
|
+
return self if code >= 200 && code < 300
|
34
|
+
raise ApiError[code], self
|
35
|
+
end
|
36
|
+
|
37
|
+
def each_page(&block)
|
38
|
+
raise ArgumentError, 'Not a collection response' unless raw['items']
|
39
|
+
|
40
|
+
memoized_pages = (@memoized_pages ||= [self])
|
41
|
+
ret =
|
42
|
+
Enumerator.new do |y|
|
43
|
+
page_index = 0
|
44
|
+
current_page = self
|
45
|
+
loop do
|
46
|
+
y << current_page
|
47
|
+
|
48
|
+
skip_amt = current_page.raw['items'].length + current_page.raw['skip']
|
49
|
+
break if current_page.raw['items'].empty? || skip_amt >= current_page.raw['total']
|
50
|
+
|
51
|
+
page_index += 1
|
52
|
+
if page_index < memoized_pages.length
|
53
|
+
current_page = memoized_pages[page_index]
|
54
|
+
else
|
55
|
+
current_page = @client.get(
|
56
|
+
@request[:url],
|
57
|
+
(@request[:query] || {}).merge({ skip: skip_amt })
|
58
|
+
)
|
59
|
+
current_page.assert_ok!
|
60
|
+
memoized_pages.push(current_page)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
if block_given?
|
66
|
+
ret.map(&block)
|
67
|
+
else
|
68
|
+
ret.lazy
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def items
|
73
|
+
each_page.flat_map do |page|
|
74
|
+
page.raw['items']
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def count
|
79
|
+
raw['total']
|
80
|
+
end
|
81
|
+
|
82
|
+
def first
|
83
|
+
raise ArgumentError, 'Not a collection response' unless raw['items']
|
84
|
+
raw['items'].first
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class SyncResponse < Response
|
89
|
+
def initialize(response)
|
90
|
+
super(response.client, response.request, response.raw_response)
|
91
|
+
end
|
92
|
+
|
93
|
+
def next_sync_token
|
94
|
+
@next_sync_token ||= SyncResponse.parse_sync_token(raw['nextSyncUrl'])
|
95
|
+
end
|
96
|
+
|
97
|
+
def each_page
|
98
|
+
raise ArgumentError, 'Not a collection response' unless raw['items']
|
99
|
+
|
100
|
+
memoized_pages = (@memoized_pages ||= [self])
|
101
|
+
ret =
|
102
|
+
Enumerator.new do |y|
|
103
|
+
page_index = 0
|
104
|
+
current_page = self
|
105
|
+
loop do
|
106
|
+
y << current_page
|
107
|
+
|
108
|
+
break if current_page.raw['items'].empty?
|
109
|
+
|
110
|
+
page_index += 1
|
111
|
+
if page_index < memoized_pages.length
|
112
|
+
current_page = memoized_pages[page_index]
|
113
|
+
else
|
114
|
+
current_page = @client.get(raw['nextSyncUrl'])
|
115
|
+
current_page.assert_ok!
|
116
|
+
@next_sync_token = SyncResponse.parse_sync_token(current_page.raw['nextSyncUrl'])
|
117
|
+
memoized_pages.push(current_page)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
if block_given?
|
123
|
+
ret.map(&block)
|
124
|
+
else
|
125
|
+
ret.lazy
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def count
|
130
|
+
raw['items'].length
|
131
|
+
end
|
132
|
+
|
133
|
+
def self.parse_sync_token(url)
|
134
|
+
url = URI.parse(url)
|
135
|
+
q = CGI.parse(url.query)
|
136
|
+
q['sync_token']&.first
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
class ApiError < StandardError
|
141
|
+
attr_reader :response
|
142
|
+
|
143
|
+
def self.[](code)
|
144
|
+
case code
|
145
|
+
when 404
|
146
|
+
NotFoundError
|
147
|
+
else
|
148
|
+
ApiError
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def initialize(response)
|
153
|
+
@response = response
|
154
|
+
super(response.error_message)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
class NotFoundError < ApiError
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WCC::Contentful::Store
|
4
|
+
class CDNAdapter
|
5
|
+
attr_reader :client
|
6
|
+
|
7
|
+
def initialize(client)
|
8
|
+
@client = client
|
9
|
+
end
|
10
|
+
|
11
|
+
def find(key)
|
12
|
+
entry =
|
13
|
+
begin
|
14
|
+
client.entry(key, locale: '*')
|
15
|
+
rescue WCC::Contentful::SimpleClient::NotFoundError
|
16
|
+
client.asset(key, locale: '*')
|
17
|
+
end
|
18
|
+
entry&.raw
|
19
|
+
end
|
20
|
+
|
21
|
+
def find_all
|
22
|
+
raise ArgumentError, 'use find_by content type instead'
|
23
|
+
end
|
24
|
+
|
25
|
+
def find_by(content_type:)
|
26
|
+
Query.new(@client, content_type: content_type)
|
27
|
+
end
|
28
|
+
|
29
|
+
class Query
|
30
|
+
delegate :count, to: :resolve
|
31
|
+
|
32
|
+
def first
|
33
|
+
resolve.items.first
|
34
|
+
end
|
35
|
+
|
36
|
+
def map(&block)
|
37
|
+
resolve.items.map(&block)
|
38
|
+
end
|
39
|
+
|
40
|
+
def result
|
41
|
+
raise ArgumentError, 'Not Implemented'
|
42
|
+
end
|
43
|
+
|
44
|
+
def initialize(client, relation)
|
45
|
+
raise ArgumentError, 'Client cannot be nil' unless client.present?
|
46
|
+
raise ArgumentError, 'content_type must be provided' unless relation[:content_type].present?
|
47
|
+
@client = client
|
48
|
+
@relation = relation
|
49
|
+
end
|
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
|
+
def eq(field, expected, context = nil)
|
58
|
+
locale = context[:locale] if context.present?
|
59
|
+
locale ||= 'en-US'
|
60
|
+
Query.new(@client,
|
61
|
+
@relation.merge("fields.#{field}.#{locale}" => expected))
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def resolve
|
67
|
+
return @resolve if @resolve
|
68
|
+
@resolve ||=
|
69
|
+
if @relation[:content_type] == 'Asset'
|
70
|
+
@client.assets(
|
71
|
+
{ locale: '*' }.merge!(@relation.reject { |k| k == :content_type })
|
72
|
+
)
|
73
|
+
else
|
74
|
+
@client.entries({ locale: '*' }.merge!(@relation))
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WCC::Contentful::Store
|
4
|
+
class MemoryStore
|
5
|
+
def initialize
|
6
|
+
@hash = {}
|
7
|
+
@mutex = Mutex.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def index(key, value)
|
11
|
+
value = value.deep_dup.freeze
|
12
|
+
@mutex.synchronize do
|
13
|
+
@hash[key] = value
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def keys
|
18
|
+
@mutex.synchronize { @hash.keys }
|
19
|
+
end
|
20
|
+
|
21
|
+
def find(key)
|
22
|
+
@mutex.synchronize do
|
23
|
+
@hash[key]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def find_all
|
28
|
+
Query.new(@mutex.synchronize { @hash.values })
|
29
|
+
end
|
30
|
+
|
31
|
+
def find_by(content_type:)
|
32
|
+
relation = @mutex.synchronize { @hash.values }
|
33
|
+
|
34
|
+
relation =
|
35
|
+
relation.reject do |v|
|
36
|
+
value_content_type = v.dig('sys', 'contentType', 'sys', 'id')
|
37
|
+
value_content_type.nil? || value_content_type != content_type
|
38
|
+
end
|
39
|
+
Query.new(relation)
|
40
|
+
end
|
41
|
+
|
42
|
+
class Query
|
43
|
+
delegate :first, to: :@relation
|
44
|
+
delegate :map, to: :@relation
|
45
|
+
delegate :count, to: :@relation
|
46
|
+
|
47
|
+
def result
|
48
|
+
@relation.dup
|
49
|
+
end
|
50
|
+
|
51
|
+
def initialize(relation)
|
52
|
+
@relation = relation
|
53
|
+
end
|
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
|
+
def eq(field, expected, context = nil)
|
62
|
+
locale = context[:locale] if context.present?
|
63
|
+
locale ||= 'en-US'
|
64
|
+
Query.new(@relation.select do |v|
|
65
|
+
val = v.dig('fields', field, locale)
|
66
|
+
if val.is_a? Array
|
67
|
+
val.include?(expected)
|
68
|
+
else
|
69
|
+
val == expected
|
70
|
+
end
|
71
|
+
end)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
gem 'pg', '~> 1.0'
|
4
|
+
require 'pg'
|
5
|
+
|
6
|
+
module WCC::Contentful::Store
|
7
|
+
class PostgresStore
|
8
|
+
def initialize(connection_options = nil)
|
9
|
+
connection_options ||= { dbname: 'postgres' }
|
10
|
+
@conn = PG.connect(connection_options)
|
11
|
+
PostgresStore.ensure_schema(@conn)
|
12
|
+
end
|
13
|
+
|
14
|
+
def index(key, value)
|
15
|
+
@conn.exec_prepared('index_entry', [key, value.to_json])
|
16
|
+
true
|
17
|
+
end
|
18
|
+
|
19
|
+
def keys
|
20
|
+
result = @conn.exec_prepared('select_ids')
|
21
|
+
arr = []
|
22
|
+
result.each { |r| arr << r['id'].strip }
|
23
|
+
arr
|
24
|
+
end
|
25
|
+
|
26
|
+
def find(key)
|
27
|
+
result = @conn.exec_prepared('select_entry', [key])
|
28
|
+
return if result.num_tuples == 0
|
29
|
+
JSON.parse(result.getvalue(0, 1))
|
30
|
+
end
|
31
|
+
|
32
|
+
def find_all
|
33
|
+
Query.new(@conn)
|
34
|
+
end
|
35
|
+
|
36
|
+
def find_by(content_type:)
|
37
|
+
statement = "WHERE data->'sys'->'contentType'->'sys'->>'id' = $1"
|
38
|
+
Query.new(
|
39
|
+
@conn,
|
40
|
+
statement,
|
41
|
+
[content_type]
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
class Query
|
46
|
+
def initialize(conn, statement = nil, params = nil)
|
47
|
+
@conn = conn
|
48
|
+
@statement = statement ||
|
49
|
+
"WHERE data->'sys'->>'id' IS NOT NULL"
|
50
|
+
@params = params || []
|
51
|
+
end
|
52
|
+
|
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
|
+
def eq(field, expected, context = nil)
|
60
|
+
locale = context[:locale] if context.present?
|
61
|
+
locale ||= 'en-US'
|
62
|
+
|
63
|
+
params = @params.dup
|
64
|
+
|
65
|
+
statement = @statement + " AND data->'fields'->$#{push_param(field, params)}" \
|
66
|
+
"->$#{push_param(locale, params)} ? $#{push_param(expected, params)}"
|
67
|
+
|
68
|
+
Query.new(
|
69
|
+
@conn,
|
70
|
+
statement,
|
71
|
+
params
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
def count
|
76
|
+
return @count if @count
|
77
|
+
statement = 'SELECT count(*) FROM contentful_raw ' + @statement
|
78
|
+
result = @conn.exec(statement, @params)
|
79
|
+
@count = result.getvalue(0, 0).to_i
|
80
|
+
end
|
81
|
+
|
82
|
+
def first
|
83
|
+
return @first if @first
|
84
|
+
statement = 'SELECT * FROM contentful_raw ' + @statement + ' LIMIT 1'
|
85
|
+
result = @conn.exec(statement, @params)
|
86
|
+
JSON.parse(result.getvalue(0, 1))
|
87
|
+
end
|
88
|
+
|
89
|
+
def map
|
90
|
+
arr = []
|
91
|
+
resolve.each { |row| arr << yield(JSON.parse(row['data'])) }
|
92
|
+
arr
|
93
|
+
end
|
94
|
+
|
95
|
+
def result
|
96
|
+
arr = []
|
97
|
+
resolve.each { |row| arr << JSON.parse(row['data']) }
|
98
|
+
arr
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def resolve
|
104
|
+
return @resolved if @resolved
|
105
|
+
statement = 'SELECT * FROM contentful_raw ' + @statement
|
106
|
+
@resolved = @conn.exec(statement, @params)
|
107
|
+
end
|
108
|
+
|
109
|
+
def push_param(param, params)
|
110
|
+
params << param
|
111
|
+
params.length
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.ensure_schema(conn)
|
116
|
+
conn.exec(<<~HEREDOC
|
117
|
+
CREATE TABLE IF NOT EXISTS contentful_raw (
|
118
|
+
id char(22) PRIMARY KEY,
|
119
|
+
data jsonb
|
120
|
+
);
|
121
|
+
CREATE INDEX IF NOT EXISTS contentful_raw_value_type ON contentful_raw ((data->'sys'->>'type'));
|
122
|
+
CREATE INDEX IF NOT EXISTS contentful_raw_value_content_type ON contentful_raw ((data->'sys'->'contentType'->'sys'->>'id'));
|
123
|
+
HEREDOC
|
124
|
+
)
|
125
|
+
|
126
|
+
conn.prepare('index_entry', 'INSERT INTO contentful_raw (id, data) values ($1, $2) ' \
|
127
|
+
'ON CONFLICT (id) DO UPDATE SET data = $2')
|
128
|
+
conn.prepare('select_entry', 'SELECT * FROM contentful_raw WHERE id = $1')
|
129
|
+
conn.prepare('select_ids', 'SELECT id FROM contentful_raw')
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|