wcc-contentful 0.2.2 → 0.3.0.pre.rc

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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +0 -1
  3. data/README.md +181 -8
  4. data/app/controllers/wcc/contentful/webhook_controller.rb +42 -2
  5. data/app/jobs/wcc/contentful/delayed_sync_job.rb +52 -3
  6. data/app/jobs/wcc/contentful/webhook_enable_job.rb +43 -0
  7. data/bin/console +4 -3
  8. data/bin/rails +2 -0
  9. data/config/initializers/mime_types.rb +10 -1
  10. data/lib/wcc/contentful.rb +14 -142
  11. data/lib/wcc/contentful/client_ext.rb +17 -4
  12. data/lib/wcc/contentful/configuration.rb +25 -84
  13. data/lib/wcc/contentful/engine.rb +19 -0
  14. data/lib/wcc/contentful/exceptions.rb +25 -28
  15. data/lib/wcc/contentful/graphql.rb +0 -1
  16. data/lib/wcc/contentful/graphql/types.rb +1 -1
  17. data/lib/wcc/contentful/helpers.rb +3 -2
  18. data/lib/wcc/contentful/indexed_representation.rb +6 -0
  19. data/lib/wcc/contentful/model.rb +68 -34
  20. data/lib/wcc/contentful/model_builder.rb +65 -67
  21. data/lib/wcc/contentful/model_methods.rb +189 -0
  22. data/lib/wcc/contentful/model_singleton_methods.rb +83 -0
  23. data/lib/wcc/contentful/services.rb +146 -0
  24. data/lib/wcc/contentful/simple_client.rb +35 -33
  25. data/lib/wcc/contentful/simple_client/http_adapter.rb +9 -0
  26. data/lib/wcc/contentful/simple_client/management.rb +81 -0
  27. data/lib/wcc/contentful/simple_client/response.rb +61 -37
  28. data/lib/wcc/contentful/simple_client/typhoeus_adapter.rb +12 -0
  29. data/lib/wcc/contentful/store.rb +45 -18
  30. data/lib/wcc/contentful/store/base.rb +128 -8
  31. data/lib/wcc/contentful/store/cdn_adapter.rb +92 -22
  32. data/lib/wcc/contentful/store/lazy_cache_store.rb +94 -9
  33. data/lib/wcc/contentful/store/memory_store.rb +13 -8
  34. data/lib/wcc/contentful/store/postgres_store.rb +44 -11
  35. data/lib/wcc/contentful/sys.rb +28 -0
  36. data/lib/wcc/contentful/version.rb +1 -1
  37. data/wcc-contentful.gemspec +3 -9
  38. metadata +87 -107
  39. data/.circleci/config.yml +0 -51
  40. data/.gitignore +0 -26
  41. data/.rubocop.yml +0 -243
  42. data/.rubocop_todo.yml +0 -13
  43. data/.travis.yml +0 -5
  44. data/CHANGELOG.md +0 -45
  45. data/CODE_OF_CONDUCT.md +0 -74
  46. data/Guardfile +0 -58
  47. data/LICENSE.txt +0 -21
  48. data/Rakefile +0 -8
  49. data/lib/generators/wcc/USAGE +0 -24
  50. data/lib/generators/wcc/model_generator.rb +0 -90
  51. data/lib/generators/wcc/templates/.keep +0 -0
  52. data/lib/generators/wcc/templates/Procfile +0 -3
  53. data/lib/generators/wcc/templates/contentful_shell_wrapper +0 -385
  54. data/lib/generators/wcc/templates/menu/generated_add_menus.ts +0 -90
  55. data/lib/generators/wcc/templates/menu/models/menu.rb +0 -23
  56. data/lib/generators/wcc/templates/menu/models/menu_button.rb +0 -23
  57. data/lib/generators/wcc/templates/page/generated_add_pages.ts +0 -50
  58. data/lib/generators/wcc/templates/page/models/page.rb +0 -23
  59. data/lib/generators/wcc/templates/release +0 -9
  60. data/lib/generators/wcc/templates/wcc_contentful.rb +0 -17
  61. data/lib/wcc/contentful/model/menu.rb +0 -7
  62. data/lib/wcc/contentful/model/menu_button.rb +0 -15
  63. data/lib/wcc/contentful/model/page.rb +0 -8
  64. data/lib/wcc/contentful/model/redirect.rb +0 -19
  65. data/lib/wcc/contentful/model_validators.rb +0 -115
  66. data/lib/wcc/contentful/model_validators/dsl.rb +0 -165
@@ -11,63 +11,133 @@ module WCC::Contentful::Store
11
11
  @client = client
12
12
  end
13
13
 
14
- def find(key)
14
+ def find(key, hint: nil, **options)
15
+ options = { locale: '*' }.merge!(options || {})
15
16
  entry =
16
- begin
17
- client.entry(key, locale: '*')
18
- rescue WCC::Contentful::SimpleClient::NotFoundError
19
- client.asset(key, locale: '*')
17
+ if hint
18
+ client.public_send(hint.underscore, key, options)
19
+ else
20
+ begin
21
+ client.entry(key, options)
22
+ rescue WCC::Contentful::SimpleClient::NotFoundError
23
+ client.asset(key, options)
24
+ end
20
25
  end
21
26
  entry&.raw
22
27
  rescue WCC::Contentful::SimpleClient::NotFoundError
23
28
  nil
24
29
  end
25
30
 
26
- def find_by(content_type:, filter: nil)
31
+ def find_by(content_type:, filter: nil, options: nil)
27
32
  # default implementation - can be overridden
28
- q = find_all(content_type: content_type)
33
+ q = find_all(content_type: content_type, options: { limit: 1 }.merge!(options || {}))
29
34
  q = q.apply(filter) if filter
30
35
  q.first
31
36
  end
32
37
 
33
- def find_all(content_type:)
34
- Query.new(@client, content_type: content_type)
38
+ def find_all(content_type:, options: nil)
39
+ Query.new(
40
+ store: self,
41
+ client: @client,
42
+ relation: { content_type: content_type },
43
+ options: options
44
+ )
35
45
  end
36
46
 
37
47
  class Query < Base::Query
38
- delegate :count, to: :resolve
48
+ delegate :count, to: :response
39
49
 
40
50
  def result
41
- resolve.items
51
+ return response.items unless @options[:include]
52
+
53
+ response.items.map { |e| resolve_includes(e, @options[:include]) }
42
54
  end
43
55
 
44
- def initialize(client, relation)
56
+ def initialize(store:, client:, relation:, options: nil, **extra)
45
57
  raise ArgumentError, 'Client cannot be nil' unless client.present?
46
58
  raise ArgumentError, 'content_type must be provided' unless relation[:content_type].present?
59
+
60
+ super(store)
47
61
  @client = client
48
62
  @relation = relation
63
+ @options = options || {}
64
+ @extra = extra || {}
49
65
  end
50
66
 
51
- def eq(field, expected, context = nil)
52
- locale = context[:locale] if context.present?
53
- locale ||= 'en-US'
54
- Query.new(@client,
55
- @relation.merge("fields.#{field}.#{locale}" => expected))
67
+ def apply_operator(operator, field, expected, context = nil)
68
+ op = operator == :eq ? nil : operator
69
+ param = parameter(field, operator: op, context: context, locale: true)
70
+
71
+ self.class.new(
72
+ store: @store,
73
+ client: @client,
74
+ relation: @relation.merge(param => expected),
75
+ options: @options,
76
+ **@extra
77
+ )
78
+ end
79
+
80
+ def nested_conditions(field, conditions, context)
81
+ base_param = parameter(field)
82
+
83
+ conditions.reduce(self) do |query, (ref, value)|
84
+ query.apply({ "#{base_param}.#{parameter(ref)}" => value }, context)
85
+ end
86
+ end
87
+
88
+ Base::Query::OPERATORS.each do |op|
89
+ define_method(op) do |field, expected, context = nil|
90
+ apply_operator(op, field, expected, context)
91
+ end
56
92
  end
57
93
 
58
94
  private
59
95
 
60
- def resolve
61
- return @resolve if @resolve
62
- @resolve ||=
96
+ def response
97
+ @response ||=
63
98
  if @relation[:content_type] == 'Asset'
64
99
  @client.assets(
65
- { locale: '*' }.merge!(@relation.reject { |k| k == :content_type })
100
+ { locale: '*' }.merge!(@relation.reject { |k| k == :content_type }).merge!(@options)
66
101
  )
67
102
  else
68
- @client.entries({ locale: '*' }.merge!(@relation))
103
+ @client.entries({ locale: '*' }.merge!(@relation).merge!(@options))
69
104
  end
70
105
  end
106
+
107
+ def resolve_link(val, depth)
108
+ return val unless val.is_a?(Hash) && val.dig('sys', 'type') == 'Link'
109
+ return val unless included = response.includes[val.dig('sys', 'id')]
110
+
111
+ resolve_includes(included, depth - 1)
112
+ end
113
+
114
+ def parameter(field, operator: nil, context: nil, locale: false)
115
+ if sys?(field)
116
+ "#{field}#{op_param(operator)}"
117
+ elsif id?(field)
118
+ "sys.#{field}#{op_param(operator)}"
119
+ else
120
+ "#{field_reference(field)}#{locale(context) if locale}#{op_param(operator)}"
121
+ end
122
+ end
123
+
124
+ def locale(context)
125
+ ".#{(context || {}).fetch(:locale, 'en-US')}"
126
+ end
127
+
128
+ def op_param(operator)
129
+ operator ? "[#{operator}]" : ''
130
+ end
131
+
132
+ def field_reference(field)
133
+ return field if nested?(field)
134
+
135
+ "fields.#{field}"
136
+ end
137
+
138
+ def nested?(field)
139
+ field.to_s.include?('.')
140
+ end
71
141
  end
72
142
  end
73
143
  end
@@ -2,24 +2,18 @@
2
2
 
3
3
  module WCC::Contentful::Store
4
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
5
  def initialize(client, cache: nil)
13
6
  @cdn = CDNAdapter.new(client)
14
7
  @cache = cache || ActiveSupport::Cache::MemoryStore.new
8
+ @client = client
15
9
  end
16
10
 
17
- def find(key)
11
+ def find(key, **options)
18
12
  found =
19
13
  @cache.fetch(key) do
20
14
  # if it's not a contentful ID don't hit the API.
21
15
  # Store a nil object if we can't find the object on the CDN.
22
- (@cdn.find(key) || nil_obj(key)) if key =~ /^\w+$/
16
+ (@cdn.find(key, options) || nil_obj(key)) if key =~ /^\w+$/
23
17
  end
24
18
 
25
19
  case found.try(:dig, 'sys', 'type')
@@ -30,6 +24,33 @@ module WCC::Contentful::Store
30
24
  end
31
25
  end
32
26
 
27
+ # TODO: https://github.com/watermarkchurch/wcc-contentful/issues/18
28
+ # figure out how to cache the results of a find_by query, ex:
29
+ # `find_by('slug' => '/about')`
30
+ def find_by(content_type:, filter: nil, options: nil)
31
+ if filter.keys == ['sys.id']
32
+ # Direct ID lookup, like what we do in `WCC::Contentful::ModelMethods.resolve`
33
+ # We can return just this item. Stores are not required to implement :include option.
34
+ if found = @cache.read(filter['sys.id'])
35
+ return found
36
+ end
37
+ end
38
+
39
+ q = find_all(content_type: content_type, options: { limit: 1 }.merge!(options || {}))
40
+ q = q.apply(filter) if filter
41
+ q.first
42
+ end
43
+
44
+ def find_all(content_type:, options: nil)
45
+ Query.new(
46
+ store: self,
47
+ client: @client,
48
+ relation: { content_type: content_type },
49
+ cache: @cache,
50
+ options: options
51
+ )
52
+ end
53
+
33
54
  # #index is called whenever the sync API comes back with more data.
34
55
  def index(json)
35
56
  id = json.dig('sys', 'id')
@@ -41,6 +62,7 @@ module WCC::Contentful::Store
41
62
 
42
63
  # we also set deletes in the cache - no need to go hit the API when we know
43
64
  # this is a nil object
65
+ ensure_hash json
44
66
  @cache.write(id, json)
45
67
 
46
68
  case json.dig('sys', 'type')
@@ -52,6 +74,7 @@ module WCC::Contentful::Store
52
74
  end
53
75
 
54
76
  def set(key, value)
77
+ ensure_hash value
55
78
  old = @cache.read(key)
56
79
  @cache.write(key, value)
57
80
  old
@@ -72,5 +95,67 @@ module WCC::Contentful::Store
72
95
  }
73
96
  }
74
97
  end
98
+
99
+ def ensure_hash(val)
100
+ raise ArgumentError, 'Value must be a Hash' unless val.is_a?(Hash)
101
+ end
102
+
103
+ class Query < CDNAdapter::Query
104
+ def initialize(cache:, **extra)
105
+ super(cache: cache, **extra)
106
+ @cache = cache
107
+ end
108
+
109
+ private
110
+
111
+ def response
112
+ # Disabling because the superclass already took `@response`
113
+ # rubocop:disable Naming/MemoizedInstanceVariableName
114
+ @wrapped_response ||= ResponseWrapper.new(super, @cache)
115
+ # rubocop:enable Naming/MemoizedInstanceVariableName
116
+ end
117
+
118
+ ResponseWrapper =
119
+ Struct.new(:response, :cache) do
120
+ delegate :count, to: :response
121
+
122
+ def items
123
+ @items ||=
124
+ response.items.map do |item|
125
+ id = item.dig('sys', 'id')
126
+ prev = cache.read(id)
127
+ unless (prev_rev = prev&.dig('sys', 'revision')) &&
128
+ (next_rev = item.dig('sys', 'revision')) &&
129
+ next_rev < prev_rev
130
+
131
+ cache.write(id, item)
132
+ end
133
+
134
+ item
135
+ end
136
+ end
137
+
138
+ def includes
139
+ @includes ||= IncludesWrapper.new(response, cache)
140
+ end
141
+ end
142
+
143
+ IncludesWrapper =
144
+ Struct.new(:response, :cache) do
145
+ def [](id)
146
+ return unless item = response.includes[id]
147
+
148
+ prev = cache.read(id)
149
+ unless (prev_rev = prev&.dig('sys', 'revision')) &&
150
+ (next_rev = item.dig('sys', 'revision')) &&
151
+ next_rev < prev_rev
152
+
153
+ cache.write(id, item)
154
+ end
155
+
156
+ item
157
+ end
158
+ end
159
+ end
75
160
  end
76
161
  end
@@ -9,6 +9,7 @@ module WCC::Contentful::Store
9
9
 
10
10
  def set(key, value)
11
11
  value = value.deep_dup.freeze
12
+ ensure_hash value
12
13
  mutex.with_write_lock do
13
14
  old = @hash[key]
14
15
  @hash[key] = value
@@ -26,43 +27,47 @@ module WCC::Contentful::Store
26
27
  mutex.with_read_lock { @hash.keys }
27
28
  end
28
29
 
29
- def find(key)
30
+ def find(key, **_options)
30
31
  mutex.with_read_lock do
31
32
  @hash[key]
32
33
  end
33
34
  end
34
35
 
35
- def find_all(content_type:)
36
+ def find_all(content_type:, options: nil)
36
37
  relation = mutex.with_read_lock { @hash.values }
37
38
 
38
39
  relation =
39
40
  relation.reject do |v|
40
- value_content_type = v.dig('sys', 'contentType', 'sys', 'id')
41
+ value_content_type = v.try(:dig, 'sys', 'contentType', 'sys', 'id')
41
42
  value_content_type.nil? || value_content_type != content_type
42
43
  end
43
- Query.new(relation)
44
+ Query.new(self, relation, options)
44
45
  end
45
46
 
46
47
  class Query < Base::Query
47
48
  def result
48
- @relation.dup
49
+ return @relation.dup unless @options[:include]
50
+
51
+ @relation.map { |e| resolve_includes(e, @options[:include]) }
49
52
  end
50
53
 
51
- def initialize(relation)
54
+ def initialize(store, relation, options = nil)
55
+ super(store)
52
56
  @relation = relation
57
+ @options = options || {}
53
58
  end
54
59
 
55
60
  def eq(field, expected, context = nil)
56
61
  locale = context[:locale] if context.present?
57
62
  locale ||= 'en-US'
58
- Query.new(@relation.select do |v|
63
+ Query.new(@store, @relation.select do |v|
59
64
  val = v.dig('fields', field, locale)
60
65
  if val.is_a? Array
61
66
  val.include?(expected)
62
67
  else
63
68
  val == expected
64
69
  end
65
- end)
70
+ end, @options)
66
71
  end
67
72
  end
68
73
  end
@@ -5,7 +5,7 @@ require 'pg'
5
5
 
6
6
  module WCC::Contentful::Store
7
7
  class PostgresStore < Base
8
- def initialize(connection_options = nil)
8
+ def initialize(_config = nil, connection_options = nil)
9
9
  super()
10
10
  connection_options ||= { dbname: 'postgres' }
11
11
  @conn = PG.connect(connection_options)
@@ -13,8 +13,10 @@ module WCC::Contentful::Store
13
13
  end
14
14
 
15
15
  def set(key, value)
16
+ ensure_hash value
16
17
  result = @conn.exec_prepared('upsert_entry', [key, value.to_json])
17
18
  return if result.num_tuples == 0
19
+
18
20
  val = result.getvalue(0, 0)
19
21
  JSON.parse(val) if val
20
22
  end
@@ -29,30 +31,36 @@ module WCC::Contentful::Store
29
31
  def delete(key)
30
32
  result = @conn.exec_prepared('delete_by_id', [key])
31
33
  return if result.num_tuples == 0
34
+
32
35
  JSON.parse(result.getvalue(0, 1))
33
36
  end
34
37
 
35
- def find(key)
38
+ def find(key, **_options)
36
39
  result = @conn.exec_prepared('select_entry', [key])
37
40
  return if result.num_tuples == 0
41
+
38
42
  JSON.parse(result.getvalue(0, 1))
39
43
  end
40
44
 
41
- def find_all(content_type:)
45
+ def find_all(content_type:, options: nil)
42
46
  statement = "WHERE data->'sys'->'contentType'->'sys'->>'id' = $1"
43
47
  Query.new(
48
+ self,
44
49
  @conn,
45
50
  statement,
46
- [content_type]
51
+ [content_type],
52
+ options
47
53
  )
48
54
  end
49
55
 
50
56
  class Query < Base::Query
51
- def initialize(conn, statement = nil, params = nil)
57
+ def initialize(store, conn, statement = nil, params = nil, options = nil)
58
+ super(store)
52
59
  @conn = conn
53
60
  @statement = statement ||
54
61
  "WHERE data->'sys'->>'id' IS NOT NULL"
55
62
  @params = params || []
63
+ @options = options || {}
56
64
  end
57
65
 
58
66
  def eq(field, expected, context = nil)
@@ -65,14 +73,17 @@ module WCC::Contentful::Store
65
73
  "->$#{push_param(locale, params)} ? $#{push_param(expected, params)}"
66
74
 
67
75
  Query.new(
76
+ @store,
68
77
  @conn,
69
78
  statement,
70
- params
79
+ params,
80
+ @options
71
81
  )
72
82
  end
73
83
 
74
84
  def count
75
85
  return @count if @count
86
+
76
87
  statement = 'SELECT count(*) FROM contentful_raw ' + @statement
77
88
  result = @conn.exec(statement, @params)
78
89
  @count = result.getvalue(0, 0).to_i
@@ -80,27 +91,49 @@ module WCC::Contentful::Store
80
91
 
81
92
  def first
82
93
  return @first if @first
94
+
83
95
  statement = 'SELECT * FROM contentful_raw ' + @statement + ' LIMIT 1'
84
96
  result = @conn.exec(statement, @params)
85
- JSON.parse(result.getvalue(0, 1))
97
+ return if result.num_tuples == 0
98
+
99
+ resolve_includes(
100
+ JSON.parse(result.getvalue(0, 1)),
101
+ @options[:include]
102
+ )
86
103
  end
87
104
 
88
105
  def map
89
106
  arr = []
90
- resolve.each { |row| arr << yield(JSON.parse(row['data'])) }
107
+ resolve.each do |row|
108
+ arr << yield(
109
+ resolve_includes(
110
+ JSON.parse(row['data']),
111
+ @options[:include]
112
+ )
113
+ )
114
+ end
91
115
  arr
92
116
  end
93
117
 
94
118
  def result
95
119
  arr = []
96
- resolve.each { |row| arr << JSON.parse(row['data']) }
120
+ resolve.each do |row|
121
+ arr <<
122
+ resolve_includes(
123
+ JSON.parse(row['data']),
124
+ @options[:include]
125
+ )
126
+ end
97
127
  arr
98
128
  end
99
129
 
130
+ # TODO: override resolve_includes to make it more efficient
131
+
100
132
  private
101
133
 
102
134
  def resolve
103
135
  return @resolved if @resolved
136
+
104
137
  statement = 'SELECT * FROM contentful_raw ' + @statement
105
138
  @resolved = @conn.exec(statement, @params)
106
139
  end
@@ -120,7 +153,7 @@ module WCC::Contentful::Store
120
153
  CREATE INDEX IF NOT EXISTS contentful_raw_value_type ON contentful_raw ((data->'sys'->>'type'));
121
154
  CREATE INDEX IF NOT EXISTS contentful_raw_value_content_type ON contentful_raw ((data->'sys'->'contentType'->'sys'->>'id'));
122
155
 
123
- DROP FUNCTION IF EXISTS "upsert_entry";
156
+ DROP FUNCTION IF EXISTS "upsert_entry"(_id varchar, _data jsonb);
124
157
  CREATE FUNCTION "upsert_entry"(_id varchar, _data jsonb) RETURNS jsonb AS $$
125
158
  DECLARE
126
159
  prev jsonb;
@@ -133,7 +166,7 @@ module WCC::Contentful::Store
133
166
  RETURN prev;
134
167
  END;
135
168
  $$ LANGUAGE 'plpgsql';
136
- HEREDOC
169
+ HEREDOC
137
170
  )
138
171
 
139
172
  conn.prepare('upsert_entry', 'SELECT * FROM upsert_entry($1,$2)')