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
data/Gemfile
DELETED
@@ -1,63 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'active_job'
|
4
|
-
|
5
|
-
module WCC::Contentful
|
6
|
-
# This job uses the Contentful Sync API to update the configured store with
|
7
|
-
# the latest data from Contentful.
|
8
|
-
class DelayedSyncJob < ActiveJob::Base
|
9
|
-
include WCC::Contentful::ServiceAccessors
|
10
|
-
|
11
|
-
self.queue_adapter = :async
|
12
|
-
queue_as :default
|
13
|
-
|
14
|
-
def self.mutex
|
15
|
-
@mutex ||= Mutex.new
|
16
|
-
end
|
17
|
-
|
18
|
-
def perform(event = nil)
|
19
|
-
up_to_id = nil
|
20
|
-
up_to_id = event[:up_to_id] || event.dig('sys', 'id') if event
|
21
|
-
sync!(up_to_id: up_to_id)
|
22
|
-
end
|
23
|
-
|
24
|
-
# Calls the Contentful Sync API and updates the configured store with the returned
|
25
|
-
# data.
|
26
|
-
#
|
27
|
-
# @param [String] up_to_id
|
28
|
-
# An ID that we know has changed and should come back from the sync.
|
29
|
-
# If we don't find this ID in the sync data, then drop a job to try
|
30
|
-
# the sync again after a few minutes.
|
31
|
-
#
|
32
|
-
def sync!(up_to_id: nil)
|
33
|
-
return unless store.respond_to?(:index)
|
34
|
-
|
35
|
-
self.class.mutex.synchronize do
|
36
|
-
next_sync_token = store.find('sync:token')&.fetch('token')
|
37
|
-
sync_resp = client.sync(sync_token: next_sync_token)
|
38
|
-
|
39
|
-
id_found = up_to_id.nil?
|
40
|
-
|
41
|
-
count = 0
|
42
|
-
sync_resp.items.each do |item|
|
43
|
-
id = item.dig('sys', 'id')
|
44
|
-
id_found ||= id == up_to_id
|
45
|
-
store.index(item)
|
46
|
-
count += 1
|
47
|
-
end
|
48
|
-
store.set('sync:token', token: sync_resp.next_sync_token)
|
49
|
-
|
50
|
-
logger.info "Synced #{count} entries. Next sync token:\n #{sync_resp.next_sync_token}"
|
51
|
-
sync_later!(up_to_id: up_to_id) unless id_found
|
52
|
-
sync_resp.next_sync_token
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
# Drops an ActiveJob job to invoke WCC::Contentful.sync! after a given amount
|
57
|
-
# of time.
|
58
|
-
def sync_later!(up_to_id: nil, wait: 10.minutes)
|
59
|
-
WCC::Contentful::DelayedSyncJob.set(wait: wait)
|
60
|
-
.perform_later(up_to_id: up_to_id)
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
@@ -1,28 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
class Contentful::Client
|
4
|
-
class << self
|
5
|
-
alias_method :old_get_http, :get_http
|
6
|
-
end
|
7
|
-
|
8
|
-
def self.adapter
|
9
|
-
@adapter ||=
|
10
|
-
WCC::Contentful::SimpleClient.load_adapter(WCC::Contentful.configuration.http_adapter) ||
|
11
|
-
->(url, query, headers, proxy) { old_get_http(url, query, headers, proxy) }
|
12
|
-
end
|
13
|
-
|
14
|
-
def self.get_http(url, query, headers = {}, proxy = {})
|
15
|
-
if environment = WCC::Contentful.configuration.environment
|
16
|
-
url = rewrite_to_environment(url, environment)
|
17
|
-
end
|
18
|
-
|
19
|
-
adapter.call(url, query, headers, proxy)
|
20
|
-
end
|
21
|
-
|
22
|
-
REWRITE_REGEXP = /^(https?\:\/\/(?:\w+)\.contentful\.com\/spaces\/[^\/]+\/)(?!environments)(.+)$/
|
23
|
-
def self.rewrite_to_environment(url, environment)
|
24
|
-
return url unless m = REWRITE_REGEXP.match(url)
|
25
|
-
|
26
|
-
File.join(m[1], 'environments', environment, m[2])
|
27
|
-
end
|
28
|
-
end
|
@@ -1,14 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
gem 'graphql', '~> 1.7'
|
4
|
-
require 'graphql'
|
5
|
-
|
6
|
-
module WCC::Contentful
|
7
|
-
# This module builds a GraphQL schema out of our IndexedRepresentation.
|
8
|
-
# It is currently unused and not hooked up in the WCC::Contentful.init! method.
|
9
|
-
# TODO: https://zube.io/watermarkchurch/development/c/2234 hook it up
|
10
|
-
module Graphql
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
require_relative 'graphql/builder'
|
@@ -1,177 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'graphql'
|
4
|
-
|
5
|
-
require_relative 'types'
|
6
|
-
|
7
|
-
module WCC::Contentful::Graphql
|
8
|
-
class Builder
|
9
|
-
attr_reader :schema_types
|
10
|
-
|
11
|
-
def initialize(types, store)
|
12
|
-
@types = types
|
13
|
-
@store = store
|
14
|
-
end
|
15
|
-
|
16
|
-
def build_schema
|
17
|
-
@schema_types = build_schema_types
|
18
|
-
|
19
|
-
root_query_type = build_root_query(@schema_types)
|
20
|
-
|
21
|
-
builder = self
|
22
|
-
GraphQL::Schema.define do
|
23
|
-
query root_query_type
|
24
|
-
|
25
|
-
resolve_type ->(_type, obj, _ctx) {
|
26
|
-
content_type = WCC::Contentful::Helpers.content_type_from_raw(obj)
|
27
|
-
builder.schema_types[content_type]
|
28
|
-
}
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
private
|
33
|
-
|
34
|
-
def build_root_query(schema_types)
|
35
|
-
store = @store
|
36
|
-
|
37
|
-
GraphQL::ObjectType.define do
|
38
|
-
name 'Query'
|
39
|
-
description 'The query root of this schema'
|
40
|
-
|
41
|
-
schema_types.each do |content_type, schema_type|
|
42
|
-
field schema_type.name.to_sym do
|
43
|
-
type schema_type
|
44
|
-
argument :id, types.ID
|
45
|
-
description "Find a #{schema_type.name} by ID"
|
46
|
-
|
47
|
-
resolve ->(_obj, args, _ctx) {
|
48
|
-
if args['id'].nil?
|
49
|
-
store.find_by(content_type: content_type)
|
50
|
-
else
|
51
|
-
store.find(args['id'])
|
52
|
-
end
|
53
|
-
}
|
54
|
-
end
|
55
|
-
|
56
|
-
field "all#{schema_type.name}".to_sym do
|
57
|
-
type schema_type.to_list_type
|
58
|
-
argument :filter, Types::FilterType
|
59
|
-
|
60
|
-
resolve ->(_obj, args, ctx) {
|
61
|
-
relation = store.find_all(content_type: content_type)
|
62
|
-
# TODO: improve this POC
|
63
|
-
if args[:filter]
|
64
|
-
filter = {}
|
65
|
-
filter[args[:filter]['field']] = { eq: args[:filter][:eq] }
|
66
|
-
relation = relation.apply(filter, ctx)
|
67
|
-
end
|
68
|
-
relation.result
|
69
|
-
}
|
70
|
-
end
|
71
|
-
end
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
def build_schema_types
|
76
|
-
@types.each_with_object({}) do |(k, v), h|
|
77
|
-
h[k] = build_schema_type(v)
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
def build_schema_type(typedef)
|
82
|
-
store = @store
|
83
|
-
builder = self
|
84
|
-
content_type = typedef.content_type
|
85
|
-
|
86
|
-
GraphQL::ObjectType.define do
|
87
|
-
name(typedef.name)
|
88
|
-
|
89
|
-
description("Generated from content type #{content_type}")
|
90
|
-
|
91
|
-
field :id, !types.ID do
|
92
|
-
resolve ->(obj, _args, _ctx) {
|
93
|
-
obj.dig('sys', 'id')
|
94
|
-
}
|
95
|
-
end
|
96
|
-
|
97
|
-
field :_content_type, !types.String do
|
98
|
-
resolve ->(_, _, _) {
|
99
|
-
content_type
|
100
|
-
}
|
101
|
-
end
|
102
|
-
|
103
|
-
# Make a field for each column:
|
104
|
-
typedef.fields.each_value do |f|
|
105
|
-
case f.type
|
106
|
-
when :Asset
|
107
|
-
field(f.name.to_sym, -> {
|
108
|
-
type = builder.schema_types['Asset']
|
109
|
-
type = type.to_list_type if f.array
|
110
|
-
type
|
111
|
-
}) do
|
112
|
-
resolve ->(obj, _args, ctx) {
|
113
|
-
links = obj.dig('fields', f.name, ctx[:locale] || 'en-US')
|
114
|
-
return if links.nil?
|
115
|
-
|
116
|
-
if links.is_a? Array
|
117
|
-
links.reject(&:nil?).map { |l| store.find(l.dig('sys', 'id')) }
|
118
|
-
else
|
119
|
-
store.find(links.dig('sys', 'id'))
|
120
|
-
end
|
121
|
-
}
|
122
|
-
end
|
123
|
-
when :Link
|
124
|
-
field(f.name.to_sym, -> {
|
125
|
-
type =
|
126
|
-
if f.link_types.nil? || f.link_types.empty?
|
127
|
-
builder.schema_types['AnyContentful'] ||=
|
128
|
-
Types::BuildUnionType.call(builder.schema_types, 'AnyContentful')
|
129
|
-
elsif f.link_types.length == 1
|
130
|
-
builder.schema_types[f.link_types.first]
|
131
|
-
else
|
132
|
-
from_types = builder.schema_types.select { |key| f.link_types.include?(key) }
|
133
|
-
name = "#{typedef.name}_#{f.name}"
|
134
|
-
builder.schema_types[name] ||= Types::BuildUnionType.call(from_types, name)
|
135
|
-
end
|
136
|
-
type = type.to_list_type if f.array
|
137
|
-
type
|
138
|
-
}) do
|
139
|
-
resolve ->(obj, _args, ctx) {
|
140
|
-
links = obj.dig('fields', f.name, ctx[:locale] || 'en-US')
|
141
|
-
return if links.nil?
|
142
|
-
|
143
|
-
if links.is_a? Array
|
144
|
-
links.reject(&:nil?).map { |l| store.find(l.dig('sys', 'id')) }
|
145
|
-
else
|
146
|
-
store.find(links.dig('sys', 'id'))
|
147
|
-
end
|
148
|
-
}
|
149
|
-
end
|
150
|
-
else
|
151
|
-
type =
|
152
|
-
case f.type
|
153
|
-
when :DateTime
|
154
|
-
Types::DateTimeType
|
155
|
-
when :Coordinates
|
156
|
-
Types::CoordinatesType
|
157
|
-
when :Json
|
158
|
-
Types::HashType
|
159
|
-
else
|
160
|
-
types.public_send(f.type)
|
161
|
-
end
|
162
|
-
type = type.to_list_type if f.array
|
163
|
-
field(f.name.to_sym, type) do
|
164
|
-
resolve ->(obj, _args, ctx) {
|
165
|
-
if obj.is_a? Array
|
166
|
-
obj.map { |o| o.dig('fields', f.name, ctx[:locale] || 'en-US') }
|
167
|
-
else
|
168
|
-
obj.dig('fields', f.name, ctx[:locale] || 'en-US')
|
169
|
-
end
|
170
|
-
}
|
171
|
-
end
|
172
|
-
end
|
173
|
-
end
|
174
|
-
end
|
175
|
-
end
|
176
|
-
end
|
177
|
-
end
|
@@ -1,54 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module WCC::Contentful::Graphql::Types
|
4
|
-
DateTimeType =
|
5
|
-
GraphQL::ScalarType.define do
|
6
|
-
name 'DateTime'
|
7
|
-
|
8
|
-
coerce_result ->(value, _ctx) { Time.zone.parse(value) }
|
9
|
-
end
|
10
|
-
|
11
|
-
HashType =
|
12
|
-
GraphQL::ScalarType.define do
|
13
|
-
name 'Hash'
|
14
|
-
|
15
|
-
coerce_result ->(value, _ctx) {
|
16
|
-
return value if value.is_a? Array
|
17
|
-
return value.to_h if value.respond_to?(:to_h)
|
18
|
-
return JSON.parse(value) if value.is_a? String
|
19
|
-
|
20
|
-
raise ArgumentError, "Cannot coerce value '#{value}' to a hash"
|
21
|
-
}
|
22
|
-
end
|
23
|
-
|
24
|
-
CoordinatesType =
|
25
|
-
GraphQL::ObjectType.define do
|
26
|
-
name 'Coordinates'
|
27
|
-
|
28
|
-
field :lat, !types.Float, hash_key: 'lat'
|
29
|
-
field :lon, !types.Float, hash_key: 'lon'
|
30
|
-
end
|
31
|
-
|
32
|
-
AnyScalarInputType =
|
33
|
-
GraphQL::ScalarType.define do
|
34
|
-
name 'Any'
|
35
|
-
end
|
36
|
-
|
37
|
-
FilterType =
|
38
|
-
GraphQL::InputObjectType.define do
|
39
|
-
name 'filter'
|
40
|
-
|
41
|
-
argument :field, !types.String
|
42
|
-
argument :eq, AnyScalarInputType
|
43
|
-
end
|
44
|
-
|
45
|
-
BuildUnionType =
|
46
|
-
->(from_types, union_type_name) do
|
47
|
-
possible_types = from_types.values.reject { |t| t.is_a? GraphQL::UnionType }
|
48
|
-
|
49
|
-
GraphQL::UnionType.define do
|
50
|
-
name union_type_name
|
51
|
-
possible_types possible_types
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
@@ -1,24 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
gem 'http'
|
4
|
-
require 'http'
|
5
|
-
|
6
|
-
class HttpAdapter
|
7
|
-
def call(url, query, headers = {}, proxy = {})
|
8
|
-
if proxy[:host]
|
9
|
-
HTTP[headers].via(proxy[:host], proxy[:port], proxy[:username], proxy[:password])
|
10
|
-
.get(url, params: query)
|
11
|
-
else
|
12
|
-
HTTP[headers].get(url, params: query)
|
13
|
-
end
|
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
|
24
|
-
end
|
@@ -1,161 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module WCC::Contentful::Store
|
4
|
-
class LazyCacheStore
|
5
|
-
def initialize(client, cache: nil)
|
6
|
-
@cdn = CDNAdapter.new(client)
|
7
|
-
@cache = cache || ActiveSupport::Cache::MemoryStore.new
|
8
|
-
@client = client
|
9
|
-
end
|
10
|
-
|
11
|
-
def find(key, **options)
|
12
|
-
found =
|
13
|
-
@cache.fetch(key) do
|
14
|
-
# if it's not a contentful ID don't hit the API.
|
15
|
-
# Store a nil object if we can't find the object on the CDN.
|
16
|
-
(@cdn.find(key, options) || nil_obj(key)) if key =~ /^\w+$/
|
17
|
-
end
|
18
|
-
|
19
|
-
case found.try(:dig, 'sys', 'type')
|
20
|
-
when 'Nil', 'DeletedEntry', 'DeletedAsset'
|
21
|
-
nil
|
22
|
-
else
|
23
|
-
found
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
# TODO: https://github.com/watermarkchurch/wcc-contentful/issues/18
|
28
|
-
# figure out how to cache the results of a find_by query, ex:
|
29
|
-
# `find_by('slug' => '/about')`
|
30
|
-
def find_by(content_type:, filter: nil, options: nil)
|
31
|
-
if filter.keys == ['sys.id']
|
32
|
-
# Direct ID lookup, like what we do in `WCC::Contentful::ModelMethods.resolve`
|
33
|
-
# We can return just this item. Stores are not required to implement :include option.
|
34
|
-
if found = @cache.read(filter['sys.id'])
|
35
|
-
return found
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
q = find_all(content_type: content_type, options: { limit: 1 }.merge!(options || {}))
|
40
|
-
q = q.apply(filter) if filter
|
41
|
-
q.first
|
42
|
-
end
|
43
|
-
|
44
|
-
def find_all(content_type:, options: nil)
|
45
|
-
Query.new(
|
46
|
-
store: self,
|
47
|
-
client: @client,
|
48
|
-
relation: { content_type: content_type },
|
49
|
-
cache: @cache,
|
50
|
-
options: options
|
51
|
-
)
|
52
|
-
end
|
53
|
-
|
54
|
-
# #index is called whenever the sync API comes back with more data.
|
55
|
-
def index(json)
|
56
|
-
id = json.dig('sys', 'id')
|
57
|
-
return unless prev = @cache.read(id)
|
58
|
-
|
59
|
-
if (prev_rev = prev&.dig('sys', 'revision')) && (next_rev = json.dig('sys', 'revision'))
|
60
|
-
return prev if next_rev < prev_rev
|
61
|
-
end
|
62
|
-
|
63
|
-
# we also set deletes in the cache - no need to go hit the API when we know
|
64
|
-
# this is a nil object
|
65
|
-
ensure_hash json
|
66
|
-
@cache.write(id, json)
|
67
|
-
|
68
|
-
case json.dig('sys', 'type')
|
69
|
-
when 'DeletedEntry', 'DeletedAsset'
|
70
|
-
nil
|
71
|
-
else
|
72
|
-
json
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
def set(key, value)
|
77
|
-
ensure_hash value
|
78
|
-
old = @cache.read(key)
|
79
|
-
@cache.write(key, value)
|
80
|
-
old
|
81
|
-
end
|
82
|
-
|
83
|
-
def delete(key)
|
84
|
-
old = @cache.read(key)
|
85
|
-
@cache.delete(key)
|
86
|
-
old
|
87
|
-
end
|
88
|
-
|
89
|
-
def nil_obj(id)
|
90
|
-
{
|
91
|
-
'sys' => {
|
92
|
-
'id' => id,
|
93
|
-
'type' => 'Nil',
|
94
|
-
'revision' => 1
|
95
|
-
}
|
96
|
-
}
|
97
|
-
end
|
98
|
-
|
99
|
-
def ensure_hash(val)
|
100
|
-
raise ArgumentError, 'Value must be a Hash' unless val.is_a?(Hash)
|
101
|
-
end
|
102
|
-
|
103
|
-
class Query < CDNAdapter::Query
|
104
|
-
def initialize(cache:, **extra)
|
105
|
-
super(cache: cache, **extra)
|
106
|
-
@cache = cache
|
107
|
-
end
|
108
|
-
|
109
|
-
private
|
110
|
-
|
111
|
-
def response
|
112
|
-
# Disabling because the superclass already took `@response`
|
113
|
-
# rubocop:disable Naming/MemoizedInstanceVariableName
|
114
|
-
@wrapped_response ||= ResponseWrapper.new(super, @cache)
|
115
|
-
# rubocop:enable Naming/MemoizedInstanceVariableName
|
116
|
-
end
|
117
|
-
|
118
|
-
ResponseWrapper =
|
119
|
-
Struct.new(:response, :cache) do
|
120
|
-
delegate :count, to: :response
|
121
|
-
|
122
|
-
def items
|
123
|
-
@items ||=
|
124
|
-
response.items.map do |item|
|
125
|
-
id = item.dig('sys', 'id')
|
126
|
-
prev = cache.read(id)
|
127
|
-
unless (prev_rev = prev&.dig('sys', 'revision')) &&
|
128
|
-
(next_rev = item.dig('sys', 'revision')) &&
|
129
|
-
next_rev < prev_rev
|
130
|
-
|
131
|
-
cache.write(id, item)
|
132
|
-
end
|
133
|
-
|
134
|
-
item
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
def includes
|
139
|
-
@includes ||= IncludesWrapper.new(response, cache)
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
IncludesWrapper =
|
144
|
-
Struct.new(:response, :cache) do
|
145
|
-
def [](id)
|
146
|
-
return unless item = response.includes[id]
|
147
|
-
|
148
|
-
prev = cache.read(id)
|
149
|
-
unless (prev_rev = prev&.dig('sys', 'revision')) &&
|
150
|
-
(next_rev = item.dig('sys', 'revision')) &&
|
151
|
-
next_rev < prev_rev
|
152
|
-
|
153
|
-
cache.write(id, item)
|
154
|
-
end
|
155
|
-
|
156
|
-
item
|
157
|
-
end
|
158
|
-
end
|
159
|
-
end
|
160
|
-
end
|
161
|
-
end
|