wcc-contentful 0.4.0.pre.beta → 1.0.0
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.
- checksums.yaml +5 -5
- data/Guardfile +43 -0
- data/README.md +205 -17
- data/Rakefile +5 -0
- data/app/controllers/wcc/contentful/webhook_controller.rb +25 -24
- data/app/jobs/wcc/contentful/webhook_enable_job.rb +36 -2
- data/config/routes.rb +1 -1
- data/doc-static/wcc-contentful.png +0 -0
- data/lib/tasks/download_schema.rake +12 -0
- data/lib/wcc/contentful.rb +70 -16
- data/lib/wcc/contentful/active_record_shim.rb +72 -0
- data/lib/wcc/contentful/configuration.rb +177 -46
- data/lib/wcc/contentful/content_type_indexer.rb +14 -0
- data/lib/wcc/contentful/downloads_schema.rb +112 -0
- data/lib/wcc/contentful/engine.rb +33 -14
- data/lib/wcc/contentful/event.rb +171 -0
- data/lib/wcc/contentful/events.rb +41 -0
- data/lib/wcc/contentful/exceptions.rb +3 -0
- data/lib/wcc/contentful/indexed_representation.rb +2 -2
- data/lib/wcc/contentful/instrumentation.rb +31 -0
- data/lib/wcc/contentful/link.rb +28 -0
- data/lib/wcc/contentful/link_visitor.rb +122 -0
- data/lib/wcc/contentful/middleware.rb +7 -0
- data/lib/wcc/contentful/middleware/store.rb +158 -0
- data/lib/wcc/contentful/middleware/store/caching_middleware.rb +114 -0
- data/lib/wcc/contentful/model.rb +37 -3
- data/lib/wcc/contentful/model_builder.rb +1 -0
- data/lib/wcc/contentful/model_methods.rb +40 -15
- data/lib/wcc/contentful/model_singleton_methods.rb +47 -30
- data/lib/wcc/contentful/rake.rb +4 -0
- data/lib/wcc/contentful/rspec.rb +13 -8
- data/lib/wcc/contentful/services.rb +61 -27
- data/lib/wcc/contentful/simple_client.rb +81 -25
- data/lib/wcc/contentful/simple_client/management.rb +43 -10
- data/lib/wcc/contentful/simple_client/response.rb +61 -22
- data/lib/wcc/contentful/simple_client/typhoeus_adapter.rb +17 -17
- data/lib/wcc/contentful/store.rb +7 -66
- data/lib/wcc/contentful/store/README.md +85 -0
- data/lib/wcc/contentful/store/base.rb +34 -119
- data/lib/wcc/contentful/store/cdn_adapter.rb +71 -12
- data/lib/wcc/contentful/store/factory.rb +186 -0
- data/lib/wcc/contentful/store/instrumentation.rb +55 -0
- data/lib/wcc/contentful/store/interface.rb +82 -0
- data/lib/wcc/contentful/store/memory_store.rb +27 -24
- data/lib/wcc/contentful/store/postgres_store.rb +253 -107
- data/lib/wcc/contentful/store/postgres_store/schema_1.sql +73 -0
- data/lib/wcc/contentful/store/postgres_store/schema_2.sql +21 -0
- data/lib/wcc/contentful/store/query.rb +246 -0
- data/lib/wcc/contentful/store/query/interface.rb +63 -0
- data/lib/wcc/contentful/store/rspec_examples.rb +48 -0
- data/lib/wcc/contentful/store/rspec_examples/basic_store.rb +629 -0
- data/lib/wcc/contentful/store/rspec_examples/include_param.rb +283 -0
- data/lib/wcc/contentful/store/rspec_examples/nested_queries.rb +342 -0
- data/lib/wcc/contentful/sync_engine.rb +181 -0
- data/lib/wcc/contentful/test/attributes.rb +17 -5
- data/lib/wcc/contentful/test/factory.rb +22 -46
- data/lib/wcc/contentful/version.rb +1 -1
- data/wcc-contentful.gemspec +22 -11
- metadata +295 -144
- data/Gemfile +0 -6
- data/app/jobs/wcc/contentful/delayed_sync_job.rb +0 -63
- data/lib/wcc/contentful/client_ext.rb +0 -28
- data/lib/wcc/contentful/graphql.rb +0 -14
- data/lib/wcc/contentful/graphql/builder.rb +0 -177
- data/lib/wcc/contentful/graphql/types.rb +0 -54
- data/lib/wcc/contentful/simple_client/http_adapter.rb +0 -24
- 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
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
@@ -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
|