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
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
+ ]