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.
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