wcc-contentful 0.3.0.pre.rc3 → 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 (69) hide show
  1. checksums.yaml +5 -5
  2. data/.rspec +1 -0
  3. data/Guardfile +101 -0
  4. data/README.md +161 -11
  5. data/app/controllers/wcc/contentful/webhook_controller.rb +25 -24
  6. data/app/jobs/wcc/contentful/webhook_enable_job.rb +36 -2
  7. data/config/routes.rb +1 -1
  8. data/doc/wcc-contentful.png +0 -0
  9. data/lib/tasks/download_schema.rake +12 -0
  10. data/lib/wcc/contentful.rb +70 -16
  11. data/lib/wcc/contentful/active_record_shim.rb +72 -0
  12. data/lib/wcc/contentful/configuration.rb +177 -46
  13. data/lib/wcc/contentful/content_type_indexer.rb +14 -0
  14. data/lib/wcc/contentful/downloads_schema.rb +112 -0
  15. data/lib/wcc/contentful/engine.rb +33 -14
  16. data/lib/wcc/contentful/event.rb +171 -0
  17. data/lib/wcc/contentful/events.rb +41 -0
  18. data/lib/wcc/contentful/exceptions.rb +3 -0
  19. data/lib/wcc/contentful/indexed_representation.rb +2 -2
  20. data/lib/wcc/contentful/instrumentation.rb +31 -0
  21. data/lib/wcc/contentful/link.rb +28 -0
  22. data/lib/wcc/contentful/link_visitor.rb +122 -0
  23. data/lib/wcc/contentful/middleware.rb +7 -0
  24. data/lib/wcc/contentful/middleware/store.rb +158 -0
  25. data/lib/wcc/contentful/middleware/store/caching_middleware.rb +114 -0
  26. data/lib/wcc/contentful/model.rb +37 -3
  27. data/lib/wcc/contentful/model_builder.rb +1 -0
  28. data/lib/wcc/contentful/model_methods.rb +40 -15
  29. data/lib/wcc/contentful/model_singleton_methods.rb +47 -30
  30. data/lib/wcc/contentful/rake.rb +3 -0
  31. data/lib/wcc/contentful/rspec.rb +46 -0
  32. data/lib/wcc/contentful/services.rb +61 -27
  33. data/lib/wcc/contentful/simple_client.rb +81 -25
  34. data/lib/wcc/contentful/simple_client/management.rb +43 -10
  35. data/lib/wcc/contentful/simple_client/response.rb +61 -22
  36. data/lib/wcc/contentful/simple_client/typhoeus_adapter.rb +17 -17
  37. data/lib/wcc/contentful/store.rb +7 -66
  38. data/lib/wcc/contentful/store/README.md +85 -0
  39. data/lib/wcc/contentful/store/base.rb +34 -119
  40. data/lib/wcc/contentful/store/cdn_adapter.rb +71 -12
  41. data/lib/wcc/contentful/store/factory.rb +186 -0
  42. data/lib/wcc/contentful/store/instrumentation.rb +55 -0
  43. data/lib/wcc/contentful/store/interface.rb +82 -0
  44. data/lib/wcc/contentful/store/memory_store.rb +27 -24
  45. data/lib/wcc/contentful/store/postgres_store.rb +268 -101
  46. data/lib/wcc/contentful/store/postgres_store/schema_1.sql +73 -0
  47. data/lib/wcc/contentful/store/postgres_store/schema_2.sql +21 -0
  48. data/lib/wcc/contentful/store/query.rb +246 -0
  49. data/lib/wcc/contentful/store/query/interface.rb +63 -0
  50. data/lib/wcc/contentful/store/rspec_examples.rb +48 -0
  51. data/lib/wcc/contentful/store/rspec_examples/basic_store.rb +629 -0
  52. data/lib/wcc/contentful/store/rspec_examples/include_param.rb +283 -0
  53. data/lib/wcc/contentful/store/rspec_examples/nested_queries.rb +342 -0
  54. data/lib/wcc/contentful/sync_engine.rb +181 -0
  55. data/lib/wcc/contentful/test.rb +7 -0
  56. data/lib/wcc/contentful/test/attributes.rb +56 -0
  57. data/lib/wcc/contentful/test/double.rb +76 -0
  58. data/lib/wcc/contentful/test/factory.rb +101 -0
  59. data/lib/wcc/contentful/version.rb +1 -1
  60. data/wcc-contentful.gemspec +22 -10
  61. metadata +282 -103
  62. data/Gemfile +0 -8
  63. data/app/jobs/wcc/contentful/delayed_sync_job.rb +0 -63
  64. data/lib/wcc/contentful/client_ext.rb +0 -28
  65. data/lib/wcc/contentful/graphql.rb +0 -14
  66. data/lib/wcc/contentful/graphql/builder.rb +0 -177
  67. data/lib/wcc/contentful/graphql/types.rb +0 -54
  68. data/lib/wcc/contentful/simple_client/http_adapter.rb +0 -24
  69. data/lib/wcc/contentful/store/lazy_cache_store.rb +0 -161
@@ -6,6 +6,20 @@ module WCC::Contentful
6
6
  class ContentTypeIndexer
7
7
  include WCC::Contentful::Helpers
8
8
 
9
+ class << self
10
+ def load(schema_file)
11
+ from_json_schema(
12
+ JSON.parse(File.read(schema_file))['contentTypes']
13
+ )
14
+ end
15
+
16
+ def from_json_schema(schema)
17
+ new.tap do |ixr|
18
+ schema.each { |type| ixr.index(type) }
19
+ end
20
+ end
21
+ end
22
+
9
23
  attr_reader :types
10
24
 
11
25
  def initialize
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'wcc/contentful'
4
+
5
+ class WCC::Contentful::DownloadsSchema
6
+ def self.call(file = nil, management_client: nil)
7
+ new(file, management_client: management_client).call
8
+ end
9
+
10
+ def initialize(file = nil, management_client: nil)
11
+ @client = management_client || WCC::Contentful::Services.instance.management_client
12
+ @file = file || WCC::Contentful.configuration&.schema_file
13
+ raise ArgumentError, 'Please configure your management token' unless @client
14
+ raise ArgumentError, 'Please pass filename or call WCC::Contentful.configure' unless @file
15
+ end
16
+
17
+ def call
18
+ return unless needs_update?
19
+
20
+ update!
21
+ end
22
+
23
+ def update!
24
+ FileUtils.mkdir_p(File.dirname(@file))
25
+
26
+ File.write(@file, format_json({
27
+ 'contentTypes' => content_types,
28
+ 'editorInterfaces' => editor_interfaces
29
+ }))
30
+ end
31
+
32
+ def needs_update?
33
+ return true unless File.exist?(@file)
34
+
35
+ contents =
36
+ begin
37
+ JSON.parse(File.read(@file))
38
+ rescue JSON::ParserError
39
+ return true
40
+ end
41
+
42
+ existing_cts = contents['contentTypes'].sort_by { |ct| ct.dig('sys', 'id') }
43
+ return true unless content_types.count == existing_cts.count
44
+ return true unless deep_contains_all(content_types, existing_cts)
45
+
46
+ existing_eis = contents['editorInterfaces'].sort_by { |i| i.dig('sys', 'contentType', 'sys', 'id') }
47
+ return true unless editor_interfaces.count == existing_eis.count
48
+
49
+ !deep_contains_all(editor_interfaces, existing_eis)
50
+ end
51
+
52
+ def content_types
53
+ @content_types ||=
54
+ @client.content_types(limit: 1000)
55
+ .items
56
+ .map { |ct| strip_sys(ct) }
57
+ .sort_by { |ct| ct.dig('sys', 'id') }
58
+ end
59
+
60
+ def editor_interfaces
61
+ @editor_interfaces ||=
62
+ content_types
63
+ .map { |ct| @client.editor_interface(ct.dig('sys', 'id')).raw }
64
+ .map { |i| sort_controls(strip_sys(i)) }
65
+ .sort_by { |i| i.dig('sys', 'contentType', 'sys', 'id') }
66
+ end
67
+
68
+ private
69
+
70
+ def strip_sys(obj)
71
+ obj.merge!({
72
+ 'sys' => obj['sys'].slice('id', 'type', 'contentType')
73
+ })
74
+ end
75
+
76
+ def sort_controls(editor_interface)
77
+ {
78
+ 'sys' => editor_interface['sys'],
79
+ 'controls' => editor_interface['controls']
80
+ .sort_by { |c| c['fieldId'] }
81
+ .map { |c| c.slice('fieldId', 'settings', 'widgetId') }
82
+ }
83
+ end
84
+
85
+ def deep_contains_all(expected, actual)
86
+ if expected.is_a? Array
87
+ expected.each_with_index do |val, i|
88
+ return false unless actual[i]
89
+ return false unless deep_contains_all(val, actual[i])
90
+ end
91
+ true
92
+ elsif expected.is_a? Hash
93
+ expected.each do |(key, val)|
94
+ return false unless actual.key?(key)
95
+ return false unless deep_contains_all(val, actual[key])
96
+ end
97
+ true
98
+ else
99
+ expected == actual
100
+ end
101
+ end
102
+
103
+ def format_json(hash)
104
+ json_string = JSON.pretty_generate(hash)
105
+
106
+ # The pretty_generate format differs from contentful-shell and nodejs formats
107
+ # only in its treatment of empty arrays in the "validations" field.
108
+ json_string = json_string.gsub(/\[\n\n\s+\]/, '[]')
109
+ # contentful-shell also adds a newline at the end.
110
+ json_string + "\n"
111
+ end
112
+ end
@@ -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