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
@@ -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