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.
- checksums.yaml +5 -5
- data/Guardfile +43 -0
- data/README.md +101 -12
- 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/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 +3 -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 +14 -11
- metadata +201 -146
- 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
@@ -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
|
@@ -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
|