wcc-contentful 0.4.0.pre.rc → 1.0.0.pre.rc1

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 +5 -5
  2. data/Guardfile +43 -0
  3. data/README.md +101 -12
  4. data/app/controllers/wcc/contentful/webhook_controller.rb +25 -24
  5. data/app/jobs/wcc/contentful/webhook_enable_job.rb +36 -2
  6. data/config/routes.rb +1 -1
  7. data/doc/wcc-contentful.png +0 -0
  8. data/lib/tasks/download_schema.rake +12 -0
  9. data/lib/wcc/contentful.rb +70 -16
  10. data/lib/wcc/contentful/active_record_shim.rb +72 -0
  11. data/lib/wcc/contentful/configuration.rb +177 -46
  12. data/lib/wcc/contentful/content_type_indexer.rb +14 -0
  13. data/lib/wcc/contentful/downloads_schema.rb +112 -0
  14. data/lib/wcc/contentful/engine.rb +33 -14
  15. data/lib/wcc/contentful/event.rb +171 -0
  16. data/lib/wcc/contentful/events.rb +41 -0
  17. data/lib/wcc/contentful/exceptions.rb +3 -0
  18. data/lib/wcc/contentful/indexed_representation.rb +2 -2
  19. data/lib/wcc/contentful/instrumentation.rb +31 -0
  20. data/lib/wcc/contentful/link.rb +28 -0
  21. data/lib/wcc/contentful/link_visitor.rb +122 -0
  22. data/lib/wcc/contentful/middleware.rb +7 -0
  23. data/lib/wcc/contentful/middleware/store.rb +158 -0
  24. data/lib/wcc/contentful/middleware/store/caching_middleware.rb +114 -0
  25. data/lib/wcc/contentful/model.rb +37 -3
  26. data/lib/wcc/contentful/model_builder.rb +1 -0
  27. data/lib/wcc/contentful/model_methods.rb +40 -15
  28. data/lib/wcc/contentful/model_singleton_methods.rb +47 -30
  29. data/lib/wcc/contentful/rake.rb +3 -0
  30. data/lib/wcc/contentful/rspec.rb +13 -8
  31. data/lib/wcc/contentful/services.rb +61 -27
  32. data/lib/wcc/contentful/simple_client.rb +81 -25
  33. data/lib/wcc/contentful/simple_client/management.rb +43 -10
  34. data/lib/wcc/contentful/simple_client/response.rb +61 -22
  35. data/lib/wcc/contentful/simple_client/typhoeus_adapter.rb +17 -17
  36. data/lib/wcc/contentful/store.rb +7 -66
  37. data/lib/wcc/contentful/store/README.md +85 -0
  38. data/lib/wcc/contentful/store/base.rb +34 -119
  39. data/lib/wcc/contentful/store/cdn_adapter.rb +71 -12
  40. data/lib/wcc/contentful/store/factory.rb +186 -0
  41. data/lib/wcc/contentful/store/instrumentation.rb +55 -0
  42. data/lib/wcc/contentful/store/interface.rb +82 -0
  43. data/lib/wcc/contentful/store/memory_store.rb +27 -24
  44. data/lib/wcc/contentful/store/postgres_store.rb +253 -107
  45. data/lib/wcc/contentful/store/postgres_store/schema_1.sql +73 -0
  46. data/lib/wcc/contentful/store/postgres_store/schema_2.sql +21 -0
  47. data/lib/wcc/contentful/store/query.rb +246 -0
  48. data/lib/wcc/contentful/store/query/interface.rb +63 -0
  49. data/lib/wcc/contentful/store/rspec_examples.rb +48 -0
  50. data/lib/wcc/contentful/store/rspec_examples/basic_store.rb +629 -0
  51. data/lib/wcc/contentful/store/rspec_examples/include_param.rb +283 -0
  52. data/lib/wcc/contentful/store/rspec_examples/nested_queries.rb +342 -0
  53. data/lib/wcc/contentful/sync_engine.rb +181 -0
  54. data/lib/wcc/contentful/test/attributes.rb +17 -5
  55. data/lib/wcc/contentful/test/factory.rb +22 -46
  56. data/lib/wcc/contentful/version.rb +1 -1
  57. data/wcc-contentful.gemspec +14 -11
  58. metadata +201 -146
  59. data/Gemfile +0 -6
  60. data/app/jobs/wcc/contentful/delayed_sync_job.rb +0 -63
  61. data/lib/wcc/contentful/client_ext.rb +0 -28
  62. data/lib/wcc/contentful/graphql.rb +0 -14
  63. data/lib/wcc/contentful/graphql/builder.rb +0 -177
  64. data/lib/wcc/contentful/graphql/types.rb +0 -54
  65. data/lib/wcc/contentful/simple_client/http_adapter.rb +0 -24
  66. data/lib/wcc/contentful/store/lazy_cache_store.rb +0 -161
@@ -2,29 +2,48 @@
2
2
 
3
3
  module WCC::Contentful
4
4
  class Engine < ::Rails::Engine
5
- isolate_namespace WCC::Contentful
6
-
7
5
  initializer 'enable webhook' do
8
6
  config = WCC::Contentful.configuration
9
- next unless config&.management_token.present?
10
- next unless config.app_url.present?
11
7
 
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
8
+ jobs = []
9
+ if WCC::Contentful::Services.instance.sync_engine&.should_sync?
10
+ jobs << WCC::Contentful::SyncEngine::Job
11
+ end
12
+ jobs.push(*WCC::Contentful.configuration.webhook_jobs)
13
+
14
+ jobs.each do |job|
15
+ WCC::Contentful::WebhookController.subscribe(
16
+ ->(event) do
17
+ begin
18
+ if job.respond_to?(:perform_later)
19
+ job.perform_later(event.to_h)
20
+ else
21
+ Rails.logger.error "Misconfigured webhook job: #{job} does not respond to " \
22
+ ':perform_later'
23
+ end
24
+ rescue StandardError => e
25
+ warn "Error in job #{job}: #{e}"
26
+ Rails.logger.error "Error in job #{job}: #{e}"
27
+ end
28
+ end,
29
+ with: :call
22
30
  )
23
31
  end
32
+
33
+ next unless config&.management_token.present?
34
+ next unless config.app_url.present?
35
+
36
+ WebhookEnableJob.set(wait: 10.seconds).perform_later if Rails.env.production?
24
37
  end
25
38
 
26
39
  config.generators do |g|
27
40
  g.test_framework :rspec, fixture: false
28
41
  end
42
+
43
+ # Clear the model registry to allow dev reloads to work properly
44
+ # https://api.rubyonrails.org/classes/Rails/Railtie/Configuration.html#method-i-to_prepare
45
+ config.to_prepare do
46
+ WCC::Contentful::Model.reload!
47
+ end
29
48
  end
30
49
  end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+
5
+ module WCC::Contentful::Event
6
+ extend ActiveSupport::Concern
7
+
8
+ # Creates an Event out of a raw value received by a webhook or given from
9
+ # the Contentful Sync API.
10
+ def self.from_raw(raw, context = nil)
11
+ const = Registry.instance.get(raw.dig('sys', 'type'))
12
+
13
+ const.new(raw, context)
14
+ end
15
+
16
+ class Registry
17
+ include Singleton
18
+
19
+ def get(name)
20
+ @event_types ||= {}
21
+ @event_types[name] || WCC::Contentful::Event::Unknown
22
+ end
23
+
24
+ def register(constant)
25
+ name = constant.try(:type) || constant.name.demodulize
26
+ unless constant.respond_to?(:new)
27
+ raise ArgumentError, "Constant #{constant} does not define 'new'"
28
+ end
29
+
30
+ @event_types ||= {}
31
+ @event_types[name] = constant
32
+ end
33
+ end
34
+
35
+ included do
36
+ Registry.instance.register(self)
37
+
38
+ def initialize(raw, context = nil, source: nil)
39
+ @raw = raw.freeze
40
+ @source = source
41
+
42
+ created_at = raw.dig('sys', 'createdAt')
43
+ created_at = Time.parse(created_at) if created_at.present?
44
+ updated_at = raw.dig('sys', 'updatedAt')
45
+ updated_at = Time.parse(updated_at) if updated_at.present?
46
+ @sys = WCC::Contentful::Sys.new(
47
+ raw.dig('sys', 'id'),
48
+ raw.dig('sys', 'type'),
49
+ raw.dig('sys', 'locale') || context.try(:[], :locale) || 'en-US',
50
+ raw.dig('sys', 'space', 'sys', 'id'),
51
+ created_at,
52
+ updated_at,
53
+ raw.dig('sys', 'revision'),
54
+ OpenStruct.new(context).freeze
55
+ )
56
+ end
57
+
58
+ attr_reader :sys
59
+ attr_reader :raw
60
+ attr_reader :source
61
+ delegate :id, to: :sys
62
+ delegate :type, to: :sys
63
+ delegate :created_at, to: :sys
64
+ delegate :updated_at, to: :sys
65
+ delegate :revision, to: :sys
66
+ delegate :space, to: :sys
67
+
68
+ delegate :dig, :[], to: :to_h
69
+ delegate :to_h, to: :raw
70
+ end
71
+ end
72
+
73
+ class WCC::Contentful::Event::Entry
74
+ include WCC::Contentful::Event
75
+
76
+ def content_type
77
+ raw.dig('sys', 'contentType', 'sys', 'id')
78
+ end
79
+
80
+ def entry
81
+ @entry ||= WCC::Contentful::Model.new_from_raw(raw, sys.context)
82
+ end
83
+ end
84
+
85
+ class WCC::Contentful::Event::Asset
86
+ include WCC::Contentful::Event
87
+
88
+ def content_type
89
+ 'Asset'
90
+ end
91
+
92
+ def asset
93
+ @asset ||= WCC::Contentful::Model.new_from_raw(raw, sys.context)
94
+ end
95
+
96
+ alias_method :entry, :asset
97
+ end
98
+
99
+ class WCC::Contentful::Event::DeletedEntry
100
+ include WCC::Contentful::Event
101
+
102
+ def deleted_at
103
+ raw.dig('sys', 'deletedAt')
104
+ end
105
+
106
+ def content_type
107
+ raw.dig('sys', 'contentType', 'sys', 'id')
108
+ end
109
+
110
+ def entry
111
+ @entry ||= WCC::Contentful::Model.new_from_raw(raw, sys.context)
112
+ end
113
+ end
114
+
115
+ class WCC::Contentful::Event::DeletedAsset
116
+ include WCC::Contentful::Event
117
+
118
+ def deleted_at
119
+ raw.dig('sys', 'deletedAt')
120
+ end
121
+
122
+ def content_type
123
+ 'Asset'
124
+ end
125
+
126
+ def asset
127
+ @asset ||= WCC::Contentful::Model.new_from_raw(raw, sys.context)
128
+ end
129
+
130
+ alias_method :entry, :asset
131
+ end
132
+
133
+ class WCC::Contentful::Event::SyncComplete
134
+ include WCC::Contentful::Event
135
+
136
+ def initialize(items, context = nil, source: nil)
137
+ items =
138
+ items.map do |item|
139
+ next item if item.is_a? WCC::Contentful::Event
140
+
141
+ WCC::Contentful::Event.from_raw(item, context, source: source)
142
+ end
143
+ @items = items.freeze
144
+ @source = source
145
+ @sys = WCC::Contentful::Sys.new(
146
+ nil,
147
+ 'Array',
148
+ nil,
149
+ nil,
150
+ nil,
151
+ nil,
152
+ nil,
153
+ OpenStruct.new(context).freeze
154
+ )
155
+ end
156
+
157
+ attr_reader :sys, :items, :source
158
+
159
+ def to_h
160
+ {
161
+ 'sys' => {
162
+ 'type' => 'Array'
163
+ },
164
+ 'items' => items.map(&:to_h)
165
+ }
166
+ end
167
+ end
168
+
169
+ class WCC::Contentful::Event::Unknown
170
+ include WCC::Contentful::Event
171
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'wisper'
4
+ require 'singleton'
5
+
6
+ # WCC::Contentful::Events is a singleton which rebroadcasts Contentful update
7
+ # events. You can subscribe to these events in your initializer using the
8
+ # [wisper gem syntax](https://github.com/krisleech/wisper).
9
+ # All published events are in the namespace WCC::Contentful::Event.
10
+ class WCC::Contentful::Events
11
+ include Wisper::Publisher
12
+
13
+ def self.instance
14
+ @instance ||= new
15
+ end
16
+
17
+ def initialize
18
+ _attach_listeners
19
+ end
20
+
21
+ def rebroadcast(event)
22
+ type = event.dig('sys', 'type')
23
+ raise ArgumentError, "Unknown event type #{event}" unless type.present?
24
+
25
+ broadcast(type, event)
26
+ end
27
+
28
+ private
29
+
30
+ def _attach_listeners
31
+ publishers = [
32
+ WCC::Contentful::Services.instance.sync_engine
33
+ ]
34
+
35
+ publishers << WCC::Contentful::WebhookController if defined?(Rails)
36
+
37
+ publishers.each do |publisher|
38
+ publisher.subscribe(self, with: :rebroadcast) if publisher.present?
39
+ end
40
+ end
41
+ end
@@ -34,4 +34,7 @@ module WCC::Contentful
34
34
  # to resolve an entry's links and that entry cannot be found in the space.
35
35
  class ResolveError < StandardError
36
36
  end
37
+
38
+ class InitializationError < StandardError
39
+ end
37
40
  end
@@ -29,8 +29,8 @@ module WCC::Contentful
29
29
  ret
30
30
  end
31
31
 
32
- def to_json
33
- @types.to_json
32
+ def to_json(*args)
33
+ @types.to_json(*args)
34
34
  end
35
35
 
36
36
  def deep_dup
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WCC::Contentful
4
+ module Instrumentation
5
+ extend ActiveSupport::Concern
6
+
7
+ def _instrumentation_event_prefix
8
+ @_instrumentation_event_prefix ||=
9
+ # WCC::Contentful => contentful.wcc
10
+ '.' + (is_a?(Class) || is_a?(Module) ? self : self.class)
11
+ .name.parameterize.split('-').reverse.join('.')
12
+ end
13
+
14
+ included do
15
+ protected
16
+
17
+ def _instrument(name, payload = {}, &block)
18
+ name += _instrumentation_event_prefix
19
+ (@_instrumentation ||= WCC::Contentful::Services.instance.instrumentation)
20
+ .instrument(name, payload, &block)
21
+ end
22
+ end
23
+
24
+ class << self
25
+ def instrument(name, payload = {}, &block)
26
+ WCC::Contentful::Services.instance
27
+ .instrumentation.instrument(name, payload, &block)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class WCC::Contentful::Link
4
+ attr_reader :id
5
+ attr_reader :link_type
6
+ attr_reader :raw
7
+
8
+ LINK_TYPES = {
9
+ Asset: 'Asset',
10
+ Link: 'Entry'
11
+ }.freeze
12
+
13
+ def initialize(model, link_type = nil)
14
+ @id = model.try(:id) || model
15
+ @link_type = link_type
16
+ @link_type ||= model.is_a?(WCC::Contentful::Model::Asset) ? :Asset : :Link
17
+ @raw =
18
+ {
19
+ 'sys' => {
20
+ 'type' => 'Link',
21
+ 'linkType' => LINK_TYPES[@link_type] || link_type,
22
+ 'id' => @id
23
+ }
24
+ }
25
+ end
26
+
27
+ alias_method :to_h, :raw
28
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The LinkVisitor is a utility class for walking trees of linked entries.
4
+ # It is used internally by the Store layer to compose the resulting resolved hashes.
5
+ # But you can use it too!
6
+ class WCC::Contentful::LinkVisitor
7
+ attr_reader :entry
8
+ attr_reader :type
9
+ attr_reader :fields
10
+ attr_reader :depth
11
+
12
+ # @param [Hash] entry The entry hash (resolved or unresolved) to walk
13
+ # @param [Array<String, Symbol>] The fields to select from the entry tree.
14
+ # Use `:Link` to select only links, or `'slug'` to select all slugs in the tree.
15
+ # @param [Fixnum] depth (optional) How far to walk down the tree of links. Be careful of
16
+ # recursive trees!
17
+ # @example
18
+ # entry = store.find_by(id: id, include: 3)
19
+ # WCC::Contentful::LinkVisitor.new(entry, 'slug', depth: 3)
20
+ # .map { |slug| 'https://mirror-site' + slug }
21
+ def initialize(entry, *fields, depth: 0)
22
+ unless entry.is_a?(Hash) && entry.dig('sys', 'id')
23
+ raise ArgumentError, "Please provide an entry as a hash value (got #{entry})"
24
+ end
25
+ unless ct = entry.dig('sys', 'contentType', 'sys', 'id')
26
+ raise ArgumentError, 'Entry has no content type!'
27
+ end
28
+
29
+ @type = WCC::Contentful.types[ct]
30
+ raise ArgumentError, "Unknown content type '#{ct}'" unless @type
31
+
32
+ @entry = entry
33
+ @fields = fields
34
+ @depth = depth
35
+ end
36
+
37
+ # Walks an entry and its resolved links, without transforming the entry.
38
+ # @yield [value, field, locale]
39
+ # @yieldparam [Object] value The value of the selected field.
40
+ # @yieldparam [WCC::Contentful::IndexedRepresentation::Field] field The type of the selected field
41
+ # @yieldparam [String] locale The locale of the current field value
42
+ # @returns nil
43
+ def each(&block)
44
+ _each do |val, field, locale, index|
45
+ yield(val, field, locale, index) if should_yield_field?(field)
46
+
47
+ next unless should_walk_link?(field, val)
48
+
49
+ self.class.new(val, *fields, depth: depth - 1).each(&block)
50
+ end
51
+
52
+ nil
53
+ end
54
+
55
+ def map!(&block)
56
+ _each do |val, field, locale, index|
57
+ if should_yield_field?(field)
58
+ val = yield(val, field, locale, index)
59
+ set_field(field, locale, index, val)
60
+ end
61
+
62
+ next unless should_walk_link?(field, val)
63
+
64
+ self.class.new(val, *fields, depth: depth - 1).map!(&block)
65
+ end
66
+
67
+ entry
68
+ end
69
+
70
+ private
71
+
72
+ def _each(&block)
73
+ type.fields.each_value do |f|
74
+ each_field(f, &block)
75
+ end
76
+ end
77
+
78
+ def each_field(field)
79
+ each_locale(field) do |val, locale|
80
+ if field.array
81
+ val&.each_with_index do |v, index|
82
+ yield(v, field, locale, index) unless v.nil?
83
+ end
84
+ else
85
+ yield(val, field, locale) unless val.nil?
86
+ end
87
+ end
88
+ end
89
+
90
+ def each_locale(field)
91
+ raw_value = entry.dig('fields', field.name)
92
+ if locale = entry.dig('sys', 'locale')
93
+ if raw_value.is_a?(Hash) && raw_value[locale]
94
+ # it's a locale=* entry, but they've added sys.locale to those now
95
+ raw_value = raw_value[locale]
96
+ end
97
+ yield(raw_value, locale)
98
+ else
99
+ raw_value&.each_with_object({}) do |(l, val), h|
100
+ h[l] = yield(val, l)
101
+ end
102
+ end
103
+ end
104
+
105
+ def should_yield_field?(field)
106
+ fields.empty? || fields.include?(field.type) || fields.include?(field.name)
107
+ end
108
+
109
+ def should_walk_link?(field, val, dep = depth)
110
+ dep > 0 && field.type == :Link && val.dig('sys', 'type') == 'Entry'
111
+ end
112
+
113
+ def set_field(field, locale, index, val)
114
+ current_field = (entry['fields'][field.name] ||= {})
115
+
116
+ if field.array
117
+ (current_field[locale] ||= [])[index] = val
118
+ else
119
+ current_field[locale] = val
120
+ end
121
+ end
122
+ end