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
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This module is extended by all models and defines singleton
4
+ # methods that are not dynamically generated.
5
+ # @api Model
6
+ module WCC::Contentful::ModelSingletonMethods
7
+ def store(preview = false)
8
+ if preview
9
+ if WCC::Contentful::Model.preview_store.nil?
10
+ raise ArgumentError,
11
+ 'You must include a contentful preview token in your WCC::Contentful.configure block'
12
+ end
13
+ WCC::Contentful::Model.preview_store
14
+ else
15
+ WCC::Contentful::Model.store
16
+ end
17
+ end
18
+
19
+ # Finds an instance of this content type.
20
+ #
21
+ # @return [nil, WCC::Contentful::Model] An instance of the appropriate model class
22
+ # for this content type, or nil if the ID does not exist in the space.
23
+ # @example
24
+ # WCC::Contentful::Model::Page.find(id)
25
+ def find(id, options: nil)
26
+ options ||= {}
27
+ raw = store(options[:preview])
28
+ .find(id, { hint: type }.merge!(options.except(:preview)))
29
+ new(raw, options) if raw.present?
30
+ end
31
+
32
+ # Finds all instances of this content type, optionally limiting to those matching
33
+ # a given filter query.
34
+ #
35
+ # @return [Enumerator::Lazy<WCC::Contentful::Model>, <WCC::Contentful::Model>]
36
+ # A set of instantiated model objects matching the given query.
37
+ # @example
38
+ # WCC::Contentful::Model::Page.find_all('sys.created_at' => { lte: Date.today })
39
+ def find_all(filter = nil)
40
+ filter = filter&.dup
41
+ options = filter&.delete(:options) || {}
42
+
43
+ if filter.present?
44
+ filter.transform_keys! { |k| k.to_s.camelize(:lower) }
45
+ bad_fields = filter.keys.reject { |k| self::FIELDS.include?(k) }
46
+ raise ArgumentError, "These fields do not exist: #{bad_fields}" unless bad_fields.empty?
47
+ end
48
+
49
+ query = store(options[:preview])
50
+ .find_all(content_type: content_type, options: options.except(:preview))
51
+ query = query.apply(filter) if filter.present?
52
+ query.map { |r| new(r, options) }
53
+ end
54
+
55
+ # Finds the first instance of this content type matching the given query.
56
+ #
57
+ # @return [nil, WCC::Contentful::Model] A set of instantiated model objects matching
58
+ # the given query.
59
+ # @example
60
+ # WCC::Contentful::Model::Page.find_by(slug: '/some-slug')
61
+ def find_by(filter = nil)
62
+ filter = filter&.dup
63
+ options = filter&.delete(:options) || {}
64
+
65
+ if filter.present?
66
+ filter.transform_keys! { |k| k.to_s.camelize(:lower) }
67
+ bad_fields = filter.keys.reject { |k| self::FIELDS.include?(k) }
68
+ raise ArgumentError, "These fields do not exist: #{bad_fields}" unless bad_fields.empty?
69
+ end
70
+
71
+ result = store(options[:preview])
72
+ .find_by(content_type: content_type, filter: filter, options: options.except(:preview))
73
+
74
+ new(result, options) if result
75
+ end
76
+
77
+ def inherited(subclass)
78
+ # only register if it's not already registered
79
+ return if WCC::Contentful::Model.registered?(content_type)
80
+
81
+ WCC::Contentful::Model.register_for_content_type(content_type, klass: subclass)
82
+ end
83
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+
5
+ module WCC::Contentful
6
+ class Services
7
+ include Singleton
8
+
9
+ # Gets the data-store which executes the queries run against the dynamic
10
+ # models in the WCC::Contentful::Model namespace.
11
+ # This is one of the following based on the configured content_delivery method:
12
+ #
13
+ # [:direct] an instance of {WCC::Contentful::Store::CDNAdapter} with a
14
+ # {WCC::Contentful::SimpleClient::Cdn CDN Client} to access the CDN.
15
+ #
16
+ # [:lazy_sync] an instance of {WCC::Contentful::Store::LazyCacheStore}
17
+ # with the configured ActiveSupport::Cache implementation and a
18
+ # {WCC::Contentful::SimpleClient::Cdn CDN Client} for when data
19
+ # cannot be found in the cache.
20
+ #
21
+ # [:eager_sync] an instance of the configured Store type, defined by
22
+ # {WCC::Contentful::Configuration#sync_store}
23
+ #
24
+ # @api Store
25
+ def store
26
+ @store ||=
27
+ ensure_configured do |config|
28
+ WCC::Contentful::Store::Factory.new(
29
+ config,
30
+ self,
31
+ config.content_delivery,
32
+ config.content_delivery_params
33
+ ).build_sync_store
34
+ end
35
+ end
36
+
37
+ # An instance of {WCC::Contentful::Store::CDNAdapter} which connects to the
38
+ # Contentful Preview API to return preview content.
39
+ #
40
+ # @api Store
41
+ def preview_store
42
+ @preview_store ||=
43
+ ensure_configured do |config|
44
+ WCC::Contentful::Store::Factory.new(
45
+ config,
46
+ self,
47
+ :direct,
48
+ [{ preview: true }]
49
+ ).build_sync_store
50
+ end
51
+ end
52
+
53
+ # Gets a {WCC::Contentful::SimpleClient::Cdn CDN Client} which provides
54
+ # methods for getting and paging raw JSON data from the Contentful CDN.
55
+ #
56
+ # @api Client
57
+ def client
58
+ @client ||=
59
+ ensure_configured do |config|
60
+ WCC::Contentful::SimpleClient::Cdn.new(
61
+ access_token: config.access_token,
62
+ space: config.space,
63
+ default_locale: config.default_locale,
64
+ adapter: config.http_adapter,
65
+ environment: config.environment
66
+ )
67
+ end
68
+ end
69
+
70
+ # Gets a {WCC::Contentful::SimpleClient::Cdn CDN Client} which provides
71
+ # methods for getting and paging raw JSON data from the Contentful Preview API.
72
+ #
73
+ # @api Client
74
+ def preview_client
75
+ @preview_client ||=
76
+ ensure_configured do |config|
77
+ if config.preview_token.present?
78
+ WCC::Contentful::SimpleClient::Preview.new(
79
+ preview_token: config.preview_token,
80
+ space: config.space,
81
+ default_locale: config.default_locale,
82
+ adapter: config.http_adapter,
83
+ environment: config.environment
84
+ )
85
+ end
86
+ end
87
+ end
88
+
89
+ # Gets a {WCC::Contentful::SimpleClient::Management Management Client} which provides
90
+ # methods for updating data via the Contentful Management API
91
+ #
92
+ # @api Client
93
+ def management_client
94
+ @management_client ||=
95
+ ensure_configured do |config|
96
+ if config.management_token.present?
97
+ WCC::Contentful::SimpleClient::Management.new(
98
+ management_token: config.management_token,
99
+ space: config.space,
100
+ default_locale: config.default_locale,
101
+ adapter: config.http_adapter,
102
+ environment: config.environment
103
+ )
104
+ end
105
+ end
106
+ end
107
+
108
+ private
109
+
110
+ def ensure_configured
111
+ if WCC::Contentful.configuration.nil?
112
+ raise StandardError, 'WCC::Contentful has not yet been configured!'
113
+ end
114
+
115
+ yield WCC::Contentful.configuration
116
+ end
117
+ end
118
+
119
+ # Include this module to define accessors for every method defined on the
120
+ # {Services} singleton.
121
+ #
122
+ # @example
123
+ # class MyJob < ApplicationJob
124
+ # include WCC::Contentful::ServiceAccessors
125
+ #
126
+ # def perform
127
+ # Page.find(...)
128
+ #
129
+ # store.find(...)
130
+ #
131
+ # client.entries(...)
132
+ # end
133
+ # end
134
+ # @see Services
135
+ module ServiceAccessors
136
+ SERVICES = (WCC::Contentful::Services.instance_methods -
137
+ Object.instance_methods -
138
+ Singleton.instance_methods)
139
+
140
+ SERVICES.each do |m|
141
+ define_method m do
142
+ Services.instance.public_send(m)
143
+ end
144
+ end
145
+ end
146
+ end
@@ -1,33 +1,59 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'simple_client/response'
4
+ require_relative 'simple_client/management'
4
5
 
5
6
  module WCC::Contentful
6
- ##
7
7
  # The SimpleClient accesses the Contentful CDN to get JSON responses,
8
- # returning the raw JSON data as a parsed hash.
8
+ # returning the raw JSON data as a parsed hash. This is the bottom layer of
9
+ # the WCC::Contentful gem.
10
+ #
11
+ # Note: Do not create this directly, instead create one of
12
+ # WCC::Contentful::SimpleClient::Cdn, WCC::Contentful::SimpleClient::Preview,
13
+ # WCC::Contentful::SimpleClient::Management
14
+ #
9
15
  # It can be configured to access any API url and exposes only a single method,
10
16
  # `get`. This method returns a WCC::Contentful::SimpleClient::Response
11
17
  # that handles paging automatically.
12
18
  #
13
19
  # The SimpleClient by default uses 'http' to perform the gets, but any HTTP
14
20
  # client can be injected by passing a proc as the `adapter:` option.
21
+ #
22
+ # @api Client
15
23
  class SimpleClient
24
+ attr_reader :api_url
25
+ attr_reader :space
26
+
27
+ # Creates a new SimpleClient with the given configuration.
28
+ #
29
+ # @param [String] api_url the base URL of the Contentful API to connect to
30
+ # @param [String] space The Space ID to access
31
+ # @param [String] access_token A Contentful Access Token to be sent in the Authorization header
32
+ # @param [Hash] options The remaining optional parameters, defined below
33
+ # @option options [Symbol, Object] adapter The Adapter to use to make requests.
34
+ # Auto-discovered based on what gems are installed if this is not provided.
35
+ # @option options [String] default_locale The locale query param to set by default.
36
+ # @option options [String] environment The contentful environment to access. Defaults to 'master'.
37
+ # @option options [Boolean] no_follow_redirects If true, do not follow 300 level redirects.
16
38
  def initialize(api_url:, space:, access_token:, **options)
17
39
  @api_url = URI.join(api_url, '/spaces/', space + '/')
18
40
  @space = space
19
41
  @access_token = access_token
20
42
 
21
- @get_http = SimpleClient.load_adapter(options[:adapter])
43
+ @adapter = SimpleClient.load_adapter(options[:adapter])
22
44
 
23
45
  @options = options
24
46
  @query_defaults = {}
25
47
  @query_defaults[:locale] = @options[:default_locale] if @options[:default_locale]
26
48
 
27
- return unless env = options[:environment]
28
- @api_url = URI.join(@api_url, 'environments/', env + '/')
49
+ return unless options[:environment].present?
50
+
51
+ @api_url = URI.join(@api_url, 'environments/', options[:environment] + '/')
29
52
  end
30
53
 
54
+ # performs an HTTP GET request to the specified path within the configured
55
+ # space and environment. Query parameters are merged with the defaults and
56
+ # appended to the request.
31
57
  def get(path, query = {})
32
58
  url = URI.join(@api_url, path)
33
59
 
@@ -79,7 +105,7 @@ module WCC::Contentful
79
105
  q = @query_defaults.dup
80
106
  q = q.merge(query) if query
81
107
 
82
- resp = @get_http.call(url, q, headers, proxy)
108
+ resp = @adapter.call(url, q, headers, proxy)
83
109
 
84
110
  if [301, 302, 307].include?(resp.code) && !@options[:no_follow_redirects]
85
111
  resp = get_http(resp.headers['location'], nil, headers, proxy)
@@ -87,11 +113,12 @@ module WCC::Contentful
87
113
  resp
88
114
  end
89
115
 
90
- ##
91
116
  # The CDN SimpleClient accesses 'https://cdn.contentful.com' to get raw
92
117
  # JSON responses. It exposes methods to query entries, assets, and content_types.
93
118
  # The responses are instances of WCC::Contentful::SimpleClient::Response
94
119
  # which handles paging automatically.
120
+ #
121
+ # @api Client
95
122
  class Cdn < SimpleClient
96
123
  def initialize(space:, access_token:, **options)
97
124
  super(
@@ -106,42 +133,36 @@ module WCC::Contentful
106
133
  'cdn'
107
134
  end
108
135
 
109
- ##
110
136
  # Gets an entry by ID
111
137
  def entry(key, query = {})
112
138
  resp = get("entries/#{key}", query)
113
139
  resp.assert_ok!
114
140
  end
115
141
 
116
- ##
117
142
  # Queries entries with optional query parameters
118
143
  def entries(query = {})
119
144
  resp = get('entries', query)
120
145
  resp.assert_ok!
121
146
  end
122
147
 
123
- ##
124
148
  # Gets an asset by ID
125
149
  def asset(key, query = {})
126
150
  resp = get("assets/#{key}", query)
127
151
  resp.assert_ok!
128
152
  end
129
153
 
130
- ##
131
154
  # Queries assets with optional query parameters
132
155
  def assets(query = {})
133
156
  resp = get('assets', query)
134
157
  resp.assert_ok!
135
158
  end
136
159
 
137
- ##
138
160
  # Queries content types with optional query parameters
139
161
  def content_types(query = {})
140
162
  resp = get('content_types', query)
141
163
  resp.assert_ok!
142
164
  end
143
165
 
144
- ##
145
166
  # Accesses the Sync API to get a list of items that have changed since
146
167
  # the last sync.
147
168
  #
@@ -161,26 +182,7 @@ module WCC::Contentful
161
182
  end
162
183
  end
163
184
 
164
- class Management < SimpleClient
165
- def initialize(space:, management_token:, **options)
166
- super(
167
- api_url: options[:api_url] || 'https://api.contentful.com',
168
- space: space,
169
- access_token: management_token,
170
- **options
171
- )
172
- end
173
-
174
- def client_type
175
- 'management'
176
- end
177
-
178
- def content_types(**query)
179
- resp = get('content_types', query)
180
- resp.assert_ok!
181
- end
182
- end
183
-
185
+ # @api Client
184
186
  class Preview < Cdn
185
187
  def initialize(space:, preview_token:, **options)
186
188
  super(
@@ -12,4 +12,13 @@ class HttpAdapter
12
12
  HTTP[headers].get(url, params: query)
13
13
  end
14
14
  end
15
+
16
+ def post(url, body, headers = {}, proxy = {})
17
+ if proxy[:host]
18
+ HTTP[headers].via(proxy[:host], proxy[:port], proxy[:username], proxy[:password])
19
+ .post(url, json: body)
20
+ else
21
+ HTTP[headers].post(url, json: body)
22
+ end
23
+ end
15
24
  end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api Client
4
+ class WCC::Contentful::SimpleClient::Management < WCC::Contentful::SimpleClient
5
+ def initialize(space:, management_token:, **options)
6
+ super(
7
+ api_url: options[:api_url] || 'https://api.contentful.com',
8
+ space: space,
9
+ access_token: management_token,
10
+ **options
11
+ )
12
+
13
+ @post_adapter = @adapter if @adapter.respond_to?(:post)
14
+ @post_adapter ||= self.class.load_adapter(nil)
15
+ end
16
+
17
+ def client_type
18
+ 'management'
19
+ end
20
+
21
+ def content_types(**query)
22
+ resp = get('content_types', query)
23
+ resp.assert_ok!
24
+ end
25
+
26
+ def webhook_definitions(**query)
27
+ resp = get("/spaces/#{space}/webhook_definitions", query)
28
+ resp.assert_ok!
29
+ end
30
+
31
+ # {
32
+ # "name": "My webhook",
33
+ # "url": "https://www.example.com/test",
34
+ # "topics": [
35
+ # "Entry.create",
36
+ # "ContentType.create",
37
+ # "*.publish",
38
+ # "Asset.*"
39
+ # ],
40
+ # "httpBasicUsername": "yolo",
41
+ # "httpBasicPassword": "yolo",
42
+ # "headers": [
43
+ # {
44
+ # "key": "header1",
45
+ # "value": "value1"
46
+ # },
47
+ # {
48
+ # "key": "header2",
49
+ # "value": "value2"
50
+ # }
51
+ # ]
52
+ # }
53
+ def post_webhook_definition(webhook)
54
+ resp = post("/spaces/#{space}/webhook_definitions", webhook)
55
+ resp.assert_ok!
56
+ end
57
+
58
+ def post(path, body)
59
+ url = URI.join(@api_url, path)
60
+
61
+ Response.new(self,
62
+ { url: url, body: body },
63
+ post_http(url, body))
64
+ end
65
+
66
+ private
67
+
68
+ def post_http(url, body, headers = {}, proxy = {})
69
+ headers = {
70
+ Authorization: "Bearer #{@access_token}",
71
+ 'Content-Type' => 'application/vnd.contentful.management.v1+json'
72
+ }.merge(headers || {})
73
+
74
+ resp = @post_adapter.post(url, body, headers, proxy)
75
+
76
+ if [301, 302, 307].include?(resp.code) && !@options[:no_follow_redirects]
77
+ resp = get_http(resp.headers['location'], nil, headers, proxy)
78
+ end
79
+ resp
80
+ end
81
+ end