wcc-contentful 0.0.3 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|