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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +51 -0
  3. data/.gitignore +8 -0
  4. data/.rspec +1 -0
  5. data/.rubocop.yml +240 -0
  6. data/.rubocop_todo.yml +13 -0
  7. data/CHANGELOG.md +7 -1
  8. data/Gemfile +4 -2
  9. data/Guardfile +36 -0
  10. data/README.md +1 -1
  11. data/Rakefile +5 -3
  12. data/bin/rspec +3 -0
  13. data/lib/generators/wcc/USAGE +24 -0
  14. data/lib/generators/wcc/menu_generator.rb +67 -0
  15. data/lib/generators/wcc/templates/.keep +0 -0
  16. data/lib/generators/wcc/templates/Procfile +3 -0
  17. data/lib/generators/wcc/templates/contentful_shell_wrapper +342 -0
  18. data/lib/generators/wcc/templates/menu/generated_add_menus.ts +85 -0
  19. data/lib/generators/wcc/templates/menu/menu.rb +25 -0
  20. data/lib/generators/wcc/templates/menu/menu_button.rb +25 -0
  21. data/lib/generators/wcc/templates/release +9 -0
  22. data/lib/generators/wcc/templates/wcc_contentful.rb +18 -0
  23. data/lib/wcc/contentful.rb +93 -26
  24. data/lib/wcc/contentful/client_ext.rb +15 -0
  25. data/lib/wcc/contentful/configuration.rb +93 -0
  26. data/lib/wcc/contentful/content_type_indexer.rb +153 -0
  27. data/lib/wcc/contentful/exceptions.rb +34 -0
  28. data/lib/wcc/contentful/graphql.rb +15 -0
  29. data/lib/wcc/contentful/graphql/builder.rb +172 -0
  30. data/lib/wcc/contentful/graphql/types.rb +54 -0
  31. data/lib/wcc/contentful/helpers.rb +28 -0
  32. data/lib/wcc/contentful/indexed_representation.rb +111 -0
  33. data/lib/wcc/contentful/model.rb +24 -0
  34. data/lib/wcc/contentful/model/menu.rb +7 -0
  35. data/lib/wcc/contentful/model/menu_button.rb +15 -0
  36. data/lib/wcc/contentful/model_builder.rb +151 -0
  37. data/lib/wcc/contentful/model_validators.rb +64 -0
  38. data/lib/wcc/contentful/model_validators/dsl.rb +165 -0
  39. data/lib/wcc/contentful/simple_client.rb +127 -0
  40. data/lib/wcc/contentful/simple_client/response.rb +160 -0
  41. data/lib/wcc/contentful/store.rb +8 -0
  42. data/lib/wcc/contentful/store/cdn_adapter.rb +79 -0
  43. data/lib/wcc/contentful/store/memory_store.rb +75 -0
  44. data/lib/wcc/contentful/store/postgres_store.rb +132 -0
  45. data/lib/wcc/contentful/version.rb +3 -1
  46. data/wcc-contentful.gemspec +49 -24
  47. metadata +261 -16
  48. 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,8 @@
1
+
2
+ # frozen_string_literal: true
3
+
4
+ require_relative 'store/memory_store'
5
+ require_relative 'store/cdn_adapter'
6
+
7
+ # required dynamically if they select the 'postgres' store option
8
+ # require_relative 'store/postgres_store'
@@ -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