wcc-contentful 0.4.0.pre.alpha → 1.0.0.pre.rc3

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 (69) hide show
  1. checksums.yaml +5 -5
  2. data/Guardfile +43 -0
  3. data/README.md +246 -11
  4. data/Rakefile +5 -0
  5. data/app/controllers/wcc/contentful/webhook_controller.rb +25 -24
  6. data/app/jobs/wcc/contentful/webhook_enable_job.rb +36 -2
  7. data/config/routes.rb +1 -1
  8. data/doc +1 -0
  9. data/lib/tasks/download_schema.rake +12 -0
  10. data/lib/wcc/contentful.rb +70 -16
  11. data/lib/wcc/contentful/active_record_shim.rb +72 -0
  12. data/lib/wcc/contentful/configuration.rb +177 -46
  13. data/lib/wcc/contentful/content_type_indexer.rb +14 -0
  14. data/lib/wcc/contentful/downloads_schema.rb +112 -0
  15. data/lib/wcc/contentful/engine.rb +33 -14
  16. data/lib/wcc/contentful/event.rb +171 -0
  17. data/lib/wcc/contentful/events.rb +41 -0
  18. data/lib/wcc/contentful/exceptions.rb +3 -0
  19. data/lib/wcc/contentful/indexed_representation.rb +2 -2
  20. data/lib/wcc/contentful/instrumentation.rb +31 -0
  21. data/lib/wcc/contentful/link.rb +28 -0
  22. data/lib/wcc/contentful/link_visitor.rb +122 -0
  23. data/lib/wcc/contentful/middleware.rb +7 -0
  24. data/lib/wcc/contentful/middleware/store.rb +158 -0
  25. data/lib/wcc/contentful/middleware/store/caching_middleware.rb +114 -0
  26. data/lib/wcc/contentful/model.rb +37 -3
  27. data/lib/wcc/contentful/model_builder.rb +1 -0
  28. data/lib/wcc/contentful/model_methods.rb +40 -15
  29. data/lib/wcc/contentful/model_singleton_methods.rb +47 -30
  30. data/lib/wcc/contentful/rake.rb +4 -0
  31. data/lib/wcc/contentful/rspec.rb +46 -0
  32. data/lib/wcc/contentful/services.rb +61 -27
  33. data/lib/wcc/contentful/simple_client.rb +81 -25
  34. data/lib/wcc/contentful/simple_client/management.rb +43 -10
  35. data/lib/wcc/contentful/simple_client/response.rb +61 -22
  36. data/lib/wcc/contentful/simple_client/typhoeus_adapter.rb +17 -17
  37. data/lib/wcc/contentful/store.rb +7 -66
  38. data/lib/wcc/contentful/store/README.md +85 -0
  39. data/lib/wcc/contentful/store/base.rb +34 -119
  40. data/lib/wcc/contentful/store/cdn_adapter.rb +71 -12
  41. data/lib/wcc/contentful/store/factory.rb +186 -0
  42. data/lib/wcc/contentful/store/instrumentation.rb +55 -0
  43. data/lib/wcc/contentful/store/interface.rb +82 -0
  44. data/lib/wcc/contentful/store/memory_store.rb +27 -24
  45. data/lib/wcc/contentful/store/postgres_store.rb +268 -101
  46. data/lib/wcc/contentful/store/postgres_store/schema_1.sql +73 -0
  47. data/lib/wcc/contentful/store/postgres_store/schema_2.sql +21 -0
  48. data/lib/wcc/contentful/store/query.rb +246 -0
  49. data/lib/wcc/contentful/store/query/interface.rb +63 -0
  50. data/lib/wcc/contentful/store/rspec_examples.rb +48 -0
  51. data/lib/wcc/contentful/store/rspec_examples/basic_store.rb +629 -0
  52. data/lib/wcc/contentful/store/rspec_examples/include_param.rb +283 -0
  53. data/lib/wcc/contentful/store/rspec_examples/nested_queries.rb +342 -0
  54. data/lib/wcc/contentful/sync_engine.rb +181 -0
  55. data/lib/wcc/contentful/test.rb +7 -0
  56. data/lib/wcc/contentful/test/attributes.rb +56 -0
  57. data/lib/wcc/contentful/test/double.rb +76 -0
  58. data/lib/wcc/contentful/test/factory.rb +101 -0
  59. data/lib/wcc/contentful/version.rb +1 -1
  60. data/wcc-contentful.gemspec +23 -11
  61. metadata +299 -116
  62. data/Gemfile +0 -6
  63. data/app/jobs/wcc/contentful/delayed_sync_job.rb +0 -63
  64. data/lib/wcc/contentful/client_ext.rb +0 -28
  65. data/lib/wcc/contentful/graphql.rb +0 -14
  66. data/lib/wcc/contentful/graphql/builder.rb +0 -177
  67. data/lib/wcc/contentful/graphql/types.rb +0 -54
  68. data/lib/wcc/contentful/simple_client/http_adapter.rb +0 -24
  69. data/lib/wcc/contentful/store/lazy_cache_store.rb +0 -161
@@ -4,10 +4,10 @@
4
4
  class WCC::Contentful::SimpleClient::Management < WCC::Contentful::SimpleClient
5
5
  def initialize(space:, management_token:, **options)
6
6
  super(
7
- api_url: options[:api_url] || 'https://api.contentful.com',
7
+ **options,
8
+ api_url: options[:management_api_url] || 'https://api.contentful.com',
8
9
  space: space,
9
10
  access_token: management_token,
10
- **options
11
11
  )
12
12
 
13
13
  @post_adapter = @adapter if @adapter.respond_to?(:post)
@@ -19,12 +19,34 @@ class WCC::Contentful::SimpleClient::Management < WCC::Contentful::SimpleClient
19
19
  end
20
20
 
21
21
  def content_types(**query)
22
- resp = get('content_types', query)
22
+ resp =
23
+ _instrument 'content_types', query: query do
24
+ get('content_types', query)
25
+ end
26
+ resp.assert_ok!
27
+ end
28
+
29
+ def content_type(key, query = {})
30
+ resp =
31
+ _instrument 'content_types', content_type: key, query: query do
32
+ get("content_types/#{key}", query)
33
+ end
34
+ resp.assert_ok!
35
+ end
36
+
37
+ def editor_interface(content_type_id, query = {})
38
+ resp =
39
+ _instrument 'editor_interfaces', content_type: content_type_id, query: query do
40
+ get("content_types/#{content_type_id}/editor_interface", query)
41
+ end
23
42
  resp.assert_ok!
24
43
  end
25
44
 
26
45
  def webhook_definitions(**query)
27
- resp = get("/spaces/#{space}/webhook_definitions", query)
46
+ resp =
47
+ _instrument 'webhook_definitions', query: query do
48
+ get("/spaces/#{space}/webhook_definitions", query)
49
+ end
28
50
  resp.assert_ok!
29
51
  end
30
52
 
@@ -51,30 +73,41 @@ class WCC::Contentful::SimpleClient::Management < WCC::Contentful::SimpleClient
51
73
  # ]
52
74
  # }
53
75
  def post_webhook_definition(webhook)
54
- resp = post("/spaces/#{space}/webhook_definitions", webhook)
76
+ resp =
77
+ _instrument 'post.webhook_definitions' do
78
+ post("/spaces/#{space}/webhook_definitions", webhook)
79
+ end
55
80
  resp.assert_ok!
56
81
  end
57
82
 
58
83
  def post(path, body)
59
84
  url = URI.join(@api_url, path)
60
85
 
86
+ resp =
87
+ _instrument 'post_http', url: url do
88
+ post_http(url, body)
89
+ end
90
+
61
91
  Response.new(self,
62
92
  { url: url, body: body },
63
- post_http(url, body))
93
+ resp)
64
94
  end
65
95
 
66
96
  private
67
97
 
68
- def post_http(url, body, headers = {}, proxy = {})
98
+ def post_http(url, body, headers = {})
69
99
  headers = {
70
100
  Authorization: "Bearer #{@access_token}",
71
101
  'Content-Type' => 'application/vnd.contentful.management.v1+json'
72
102
  }.merge(headers || {})
73
103
 
74
- resp = @post_adapter.post(url, body, headers, proxy)
104
+ body = body.to_json unless body.is_a? String
105
+ resp = @post_adapter.post(url, body, headers)
75
106
 
76
- if [301, 302, 307].include?(resp.code) && !@options[:no_follow_redirects]
77
- resp = get_http(resp.headers['location'], nil, headers, proxy)
107
+ if [301, 302, 307].include?(resp.status) && !@options[:no_follow_redirects]
108
+ resp = get_http(resp.headers['location'], nil, headers)
109
+ elsif resp.status == 308 && !@options[:no_follow_redirects]
110
+ resp = post_http(resp.headers['location'], body, headers)
78
111
  end
79
112
  resp
80
113
  end
@@ -1,12 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../instrumentation'
4
+
3
5
  class WCC::Contentful::SimpleClient
4
6
  class Response
7
+ include ::WCC::Contentful::Instrumentation
8
+
5
9
  attr_reader :raw_response
6
10
  attr_reader :client
7
11
  attr_reader :request
8
12
 
9
- delegate :code, to: :raw_response
13
+ delegate :status, to: :raw_response
14
+ alias_method :code, :status
10
15
  delegate :headers, to: :raw_response
11
16
 
12
17
  def body
@@ -19,25 +24,41 @@ class WCC::Contentful::SimpleClient
19
24
  alias_method :to_json, :raw
20
25
 
21
26
  def error_message
22
- raw.dig('message') || "#{code}: #{raw_response.message}"
27
+ parsed_message =
28
+ begin
29
+ raw.dig('message')
30
+ rescue JSON::ParserError
31
+ nil
32
+ end
33
+ parsed_message || "#{code}: #{raw_response.body}"
34
+ end
35
+
36
+ def skip
37
+ raw['skip']
38
+ end
39
+
40
+ def total
41
+ raw['total']
23
42
  end
24
43
 
25
44
  def next_page?
26
45
  return unless raw.key? 'items'
27
46
 
28
- raw['items'].length + raw['skip'] < raw['total']
47
+ page_items.length + skip < total
29
48
  end
30
49
 
31
50
  def next_page
32
51
  return unless next_page?
33
-
34
- @next_page ||= @client.get(
35
- @request[:url],
36
- (@request[:query] || {}).merge({
37
- skip: raw['items'].length + raw['skip']
38
- })
39
- )
40
- @next_page.assert_ok!
52
+ return @next_page if @next_page
53
+
54
+ query = (@request[:query] || {}).merge({
55
+ skip: page_items.length + skip
56
+ })
57
+ np =
58
+ _instrument 'page', url: @request[:url], query: query do
59
+ @client.get(@request[:url], query)
60
+ end
61
+ @next_page = np.assert_ok!
41
62
  end
42
63
 
43
64
  def initialize(client, request, raw_response)
@@ -48,13 +69,13 @@ class WCC::Contentful::SimpleClient
48
69
  end
49
70
 
50
71
  def assert_ok!
51
- return self if code >= 200 && code < 300
72
+ return self if status >= 200 && status < 300
52
73
 
53
- raise ApiError[code], self
74
+ raise ApiError[status], self
54
75
  end
55
76
 
56
77
  def each_page(&block)
57
- raise ArgumentError, 'Not a collection response' unless raw['items']
78
+ raise ArgumentError, 'Not a collection response' unless page_items
58
79
 
59
80
  ret =
60
81
  Enumerator.new do |y|
@@ -75,19 +96,21 @@ class WCC::Contentful::SimpleClient
75
96
  end
76
97
 
77
98
  def items
78
- each_page.flat_map do |page|
79
- page.raw['items']
80
- end
99
+ each_page.flat_map(&:page_items)
100
+ end
101
+
102
+ def page_items
103
+ raw['items']
81
104
  end
82
105
 
83
106
  def count
84
- raw['total']
107
+ total
85
108
  end
86
109
 
87
110
  def first
88
- raise ArgumentError, 'Not a collection response' unless raw['items']
111
+ raise ArgumentError, 'Not a collection response' unless page_items
89
112
 
90
- raw['items'].first
113
+ page_items.first
91
114
  end
92
115
 
93
116
  def includes
@@ -115,7 +138,13 @@ class WCC::Contentful::SimpleClient
115
138
  def next_page
116
139
  return unless next_page?
117
140
 
118
- @next_page ||= SyncResponse.new(@client.get(raw['nextPageUrl']))
141
+ url = raw['nextPageUrl']
142
+ next_page =
143
+ _instrument 'page', url: url do
144
+ @client.get(url)
145
+ end
146
+
147
+ @next_page ||= SyncResponse.new(next_page)
119
148
  @next_page.assert_ok!
120
149
  end
121
150
 
@@ -129,7 +158,7 @@ class WCC::Contentful::SimpleClient
129
158
  end
130
159
 
131
160
  def each_page
132
- raise ArgumentError, 'Not a collection response' unless raw['items']
161
+ raise ArgumentError, 'Not a collection response' unless page_items
133
162
 
134
163
  ret =
135
164
  Enumerator.new do |y|
@@ -168,6 +197,10 @@ class WCC::Contentful::SimpleClient
168
197
  case code
169
198
  when 404
170
199
  NotFoundError
200
+ when 401
201
+ UnauthorizedError
202
+ when 429
203
+ RateLimitError
171
204
  else
172
205
  ApiError
173
206
  end
@@ -181,4 +214,10 @@ class WCC::Contentful::SimpleClient
181
214
 
182
215
  class NotFoundError < ApiError
183
216
  end
217
+
218
+ class UnauthorizedError < ApiError
219
+ end
220
+
221
+ class RateLimitError < ApiError
222
+ end
184
223
  end
@@ -3,15 +3,15 @@
3
3
  gem 'typhoeus'
4
4
  require 'typhoeus'
5
5
 
6
- class TyphoeusAdapter
7
- def call(url, query, headers = {}, proxy = {})
8
- raise NotImplementedError, 'Proxying Not Yet Implemented' if proxy[:host]
9
-
10
- TyphoeusAdapter::Response.new(
6
+ class WCC::Contentful::SimpleClient::TyphoeusAdapter
7
+ def get(url, params = {}, headers = {})
8
+ req = OpenStruct.new(params: params, headers: headers)
9
+ yield req if block_given?
10
+ Response.new(
11
11
  Typhoeus.get(
12
12
  url,
13
- params: query,
14
- headers: headers
13
+ params: req.params,
14
+ headers: req.headers
15
15
  )
16
16
  )
17
17
  end
@@ -19,7 +19,7 @@ class TyphoeusAdapter
19
19
  def post(url, body, headers = {}, proxy = {})
20
20
  raise NotImplementedError, 'Proxying Not Yet Implemented' if proxy[:host]
21
21
 
22
- TyphoeusAdapter::Response.new(
22
+ Response.new(
23
23
  Typhoeus.post(
24
24
  url,
25
25
  body: body.to_json,
@@ -28,15 +28,15 @@ class TyphoeusAdapter
28
28
  )
29
29
  end
30
30
 
31
- Response =
32
- Struct.new(:raw) do
33
- delegate :body, to: :raw
34
- delegate :to_s, to: :body
35
- delegate :code, to: :raw
36
- delegate :headers, to: :raw
31
+ class Response < SimpleDelegator
32
+ delegate :to_s, to: :body
37
33
 
38
- def status
39
- raw.code
40
- end
34
+ def raw
35
+ __getobj__
41
36
  end
37
+
38
+ def status
39
+ code
40
+ end
41
+ end
42
42
  end
@@ -1,9 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'store/base'
4
- require_relative 'store/memory_store'
5
- require_relative 'store/cdn_adapter'
6
- require_relative 'store/lazy_cache_store'
3
+ require_relative 'store/factory'
7
4
 
8
5
  # The "Store" is the middle layer in the WCC::Contentful gem. It exposes an API
9
6
  # that implements the configured content delivery strategy.
@@ -18,84 +15,28 @@ require_relative 'store/lazy_cache_store'
18
15
  #
19
16
  # lazy_sync:: Uses the Contentful CDN in combination with an ActiveSupport::Cache
20
17
  # implementation in order to respond with the cached data where possible,
21
- # saving your CDN quota. The cache is kept up-to-date via the Sync API
22
- # and the WCC::Contentful::DelayedSyncJob. It is correct, but not complete.
18
+ # saving your CDN quota. The cache is kept up-to-date via the Sync Engine
19
+ # and the WCC::Contentful::SyncEngine::Job. It is correct, but not complete.
23
20
  #
24
21
  # eager_sync:: Uses one of the full store implementations to store the entirety
25
22
  # of the Contentful space locally. All queries are run against this
26
- # local copy, which is kept up to date via the Sync API and the
27
- # WCC::Contentful::DelayedSyncJob. The local store is correct and complete.
23
+ # local copy, which is kept up to date via the Sync Engine and the
24
+ # WCC::Contentful::SyncEngine::Job. The local store is correct and complete.
28
25
  #
29
26
  # The currently configured store is available on WCC::Contentful::Services.instance.store
30
27
  module WCC::Contentful::Store
31
28
  SYNC_STORES = {
32
- memory: ->(_config) { WCC::Contentful::Store::MemoryStore.new },
29
+ memory: ->(_config, *_options) { WCC::Contentful::Store::MemoryStore.new },
33
30
  postgres: ->(config, *options) {
34
31
  require_relative 'store/postgres_store'
35
32
  WCC::Contentful::Store::PostgresStore.new(config, *options)
36
33
  }
37
34
  }.freeze
38
35
 
39
- CDN_METHODS = %i[
36
+ PRESETS = %i[
40
37
  eager_sync
41
38
  lazy_sync
42
39
  direct
43
40
  custom
44
41
  ].freeze
45
-
46
- Factory =
47
- Struct.new(:config, :services, :cdn_method, :content_delivery_params) do
48
- def build_sync_store
49
- unless respond_to?("build_#{cdn_method}")
50
- raise ArgumentError, "Don't know how to build content delivery method #{cdn_method}"
51
- end
52
-
53
- public_send("build_#{cdn_method}", config, *content_delivery_params)
54
- end
55
-
56
- def validate!
57
- unless CDN_METHODS.include?(cdn_method)
58
- raise ArgumentError, "Please use one of #{CDN_METHODS} instead of #{cdn_method}"
59
- end
60
-
61
- return unless respond_to?("validate_#{cdn_method}")
62
-
63
- public_send("validate_#{cdn_method}", config, *content_delivery_params)
64
- end
65
-
66
- def build_eager_sync(config, store = nil, *options)
67
- store = SYNC_STORES[store].call(config, *options) if store.is_a?(Symbol)
68
- store || MemoryStore.new
69
- end
70
-
71
- def build_lazy_sync(_config, *options)
72
- WCC::Contentful::Store::LazyCacheStore.new(
73
- services.client,
74
- cache: ActiveSupport::Cache.lookup_store(*options)
75
- )
76
- end
77
-
78
- def build_direct(_config, *options)
79
- if options.find { |array| array[:preview] == true }
80
- CDNAdapter.new(services.preview_client)
81
- else
82
- CDNAdapter.new(services.client)
83
- end
84
- end
85
-
86
- def build_custom(config, *options)
87
- store = config.store
88
- return store unless store&.respond_to?(:new)
89
-
90
- store.new(config, options)
91
- end
92
-
93
- def validate_eager_sync(_config, store = nil, *_options)
94
- return unless store.is_a?(Symbol)
95
-
96
- return if SYNC_STORES.key?(store)
97
-
98
- raise ArgumentError, "Please use one of #{SYNC_STORES.keys}"
99
- end
100
- end
101
42
  end
@@ -0,0 +1,85 @@
1
+ # Store API
2
+
3
+ The Store layer is used by the Model API to access Contentful data in a raw form.
4
+ The Store layer returns entries as hashes parsed from JSON, conforming to the
5
+ object structure returned from the Contentful CDN.
6
+
7
+ ## Public Interface
8
+
9
+ See WCC::Contentful::Store::Interface in wcc/contentful/store/interface.rb
10
+
11
+ This is the interface consumed by higher layers, such as the Model and GraphQL
12
+ APIs. It implements the following methods:
13
+
14
+ * `index?` - Returns boolean true if the SyncEngine should call the store's `index(json)`
15
+ method with the results of a Sync.
16
+ * `index(json)` - Updates the store with the latest data. The JSON can be an Entry,
17
+ Asset, DeletedEntry, or DeletedAsset.
18
+ * `find(id)` - Finds an entry or asset by it's ID.
19
+ * `find_all(content_type:, options: nil)` - Returns a query object that can be
20
+ enumerated to lazily iterate over all values of a content type. Query conditions
21
+ can be applied on the query object before it is enumerated to restrict the result
22
+ set.
23
+ * `find_by(content_type:, filter: nil, options: nil)` - Returns the first entry
24
+ of the given content type which matches the filter.
25
+ Note: assets have the special content
26
+ type of `Asset` (capital A)
27
+
28
+ ## Implementing your own store
29
+
30
+ The most straightforward way to implement a store is to include the
31
+ WCC::Contentful::Store::Interface module and then override all the defined
32
+ methods. The easiest way however, is to inherit from WCC::Contentful::Store::Base
33
+ and override the `set`, `delete`, `find`, and `execute` methods.
34
+
35
+ Let's take a look at the MemoryStore for a simplistic example. The MemoryStore
36
+ stores entries in a simple Ruby hash keyed by entry ID. `set` is simply
37
+ assigning to the key in the hash and returning the old value. `delete` and `find`
38
+ are likewise simple. The only complex method is `execute`, because it powers the
39
+ `find_by` and `find_all` query methods.
40
+
41
+ The query passed in to `execute` is an instance of WCC::Contentful::Store::Query.
42
+ This object contains a set of WCC::Contentful::Store::Query::Condition structs.
43
+ Each struct is a tuple of `path`, `op`, and `expected`. `op` is one of
44
+ WCC::Contentful::Store::Query::Interface::OPERATORS, `path` is an array of fields
45
+ pointing to a value in the JSON representation of an entry, and `expected` is the
46
+ expected value that should be compared to the value selected by `path`.
47
+
48
+ Since the MemoryStore only implements the equality operator, it simply digs
49
+ out the value at the given path using `val = entry.dig(*condition.path)`
50
+ and compares it using Contentful's definition of equality:
51
+ ```rb
52
+ # For arrays, equality is defined as does the array include the expected value.
53
+ # See https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/array-equality-inequality
54
+ if val.is_a? Array
55
+ val.include?(condition.expected)
56
+ else
57
+ val == condition.expected
58
+ end
59
+ ```
60
+
61
+ ### RSpec shared examples
62
+
63
+ To ensure you have implemented all the appropriate behavior, there
64
+ are a set of rspec shared examples in wcc/contentful/store/rspec_examples.rb which
65
+ you can include in your specs for your store implementation. Let's look at
66
+ spec/wcc/contentful/store/memory_store_spec.rb to see how it's used:
67
+
68
+ ```rb
69
+ require 'wcc/contentful/store/rspec_examples'
70
+
71
+ RSpec.describe WCC::Contentful::Store::MemoryStore do
72
+ subject { WCC::Contentful::Store::MemoryStore.new }
73
+
74
+ it_behaves_like 'contentful store', {
75
+ # memory store does not support JOINs like `Player.find_by(team: { slug: 'dallas-cowboys' })
76
+ nested_queries: false,
77
+ # Memory store supports resolving includes, but it does so in the most naiive
78
+ # way possible (by recursing down the entry's links and calling #find on every one)
79
+ include_param: 0
80
+ }
81
+ ```
82
+
83
+ The hash passed to the shared examples describes the features that the store
84
+ supports. Any key not provided causes the specs to be given the 'pending'
85
+ attribute. You can disable a set of specs by providing `false` for that key.