wcc-contentful 0.2.2 → 0.3.0.pre.rc

Sign up to get free protection for your applications and to get access to all the features.
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)')