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