wcc-contentful 0.4.0.pre.rc → 1.0.0.pre.rc1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|