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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 58ee6d756754247484bf90a993340caf891a7471
4
- data.tar.gz: 947b949744aacf47612a91237e70807e91c53664
3
+ metadata.gz: '08ab30632430c1a52ed10b696480397ac53fc682'
4
+ data.tar.gz: be04ea6c9ce005456b08cf87770b70c466dadcff
5
5
  SHA512:
6
- metadata.gz: b89c3971ea984941b7f6dcba32cf1b9942417236a8074224304716379bccc9e8a7e10cfb646cece2d4989b514ee8ada7e55bc65f03668d8686482418d2699dc9
7
- data.tar.gz: 47ebcda216d33570a8a99018131c84411bbc42c3dead50c47632bc3b2e1d516d3af18447b31e2c17dc003e7805423e4bef891545c3302fd6fa1792504cf4d6bd
6
+ metadata.gz: 1f0c325803a5b8c5f0a228401fea6634e9498c21cf50ac929d8881d5772a70732e8e4a7346fcd8e13e0434b0a42d3687480818750fe0008ff4cc2bfef76169d3
7
+ data.tar.gz: 90f7524d0bdbbbc5b40db0f060e6e631dabaeff0ee5fb8142eeadde3117cf6b6add58dec68914454fe9ef28db608b3e29af2ae5f6b616460842b31662677b257
data/.rspec CHANGED
@@ -1,4 +1,3 @@
1
1
  --format documentation
2
2
  --color
3
- --require spec_helper
4
3
  --order rand
data/README.md CHANGED
@@ -1,3 +1,8 @@
1
+ [![Gem Version](https://badge.fury.io/rb/wcc-contentful.svg)](https://badge.fury.io/rb/wcc-contentful)
2
+ [![CircleCI](https://circleci.com/gh/watermarkchurch/wcc-contentful.svg?style=svg)](https://circleci.com/gh/watermarkchurch/wcc-contentful)
3
+
4
+ Full documentation: https://www.rubydoc.info/github/watermarkchurch/wcc-contentful
5
+
1
6
  # WCC::Contentful
2
7
 
3
8
  ## Installation
@@ -21,20 +26,188 @@ Or install it yourself as:
21
26
  ```ruby
22
27
  WCC::Contentful.configure do |config|
23
28
  config.access_token = <CONTENTFUL_ACCESS_TOKEN>
24
- config.preview_token = <CONTENTFUL_PREVIEW_TOKEN>
25
29
  config.space = <CONTENTFUL_SPACE_ID>
26
- config.default_locale = "en-US"
27
30
  end
31
+
32
+ WCC::Contentful.init!
28
33
  ```
29
34
 
30
35
  ## Usage
31
36
 
32
- ```ruby
33
- redirect_object = WCC::Contentful::Model::Redirect.find_by({slug: 'published-redirect'}, preview: false)
34
- redirect_object.href
37
+ ### WCC::Contentful::Model API
38
+
39
+ The WCC::Contentful::Model API exposes Contentful data as a set of dynamically
40
+ generated Ruby objects. These objects are based on the content types in your
41
+ Contentful space. All these objects are generated by WCC::Contentful.init!
42
+
43
+ The following examples show how to use this API to find entries of the `page`
44
+ content type:
35
45
 
36
- preview_redirect_object = WCC::Contentful::Model::Redirect.find_by({slug: 'draft-redirect'}, preview: true)
46
+ ```ruby
47
+ # Find objects by id
48
+ WCC::Contentful::Model::Page.find('1E2ucWSdacxxf233sfa3')
49
+ # => #<WCC::Contentful::Model::Page:0x0000000005c71a78 @created_at=2018-04-16 18:41:17 UTC...>
50
+
51
+ # Find objects by field
52
+ WCC::Contentful::Model::Page.find_by(slug: '/some-slug')
53
+ # => #<WCC::Contentful::Model::Page:0x0000000005c71a78 @created_at=2018-04-16 18:41:17 UTC...>
54
+
55
+ # Use operators to filter by a field
56
+ # must use full notation for sys attributes (except ID)
57
+ WCC::Contentful::Model::Page.find_all('sys.created_at' => { lte: Date.today })
58
+ # => [#<WCC::Contentful::Model::Page:0x0000000005c71a78 @created_at=2018-04-16 18:41:17 UTC...>, ... ]
59
+
60
+ # Nest queries to mimick joins
61
+ WCC::Contentful::Model::Page.find_by(subpages: { slug: '/some-slug' })
62
+ # => #<WCC::Contentful::Model::Page:0x0000000005c71a78 @created_at=2018-04-16 18:41:17 UTC...>
63
+
64
+ # Pass the preview flag to use the preview client (must have set preview_token config param)
65
+ preview_redirect = WCC::Contentful::Model::Redirect.find_by({ slug: 'draft-redirect' }, preview: true)
66
+ # => #<WCC::Contentful::Model::Redirect:0x0000000005d879ad @created_at=2018-04-16 18:41:17 UTC...>
37
67
  preview_redirect_object.href
68
+ # => 'http://www.somesite.com/slug-for-redirect'
69
+ ```
70
+
71
+ See the {WCC::Contentful::Model} documentation for more details.
72
+
73
+ ### Store API
74
+
75
+ The Store layer is used by the Model API to access Contentful data in a raw form.
76
+ The Store layer returns entries as hashes parsed from JSON, conforming to the
77
+ object structure returned from the Contentful CDN.
78
+
79
+ The following examples show how to use the Store API to retrieve raw data from
80
+ the store:
81
+
82
+ ```ruby
83
+ store = WCC::Contentful::Services.instance.store
84
+ # => #<WCC::Contentful::Store::CDNAdapter:0x00007fb92a221498
85
+
86
+ store.find('5FsqsbMECsM62e04U8sY4Y')
87
+ # => {"sys"=>
88
+ # ...
89
+ # "fields"=>
90
+ # ...}
91
+
92
+ store.find_by(content_type: 'page', filter: { slug: '/some-slug' })
93
+ # => {"sys"=>
94
+ # ...
95
+ # "fields"=>
96
+ # ...}
97
+
98
+ query = store.find_all(content_type: 'page').eq('group', 'some-group')
99
+ # => #<WCC::Contentful::Store::CDNAdapter::Query:0x00007fa3d40b84f0
100
+ query.first
101
+ # => {"sys"=>
102
+ # ...
103
+ # "fields"=>
104
+ # ...}
105
+ query.result
106
+ # => #<Enumerator::Lazy: ...>
107
+ query.result.force
108
+ # => [{"sys"=> ...}, {"sys"=> ...}, ...]
109
+ ```
110
+
111
+ See the {WCC::Contentful::Store} documentation for more details.
112
+
113
+ ### Direct CDN API (SimpleClient)
114
+
115
+ The SimpleClient is the bottom layer, and is used to get raw data directly from
116
+ the Contentful CDN. It handles response parsing and paging, but does not resolve
117
+ links or transform the result into a Model class.
118
+
119
+ The following examples show how to use the SimpleClient to retrieve data directly
120
+ from the Contentful CDN:
121
+
122
+ ```ruby
123
+ client = WCC::Contentful::Services.instance.client
124
+ # => #<WCC::Contentful::SimpleClient::Cdn:0x00007fa3cde89310
125
+
126
+ response = client.entry('5FsqsbMECsM62e04U8sY4Y')
127
+ # => #<WCC::Contentful::SimpleClient::Response:0x00007fa3d103a4e0
128
+ response.body
129
+ # => "{\n \"sys\": {\n ...
130
+ response.raw
131
+ # => {"sys"=>
132
+ # ...
133
+ # "fields"=>
134
+ # ...}
135
+
136
+ client.asset('5FsqsbMECsM62e04U8sY4Y').raw
137
+ # => {"sys"=>
138
+ # ...
139
+ # "fields"=>
140
+ # ...}
141
+
142
+ response = client.entries('fields.group' => 'some-group', 'limit' => 5)
143
+ # => #<WCC::Contentful::SimpleClient::Response:0x00007fa3d103a4e0
144
+ response.count
145
+ # => 99
146
+ response.first
147
+ # => {"sys"=>
148
+ # ...
149
+ # "fields"=>
150
+ # ...}
151
+ response.items
152
+ => #<Enumerator::Lazy: ...>
153
+ response.items.count # Careful! This evaluates the lazy iterator and gets all pages
154
+ # => 99
155
+
156
+ response.includes
157
+ # => {"4xNnFJ77egkSMEogE2yISa"=>
158
+ # {"sys"=> ...}
159
+ # "6Fwukxxkxa6qQCC04WCaqg"=>
160
+ # {"sys"=> ...}
161
+ # ...}
162
+ ```
163
+
164
+ The client handles Paging automatically within the lazy iterator returned by #items.
165
+ This lazy iterator does not respect the `limit` param - that param is only passed
166
+ through to the API to set the page size.
167
+
168
+ Entries included via the `include` parameter are made available on the #includes
169
+ field. This is a hash of `<entry ID> => <raw entry>` and makes it easy to grab
170
+ links. This hash is added to lazily as you enumerate the pages.
171
+
172
+ See the {WCC::Contentful::SimpleClient} documentation for more details.
173
+
174
+ ### Accessing the APIs within application code
175
+
176
+ The Model API is best exposed by defining your own model classes in the `app/models`
177
+ directory which inherit from the WCC::Contentful models.
178
+
179
+ ```ruby
180
+ # app/models/page.rb
181
+ class Page < WCC::Contentful::Model::Page
182
+
183
+ # You can add additional methods here
184
+ end
185
+
186
+ # app/controllers/pages_controller.rb
187
+ class PagesController < ApplicationController
188
+ def show
189
+ @page = Page.find_by(slug: params[:slug])
190
+ raise Exceptions::PageNotFoundError, params[:slug] unless @page
191
+ end
192
+ end
193
+ ```
194
+
195
+ The {WCC::Contentful::Services} singleton gives access to the other configured services.
196
+ You can also include the {WCC::Contentful::ServiceAccessors} concern to define these
197
+ services as attributes in a class.
198
+
199
+ ```ruby
200
+ class MyJob < ApplicationJob
201
+ include WCC::Contentful::ServiceAccessors
202
+
203
+ def perform
204
+ Page.find(...)
205
+
206
+ store.find(...)
207
+
208
+ client.entries(...)
209
+ end
210
+ end
38
211
  ```
39
212
 
40
213
  ## Development
@@ -45,7 +218,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
45
218
 
46
219
  ## Contributing
47
220
 
48
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/wcc-contentful. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
221
+ Bug reports and pull requests are welcome on GitHub at https://github.com/watermarkchurch/wcc-contentful. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
49
222
 
50
223
  ## License
51
224
 
@@ -53,4 +226,4 @@ The gem is available as open source under the terms of the [MIT License](http://
53
226
 
54
227
  ## Code of Conduct
55
228
 
56
- Everyone interacting in the WCC::Contentful project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/wcc-contentful/blob/master/CODE_OF_CONDUCT.md).
229
+ Everyone interacting in the WCC::Contentful project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/watermarkchurch/wcc-contentful/blob/master/CODE_OF_CONDUCT.md).
@@ -3,13 +3,45 @@
3
3
  require_dependency 'wcc/contentful/application_controller'
4
4
 
5
5
  module WCC::Contentful
6
+ # The WebhookController is mounted by the WCC::Contentful::Engine to receive
7
+ # webhook events from Contentful. It passes these webhook events to
8
+ # the jobs configured in {WCC::Contentful::Configuration WCC::Contentful::Configuration#webhook_jobs}
6
9
  class WebhookController < ApplicationController
10
+ include WCC::Contentful::ServiceAccessors
11
+
7
12
  before_action :authorize_contentful
13
+ protect_from_forgery unless: -> { request.format.json? }
14
+
15
+ rescue_from ActionController::ParameterMissing do |_e|
16
+ render json: { msg: 'The request must conform to Contentful webhook structure' }, status: 400
17
+ end
8
18
 
9
19
  def receive
10
- WCC::Contentful.sync!(up_to_id: params.dig('sys', 'id'))
20
+ event = params.require('webhook').permit!
21
+ event.require('sys').require(%w[id type])
22
+ event = event.to_h
23
+
24
+ # Immediately update the store, we may update again later using DelayedSyncJob.
25
+ store.index(event) if store.respond_to?(:index)
26
+
27
+ jobs.each do |job|
28
+ begin
29
+ if job.respond_to?(:perform_later)
30
+ job.perform_later(event)
31
+ elsif job.respond_to?(:call)
32
+ job.call(event)
33
+ else
34
+ Rails.logger.error "Misconfigured webhook job: #{job} does not respond to " \
35
+ ':perform_later or :call'
36
+ end
37
+ rescue StandardError => e
38
+ Rails.logger.error "Error in job #{job}: #{e}"
39
+ end
40
+ end
11
41
  end
12
42
 
43
+ private
44
+
13
45
  def authorize_contentful
14
46
  config = WCC::Contentful.configuration
15
47
 
@@ -23,8 +55,16 @@ module WCC::Contentful
23
55
  end
24
56
  end
25
57
 
26
- return if request.content_type == 'application/vnd.contentful.management.v1+json'
58
+ # 'application/vnd.contentful.management.v1+json' is an alias for the 'application/json'
59
+ # content-type, so 'request.content_type' will give 'application/json'
60
+ return if request.headers['Content-Type'] == 'application/vnd.contentful.management.v1+json'
61
+
27
62
  render json: { msg: 'This endpoint only responds to webhooks from Contentful' }, status: 406
28
63
  end
64
+
65
+ def jobs
66
+ jobs = [WCC::Contentful::DelayedSyncJob]
67
+ jobs.push(*WCC::Contentful.configuration.webhook_jobs)
68
+ end
29
69
  end
30
70
  end
@@ -3,12 +3,61 @@
3
3
  require 'active_job'
4
4
 
5
5
  module WCC::Contentful
6
+ # This job uses the Contentful Sync API to update the configured store with
7
+ # the latest data from Contentful.
6
8
  class DelayedSyncJob < ActiveJob::Base
9
+ include WCC::Contentful::ServiceAccessors
10
+
11
+ self.queue_adapter = :async
7
12
  queue_as :default
8
13
 
9
- def perform(*args)
10
- sync_options = args.first || {}
11
- WCC::Contentful.sync!(**sync_options)
14
+ def self.mutex
15
+ @mutex ||= Mutex.new
16
+ end
17
+
18
+ def perform(event = nil)
19
+ up_to_id = nil
20
+ up_to_id = event[:up_to_id] || event.dig('sys', 'id') if event
21
+ sync!(up_to_id: up_to_id)
22
+ end
23
+
24
+ # Calls the Contentful Sync API and updates the configured store with the returned
25
+ # data.
26
+ #
27
+ # @param [String] up_to_id
28
+ # An ID that we know has changed and should come back from the sync.
29
+ # If we don't find this ID in the sync data, then drop a job to try
30
+ # the sync again after a few minutes.
31
+ #
32
+ def sync!(up_to_id: nil)
33
+ return unless store.respond_to?(:index)
34
+
35
+ self.class.mutex.synchronize do
36
+ next_sync_token = store.find('sync:token')&.fetch('token')
37
+ sync_resp = client.sync(sync_token: next_sync_token)
38
+
39
+ id_found = up_to_id.nil?
40
+
41
+ count = 0
42
+ sync_resp.items.each do |item|
43
+ id = item.dig('sys', 'id')
44
+ id_found ||= id == up_to_id
45
+ store.index(item)
46
+ count += 1
47
+ end
48
+ store.set('sync:token', token: sync_resp.next_sync_token)
49
+
50
+ logger.info "Synced #{count} entries. Next sync token:\n #{sync_resp.next_sync_token}"
51
+ sync_later!(up_to_id: up_to_id) unless id_found
52
+ sync_resp.next_sync_token
53
+ end
54
+ end
55
+
56
+ # Drops an ActiveJob job to invoke WCC::Contentful.sync! after a given amount
57
+ # of time.
58
+ def sync_later!(up_to_id: nil, wait: 10.minutes)
59
+ WCC::Contentful::DelayedSyncJob.set(wait: wait)
60
+ .perform_later(up_to_id: up_to_id)
12
61
  end
13
62
  end
14
63
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_job'
4
+
5
+ module WCC::Contentful
6
+ class WebhookEnableJob < ActiveJob::Base
7
+ self.queue_adapter = :async
8
+ queue_as :default
9
+
10
+ def perform(args)
11
+ client = WCC::Contentful::SimpleClient::Management.new(
12
+ args
13
+ )
14
+ enable_webhook(client, args.slice(:app_url, :webhook_username, :webhook_password))
15
+ end
16
+
17
+ def enable_webhook(client, app_url:, webhook_username: nil, webhook_password: nil)
18
+ expected_url = URI.join(app_url, 'webhook/receive').to_s
19
+ webhook = client.webhook_definitions.items.find { |w| w['url'] == expected_url }
20
+ logger.debug "existing webhook: #{webhook.inspect}" if webhook
21
+ return if webhook
22
+
23
+ body = {
24
+ 'name' => 'WCC::Contentful webhook',
25
+ 'url' => expected_url,
26
+ 'topics' => [
27
+ '*.publish',
28
+ '*.unpublish'
29
+ ]
30
+ }
31
+ body['httpBasicUsername'] = webhook_username if webhook_username.present?
32
+ body['httpBasicPassword'] = webhook_password if webhook_password.present?
33
+
34
+ begin
35
+ resp = client.post_webhook_definition(body)
36
+ logger.info "Created webhook: #{resp.raw.dig('sys', 'id')}"
37
+ rescue WCC::Contentful::SimpleClient::ApiError => e
38
+ logger.error "#{e.response.code}: #{e.response.raw}" if e.response
39
+ raise
40
+ end
41
+ end
42
+ end
43
+ end
data/bin/console CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
- require "bundler/setup"
4
- require "wcc/contentful"
4
+ require 'bundler/setup'
5
+ require 'wcc/contentful'
5
6
 
6
7
  # You can add fixtures and/or initialization code here to make experimenting
7
8
  # with your gem easier. You can also use a different console, if you like.
@@ -10,5 +11,5 @@ require "wcc/contentful"
10
11
  # require "pry"
11
12
  # Pry.start
12
13
 
13
- require "irb"
14
+ require 'irb'
14
15
  IRB.start(__FILE__)
data/bin/rails CHANGED
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
2
4
  # This command will automatically be run when you run "rails" with Rails gems
3
5
  # installed from the root of your application.
4
6
 
@@ -1,3 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Mime::Type.register 'application/vnd.contentful.management.v1+json', :json
3
+ # https://www.contentful.com/developers/docs/references/content-management-api/
4
+ Mime::Type.register 'application/vnd.contentful.management.v1+json', :json_mgmt
5
+ # https://www.contentful.com/developers/docs/references/content-delivery-api/
6
+ Mime::Type.register 'application/vnd.contentful.delivery.v1+json', :json_cda
7
+
8
+ Mime::Type.register 'application/json', :json, [
9
+ 'application/vnd.contentful.management.v1+json',
10
+ 'application/vnd.contentful.delivery.v1+json',
11
+ 'application/json'
12
+ ]