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
@@ -8,82 +8,39 @@ require 'active_support/core_ext/object'
8
8
  require 'wcc/contentful/configuration'
9
9
  require 'wcc/contentful/exceptions'
10
10
  require 'wcc/contentful/helpers'
11
+ require 'wcc/contentful/services'
11
12
  require 'wcc/contentful/simple_client'
12
13
  require 'wcc/contentful/store'
13
14
  require 'wcc/contentful/content_type_indexer'
14
- require 'wcc/contentful/model_validators'
15
15
  require 'wcc/contentful/model'
16
+ require 'wcc/contentful/model_methods'
17
+ require 'wcc/contentful/model_singleton_methods'
16
18
  require 'wcc/contentful/model_builder'
17
19
 
18
- ##
19
20
  # The root namespace of the wcc-contentful gem
20
21
  #
21
22
  # Initialize the gem with the `configure` and `init` methods inside your
22
23
  # initializer.
23
24
  module WCC::Contentful
24
25
  class << self
25
- ##
26
26
  # Gets the current configuration, after calling WCC::Contentful.configure
27
27
  attr_reader :configuration
28
28
 
29
- ##
30
- # Gets the sync token that was returned by the Contentful CDN after the most
31
- # recent invocation of WCC::Contentful.sync!
32
- attr_reader :next_sync_token
29
+ attr_reader :types
33
30
  end
34
31
 
35
- ##
36
- # Gets a {CDN Client}[rdoc-ref:WCC::Contentful::SimpleClient::Cdn] which provides
37
- # methods for getting and paging raw JSON data from the Contentful CDN.
38
- def self.client(preview: false)
39
- if preview
40
- configuration&.preview_client
41
- else
42
- configuration&.client
43
- end
44
- end
45
-
46
- ##
47
- # Gets the data-store which executes the queries run against the dynamic
48
- # models in the WCC::Contentful::Model namespace.
49
- # This is one of the following based on the configured content_delivery method:
50
- #
51
- # [:direct] an instance of WCC::Contentful::Store::CDNAdapter with a
52
- # {CDN Client}[rdoc-ref:WCC::Contentful::SimpleClient::Cdn] to access the CDN.
53
- #
54
- # [:lazy_sync] an instance of WCC::Contentful::Store::LazyCacheStore
55
- # with the configured ActiveSupport::Cache implementation and a
56
- # {CDN Client}[rdoc-ref:WCC::Contentful::SimpleClient::Cdn] for when data
57
- # cannot be found in the cache.
58
- #
59
- # [:eager_sync] an instance of the configured Store type, defined by
60
- # WCC::Contentful::Configuration.sync_store
61
- #
62
- def self.store
63
- WCC::Contentful::Model.store
64
- end
65
-
66
- def self.preview_store
67
- WCC::Contentful::Model.preview_store
68
- end
69
-
70
- ##
71
32
  # Configures the WCC::Contentful gem to talk to a Contentful space.
72
33
  # This must be called first in your initializer, before #init! or accessing the
73
34
  # client.
74
35
  def self.configure
75
36
  @configuration ||= Configuration.new
76
- @next_sync_token = nil
77
37
  yield(configuration)
78
38
 
79
39
  configuration.validate!
80
40
 
81
- configuration.configure_contentful
82
-
83
41
  configuration
84
42
  end
85
43
 
86
- ##
87
44
  # Initializes the WCC::Contentful model-space and backing store.
88
45
  # This populates the WCC::Contentful::Model namespace with Ruby classes
89
46
  # that represent content types in the configured Contentful space.
@@ -91,24 +48,17 @@ module WCC::Contentful
91
48
  # These content types can be queried directly:
92
49
  # WCC::Contentful::Model::Page.find('1xab...')
93
50
  # Or you can inherit from them in your own app:
94
- # class Page < WCC::Contentful::Model.page; end
51
+ # class Page < WCC::Contentful::Model::Page; end
95
52
  # Page.find_by(slug: 'about-us')
96
53
  def self.init!
97
54
  raise ArgumentError, 'Please first call WCC:Contentful.configure' if configuration.nil?
98
- @mutex ||= Mutex.new
99
-
100
- use_preview_client = false
101
- # we want as much as possible the raw JSON from the API
102
- content_types_resp =
103
- if configuration.management_client
104
- configuration.management_client.content_types(limit: 1000)
105
- else
106
- configuration.client.content_types(limit: 1000)
107
- end
108
55
 
109
- (use_preview_client = true) unless configuration.preview_client.nil?
56
+ # we want as much as possible the raw JSON from the API so use the management
57
+ # client if possible
58
+ client = Services.instance.management_client ||
59
+ Services.instance.client
110
60
 
111
- @content_types = content_types_resp.items
61
+ @content_types = client.content_types(limit: 1000).items
112
62
 
113
63
  indexer =
114
64
  ContentTypeIndexer.new.tap do |ixr|
@@ -116,92 +66,14 @@ module WCC::Contentful
116
66
  end
117
67
  @types = indexer.types
118
68
 
119
- if use_preview_client
120
- store = configuration.store(preview: false)
121
- WCC::Contentful::Model.store = store
122
- preview_store = configuration.store(preview: use_preview_client)
123
- WCC::Contentful::Model.preview_store = preview_store
124
- else
125
- store = configuration.store(preview: use_preview_client)
126
- WCC::Contentful::Model.store = store
127
- end
128
-
69
+ store = Services.instance.store
129
70
  if store.respond_to?(:index)
130
- @next_sync_token = store.find("sync:#{configuration.space}:token")
131
- sync!
71
+ # Drop an initial sync
72
+ WCC::Contentful::DelayedSyncJob.perform_later
132
73
  end
133
74
 
134
75
  WCC::Contentful::ModelBuilder.new(@types).build_models
135
76
 
136
- # Extend all model types w/ validation & extra fields
137
- @types.each_value do |t|
138
- file = File.dirname(__FILE__) + "/contentful/model/#{t.name.underscore}.rb"
139
- require file if File.exist?(file)
140
- end
77
+ require_relative 'contentful/client_ext' if defined?(::Contentful)
141
78
  end
142
-
143
- ##
144
- # Runs validations over the content types returned from the Contentful API.
145
- # Validations are configured on predefined model classes using the
146
- # `validate_field` directive. Example:
147
- # validate_field :top_button, :Link, :optional, link_to: 'menuButton'
148
- # This results in a WCC::Contentful::ValidationError
149
- # if the 'topButton' field in the 'menu' content type is not a link.
150
- def self.validate_models!
151
- # Ensure application models are loaded before we validate
152
- Dir[Rails.root.join('app/models/**/*.rb')].each { |file| require file } if defined?(Rails)
153
-
154
- content_types = WCC::Contentful::ModelValidators.transform_content_types_for_validation(
155
- @content_types
156
- )
157
- errors = WCC::Contentful::Model.schema.call(content_types)
158
- raise WCC::Contentful::ValidationError, errors.errors unless errors.success?
159
- end
160
-
161
- ##
162
- # Calls the Contentful Sync API and updates the configured store with the returned
163
- # data.
164
- #
165
- # up_to_id: An ID that we know has changed and should come back from the sync.
166
- # If we don't find this ID in the sync data, then drop a job to try
167
- # the sync again after a few minutes.
168
- #
169
- def self.sync!(up_to_id: nil)
170
- return unless store.respond_to?(:index)
171
-
172
- @mutex.synchronize do
173
- sync_resp = client.sync(sync_token: next_sync_token)
174
-
175
- id_found = up_to_id.nil?
176
-
177
- sync_resp.items.each do |item|
178
- id = item.dig('sys', 'id')
179
- id_found ||= id == up_to_id
180
- store.index(item)
181
- end
182
- store.set("sync:#{configuration.space}:token", sync_resp.next_sync_token)
183
- @next_sync_token = sync_resp.next_sync_token
184
-
185
- unless id_found
186
- raise SyncError, "ID '#{up_to_id}' did not come back via sync." unless defined?(Rails)
187
- sync_later!(up_to_id: up_to_id)
188
- end
189
- next_sync_token
190
- end
191
- end
192
-
193
- ##
194
- # Drops an ActiveJob job to invoke WCC::Contentful.sync! after a given amount
195
- # of time.
196
- def self.sync_later!(up_to_id: nil, wait: 10.minutes)
197
- raise NotImplementedError, 'Cannot sync_later! outside of a Rails app' unless defined?(Rails)
198
-
199
- WCC::Contentful::DelayedSyncJob.set(wait: wait).perform_later(up_to_id)
200
- end
201
-
202
- # TODO: https://zube.io/watermarkchurch/development/c/2234 init graphql
203
- # def self.init_graphql!
204
- # require 'wcc/contentful/graphql'
205
- # etc...
206
- # end
207
79
  end
@@ -5,11 +5,24 @@ class Contentful::Client
5
5
  alias_method :old_get_http, :get_http
6
6
  end
7
7
 
8
+ def self.adapter
9
+ @adapter ||=
10
+ WCC::Contentful::SimpleClient.load_adapter(WCC::Contentful.configuration.http_adapter) ||
11
+ ->(url, query, headers, proxy) { old_get_http(url, query, headers, proxy) }
12
+ end
13
+
8
14
  def self.get_http(url, query, headers = {}, proxy = {})
9
- if override = WCC::Contentful.configuration.http_adapter
10
- override.call(url, query, headers, proxy)
11
- else
12
- old_get_http(url, query, headers, proxy)
15
+ if environment = WCC::Contentful.configuration.environment
16
+ url = rewrite_to_environment(url, environment)
13
17
  end
18
+
19
+ adapter.call(url, query, headers, proxy)
20
+ end
21
+
22
+ REWRITE_REGEXP = /^(https?\:\/\/(?:\w+)\.contentful\.com\/spaces\/[^\/]+\/)(?!environments)(.+)$/
23
+ def self.rewrite_to_environment(url, environment)
24
+ return url unless m = REWRITE_REGEXP.match(url)
25
+
26
+ File.join(m[1], 'environments', environment, m[2])
14
27
  end
15
28
  end
@@ -3,6 +3,7 @@
3
3
  class WCC::Contentful::Configuration
4
4
  ATTRIBUTES = %i[
5
5
  access_token
6
+ app_url
6
7
  management_token
7
8
  space
8
9
  environment
@@ -13,23 +14,28 @@ class WCC::Contentful::Configuration
13
14
  sync_cache_store
14
15
  webhook_username
15
16
  webhook_password
17
+ webhook_jobs
16
18
  ].freeze
17
19
  attr_accessor(*ATTRIBUTES)
18
20
 
19
- ##
21
+ # Returns true if the currently configured environment is pointing at `master`.
22
+ def master?
23
+ !environment.present?
24
+ end
25
+
20
26
  # Defines the method by which content is downloaded from the Contentful CDN.
21
27
  #
22
28
  # [:direct] `config.content_delivery = :direct`
23
29
  # with the `:direct` method, all queries result in web requests to
24
30
  # 'https://cdn.contentful.com' via the
25
- # {SimpleClient}[rdoc-ref:WCC::Contentful::SimpleClient::Cdn]
31
+ # {WCC::Contentful::SimpleClient::Cdn SimpleClient}
26
32
  #
27
33
  # [:eager_sync] `config.content_delivery = :eager_sync, [sync_store], [options]`
28
34
  # with the `:eager_sync` method, the entire content of the Contentful
29
35
  # space is downloaded locally and stored in the
30
- # {Sync Store}[rdoc-ref:WCC::Contentful.store]. The application is responsible
31
- # to periodically call `WCC::Contentful.sync!` to keep the store updated.
32
- # Alternatively, the provided {Engine}[WCC::Contentful::Engine]
36
+ # {WCC::Contentful::Services#store configured store}. The application is
37
+ # responsible to periodically call `WCC::Contentful.sync!` to keep the store
38
+ # updated. Alternatively, the provided {WCC::Contentful::Engine Engine}
33
39
  # can be mounted to receive a webhook from the Contentful space
34
40
  # on publish events:
35
41
  # mount WCC::Contentful::Engine, at: '/wcc/contentful'
@@ -51,6 +57,7 @@ class WCC::Contentful::Configuration
51
57
 
52
58
  WCC::Contentful::Store::Factory.new(
53
59
  self,
60
+ nil,
54
61
  cd,
55
62
  cd_params
56
63
  ).validate!
@@ -59,31 +66,18 @@ class WCC::Contentful::Configuration
59
66
  @content_delivery_params = cd_params
60
67
  end
61
68
 
62
- ##
63
- # Initializes the configured Sync Store.
64
- def store(preview: false)
65
- if preview
66
- @preview_store ||= WCC::Contentful::Store::Factory.new(
67
- self,
68
- :direct,
69
- [{ preview: preview }]
70
- ).build_sync_store
71
- else
72
- @store ||= WCC::Contentful::Store::Factory.new(
73
- self,
74
- @content_delivery,
75
- @content_delivery_params
76
- ).build_sync_store
77
- end
78
- end
69
+ attr_reader :content_delivery_params
79
70
 
80
- ##
81
71
  # Directly sets the adapter layer for communicating with Contentful
82
72
  def store=(value)
83
73
  @content_delivery = :custom
84
- @store = value
74
+ store, *cd_params = value
75
+ @store = store
76
+ @content_delivery_params = cd_params
85
77
  end
86
78
 
79
+ attr_reader :store
80
+
87
81
  # Sets the adapter which is used to make HTTP requests.
88
82
  # If left unset, the gem attempts to load either 'http' or 'typhoeus'.
89
83
  # You can pass your own adapter which responds to 'call', or even a lambda
@@ -93,76 +87,23 @@ class WCC::Contentful::Configuration
93
87
 
94
88
  def initialize
95
89
  @access_token = ''
90
+ @app_url = ENV['APP_URL']
96
91
  @management_token = ''
97
92
  @preview_token = ''
98
93
  @space = ''
99
94
  @default_locale = nil
100
95
  @content_delivery = :direct
101
- end
102
-
103
- ##
104
- # Gets a {CDN Client}[rdoc-ref:WCC::Contentful::SimpleClient::Cdn] which provides
105
- # methods for getting and paging raw JSON data from the Contentful CDN.
106
- attr_reader :client
107
- attr_reader :management_client
108
- attr_reader :preview_client
109
-
110
- ##
111
- # Called by WCC::Contentful.init! to configure the
112
- # Contentful clients. This method can be called independently of `init!` if
113
- # the application would prefer not to generate all the models.
114
- #
115
- # If the {contentful.rb}[https://github.com/contentful/contentful.rb] gem is
116
- # loaded, it is extended to make use of the `http_adapter` lambda.
117
- def configure_contentful
118
- @client = nil
119
- @management_client = nil
120
- @preview_client = nil
121
-
122
- if defined?(::ContentfulModel)
123
- ContentfulModel.configure do |config|
124
- config.access_token = access_token
125
- config.management_token = management_token if management_token.present?
126
- config.space = space
127
- config.default_locale = default_locale || 'en-US'
128
- end
129
- end
130
-
131
- require_relative 'client_ext' if defined?(::Contentful)
132
-
133
- @client = WCC::Contentful::SimpleClient::Cdn.new(
134
- access_token: access_token,
135
- space: space,
136
- default_locale: default_locale,
137
- adapter: http_adapter,
138
- environment: environment
139
- )
140
-
141
- if preview_token.present?
142
- @preview_client = WCC::Contentful::SimpleClient::Preview.new(
143
- preview_token: preview_token,
144
- space: space,
145
- default_locale: default_locale,
146
- adapter: http_adapter
147
- )
148
- end
149
-
150
- return unless management_token.present?
151
- @management_client = WCC::Contentful::SimpleClient::Management.new(
152
- management_token: management_token,
153
- space: space,
154
- default_locale: default_locale,
155
- adapter: http_adapter,
156
- environment: environment
157
- )
96
+ @webhook_jobs = []
158
97
  end
159
98
 
160
99
  def validate!
161
100
  raise ArgumentError, 'Please provide "space"' unless space.present?
162
101
  raise ArgumentError, 'Please provide "access_token"' unless access_token.present?
163
102
 
164
- return if environment.nil? || %i[direct custom].include?(content_delivery)
165
- raise ArgumentError, 'The Contentful Sync API currently does not work with environments. ' \
166
- 'You can use the ":direct" content_delivery method, or provide a custom store implementation.'
103
+ webhook_jobs&.each do |job|
104
+ next if job.respond_to?(:call) || job.respond_to?(:perform_later)
105
+
106
+ raise ArgumentError, "The job '#{job}' must be an instance of ActiveJob::Base or respond to :call"
107
+ end
167
108
  end
168
109
  end
@@ -4,6 +4,25 @@ module WCC::Contentful
4
4
  class Engine < ::Rails::Engine
5
5
  isolate_namespace WCC::Contentful
6
6
 
7
+ initializer 'enable webhook' do
8
+ config = WCC::Contentful.configuration
9
+ next unless config&.management_token.present?
10
+ next unless config.app_url.present?
11
+
12
+ if Rails.env.production?
13
+ WebhookEnableJob.set(wait: 10.seconds).perform_later(
14
+ management_token: config.management_token,
15
+ app_url: config.app_url,
16
+ space: config.space,
17
+ environment: config.environment,
18
+ default_locale: config.default_locale,
19
+ adapter: config.http_adapter,
20
+ webhook_username: config.webhook_username,
21
+ webhook_password: config.webhook_password
22
+ )
23
+ end
24
+ end
25
+
7
26
  config.generators do |g|
8
27
  g.test_framework :rspec, fixture: false
9
28
  end
@@ -1,40 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WCC::Contentful
4
- class ValidationError < StandardError
5
- Message =
6
- Struct.new(:path, :error) do
7
- def to_s
8
- "#{path}: #{error}"
9
- end
10
- end
4
+ class SyncError < StandardError
5
+ end
11
6
 
12
- attr_reader :errors
7
+ # Raised when a constant under {WCC::Contentful::Model} does not match to a
8
+ # content type in the configured Contentful space
9
+ class ContentTypeNotFoundError < NameError
10
+ end
13
11
 
14
- def initialize(errors)
15
- @errors = ValidationError.join_msg_keys(errors)
16
- super("Content Type Schema from Contentful failed validation!\n #{@errors.join("\n ")}")
17
- end
12
+ # Raised when an entry contains a circular reference and cannot be represented
13
+ # as a flat tree.
14
+ class CircularReferenceError < StandardError
15
+ attr_reader :stack
16
+ attr_reader :id
18
17
 
19
- # Turns the error messages hash into an array of message structs like:
20
- # menu.fields.name.type: must be equal to String
21
- def self.join_msg_keys(hash)
22
- ret =
23
- hash.map do |k, v|
24
- if v.is_a?(Hash)
25
- msgs = join_msg_keys(v)
26
- msgs.map { |msg| Message.new(k.to_s + '.' + msg.path, msg.error) }
27
- else
28
- v.map { |msg| Message.new(k.to_s, msg) }
29
- end
30
- end
31
- ret.flatten(1)
18
+ def initialize(stack, id)
19
+ @id = id
20
+ @stack = stack.slice(stack.index(id)..stack.length)
21
+ super('Circular reference detected!')
32
22
  end
33
- end
34
23
 
35
- class SyncError < StandardError
24
+ def message
25
+ return super unless stack
26
+
27
+ super + "\n " \
28
+ "#{stack.last} points to #{id} which is also it's ancestor\n " +
29
+ stack.join('->')
30
+ end
36
31
  end
37
32
 
38
- class ContentTypeNotFoundError < NameError
33
+ # Raised by {WCC::Contentful::ModelMethods#resolve Model#resolve} when attempting
34
+ # to resolve an entry's links and that entry cannot be found in the space.
35
+ class ResolveError < StandardError
39
36
  end
40
37
  end