wcc-contentful 0.3.0 → 1.0.0.pre.rc2

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 (99) hide show
  1. checksums.yaml +5 -5
  2. data/.rspec +1 -1
  3. data/Guardfile +43 -0
  4. data/README.md +161 -11
  5. data/Rakefile +3 -6
  6. data/app/controllers/wcc/contentful/webhook_controller.rb +25 -24
  7. data/app/jobs/wcc/contentful/webhook_enable_job.rb +36 -2
  8. data/bin/console +4 -3
  9. data/bin/rails +2 -0
  10. data/config/routes.rb +1 -1
  11. data/doc +1 -0
  12. data/lib/tasks/download_schema.rake +12 -0
  13. data/lib/wcc/contentful.rb +69 -45
  14. data/lib/wcc/contentful/active_record_shim.rb +72 -0
  15. data/lib/wcc/contentful/configuration.rb +177 -46
  16. data/lib/wcc/contentful/content_type_indexer.rb +14 -0
  17. data/lib/wcc/contentful/downloads_schema.rb +112 -0
  18. data/lib/wcc/contentful/engine.rb +33 -14
  19. data/lib/wcc/contentful/event.rb +171 -0
  20. data/lib/wcc/contentful/events.rb +41 -0
  21. data/lib/wcc/contentful/exceptions.rb +3 -33
  22. data/lib/wcc/contentful/indexed_representation.rb +2 -2
  23. data/lib/wcc/contentful/instrumentation.rb +31 -0
  24. data/lib/wcc/contentful/link.rb +28 -0
  25. data/lib/wcc/contentful/link_visitor.rb +122 -0
  26. data/lib/wcc/contentful/middleware.rb +7 -0
  27. data/lib/wcc/contentful/middleware/store.rb +158 -0
  28. data/lib/wcc/contentful/middleware/store/caching_middleware.rb +114 -0
  29. data/lib/wcc/contentful/model.rb +37 -4
  30. data/lib/wcc/contentful/model_builder.rb +1 -0
  31. data/lib/wcc/contentful/model_methods.rb +40 -15
  32. data/lib/wcc/contentful/model_singleton_methods.rb +47 -30
  33. data/lib/wcc/contentful/rake.rb +4 -0
  34. data/lib/wcc/contentful/rspec.rb +46 -0
  35. data/lib/wcc/contentful/services.rb +61 -27
  36. data/lib/wcc/contentful/simple_client.rb +81 -25
  37. data/lib/wcc/contentful/simple_client/management.rb +43 -10
  38. data/lib/wcc/contentful/simple_client/response.rb +61 -22
  39. data/lib/wcc/contentful/simple_client/typhoeus_adapter.rb +17 -17
  40. data/lib/wcc/contentful/store.rb +7 -66
  41. data/lib/wcc/contentful/store/README.md +85 -0
  42. data/lib/wcc/contentful/store/base.rb +34 -119
  43. data/lib/wcc/contentful/store/cdn_adapter.rb +71 -12
  44. data/lib/wcc/contentful/store/factory.rb +186 -0
  45. data/lib/wcc/contentful/store/instrumentation.rb +55 -0
  46. data/lib/wcc/contentful/store/interface.rb +82 -0
  47. data/lib/wcc/contentful/store/memory_store.rb +27 -24
  48. data/lib/wcc/contentful/store/postgres_store.rb +268 -101
  49. data/lib/wcc/contentful/store/postgres_store/schema_1.sql +73 -0
  50. data/lib/wcc/contentful/store/postgres_store/schema_2.sql +21 -0
  51. data/lib/wcc/contentful/store/query.rb +246 -0
  52. data/lib/wcc/contentful/store/query/interface.rb +63 -0
  53. data/lib/wcc/contentful/store/rspec_examples.rb +48 -0
  54. data/lib/wcc/contentful/store/rspec_examples/basic_store.rb +629 -0
  55. data/lib/wcc/contentful/store/rspec_examples/include_param.rb +283 -0
  56. data/lib/wcc/contentful/store/rspec_examples/nested_queries.rb +342 -0
  57. data/lib/wcc/contentful/sync_engine.rb +181 -0
  58. data/lib/wcc/contentful/test.rb +7 -0
  59. data/lib/wcc/contentful/test/attributes.rb +56 -0
  60. data/lib/wcc/contentful/test/double.rb +76 -0
  61. data/lib/wcc/contentful/test/factory.rb +101 -0
  62. data/lib/wcc/contentful/version.rb +1 -1
  63. data/wcc-contentful.gemspec +28 -14
  64. metadata +248 -152
  65. data/.circleci/config.yml +0 -51
  66. data/.gitignore +0 -26
  67. data/.rubocop.yml +0 -242
  68. data/.rubocop_todo.yml +0 -19
  69. data/.travis.yml +0 -5
  70. data/CHANGELOG.md +0 -180
  71. data/CODE_OF_CONDUCT.md +0 -74
  72. data/Gemfile +0 -8
  73. data/LICENSE.txt +0 -21
  74. data/app/jobs/wcc/contentful/delayed_sync_job.rb +0 -63
  75. data/lib/generators/wcc/USAGE +0 -24
  76. data/lib/generators/wcc/model_generator.rb +0 -90
  77. data/lib/generators/wcc/templates/.keep +0 -0
  78. data/lib/generators/wcc/templates/Procfile +0 -3
  79. data/lib/generators/wcc/templates/contentful_shell_wrapper +0 -385
  80. data/lib/generators/wcc/templates/menu/generated_add_menus.ts +0 -192
  81. data/lib/generators/wcc/templates/menu/models/menu.rb +0 -23
  82. data/lib/generators/wcc/templates/menu/models/menu_button.rb +0 -23
  83. data/lib/generators/wcc/templates/page/generated_add_pages.ts +0 -50
  84. data/lib/generators/wcc/templates/page/models/page.rb +0 -23
  85. data/lib/generators/wcc/templates/release +0 -9
  86. data/lib/generators/wcc/templates/wcc_contentful.rb +0 -17
  87. data/lib/wcc/contentful/client_ext.rb +0 -28
  88. data/lib/wcc/contentful/graphql.rb +0 -14
  89. data/lib/wcc/contentful/graphql/builder.rb +0 -177
  90. data/lib/wcc/contentful/graphql/types.rb +0 -54
  91. data/lib/wcc/contentful/model/dropdown_menu.rb +0 -7
  92. data/lib/wcc/contentful/model/menu.rb +0 -6
  93. data/lib/wcc/contentful/model/menu_button.rb +0 -16
  94. data/lib/wcc/contentful/model/page.rb +0 -8
  95. data/lib/wcc/contentful/model/redirect.rb +0 -19
  96. data/lib/wcc/contentful/model_validators.rb +0 -121
  97. data/lib/wcc/contentful/model_validators/dsl.rb +0 -166
  98. data/lib/wcc/contentful/simple_client/http_adapter.rb +0 -24
  99. data/lib/wcc/contentful/store/lazy_cache_store.rb +0 -161
@@ -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,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class WCC::Contentful::Model::DropdownMenu < WCC::Contentful::Model
4
- validate_field :name, :String
5
- validate_field :label, :Link, link_to: 'menuButton'
6
- validate_field :items, :Array, link_to: 'menuButton'
7
- end
@@ -1,6 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class WCC::Contentful::Model::Menu < WCC::Contentful::Model
4
- validate_field :name, :String
5
- validate_field :items, :Array, link_to: %w[dropdownMenu menuButton]
6
- end
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class WCC::Contentful::Model::MenuButton < WCC::Contentful::Model
4
- validate_field :text, :String, :required
5
- validate_field :icon, :Asset, :optional
6
- validate_field :external_link, :String, :optional
7
- validate_field :link, :Link, :optional, link_to: 'page'
8
-
9
- # Gets either the external link or the slug from the referenced page.
10
- # Example usage: `<%= link_to button.title, button.href %>`
11
- def href
12
- return external_link if external_link
13
-
14
- link&.try(:slug) || link&.try(:url)
15
- end
16
- end
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class WCC::Contentful::Model::Page < WCC::Contentful::Model
4
- validate_field :title, :String
5
- validate_field :slug, :String
6
- validate_field :subpages, :Array, link_to: %w[page]
7
- validate_field :sections, :Array, link_to: /^section/
8
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class WCC::Contentful::Model::Redirect < WCC::Contentful::Model
4
- def href
5
- if !url.nil?
6
- url
7
- elsif valid_page_reference?(pageReference)
8
- "/#{pageReference.url}"
9
- end
10
- end
11
-
12
- def valid_page_reference?(page_ref)
13
- if !page_ref.nil? && !defined?(page_ref.url).nil?
14
- true
15
- else
16
- false
17
- end
18
- end
19
- end
@@ -1,121 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'dry-validation'
4
-
5
- require_relative 'model_validators/dsl'
6
-
7
- module WCC::Contentful::ModelValidators
8
- def schema
9
- return if validations.nil? || validations.empty?
10
-
11
- all_field_validations =
12
- validations.each_with_object({}) do |(content_type, procs), h|
13
- next if procs.empty?
14
-
15
- # "page": {
16
- # "sys": { ... }
17
- # "fields": {
18
- # "title": { ... },
19
- # "sections": { ... },
20
- # ...
21
- # }
22
- # }
23
- h[content_type] =
24
- Dry::Validation.Schema do
25
- # Had to dig through the internals of Dry::Validation to find
26
- # this magic incantation
27
- procs.each { |dsl| instance_eval(&dsl.to_proc) }
28
- end
29
- end
30
-
31
- Dry::Validation.Schema do
32
- all_field_validations.each do |content_type, fields_schema|
33
- required(content_type).schema do
34
- required('fields').schema(fields_schema)
35
- end
36
- end
37
- end
38
- end
39
-
40
- def validations
41
- # This needs to be a class variable so that subclasses defined in application
42
- # code can add to the total package of model validations
43
- # rubocop:disable Style/ClassVars
44
- @@validations ||= {}
45
- # rubocop:enable Style/ClassVars
46
- end
47
-
48
- # Accepts a block which uses the {dry-validation DSL}[http://dry-rb.org/gems/dry-validation/]
49
- # to validate the 'fields' object of a content type.
50
- def validate_fields(&block)
51
- raise ArgumentError, 'validate_fields requires a block' unless block_given?
52
-
53
- dsl = ProcDsl.new(Proc.new(&block))
54
-
55
- ct = try(:content_type) || name.demodulize.camelize(:lower)
56
- (validations[ct] ||= []) << dsl
57
- end
58
-
59
- # Validates a single field is of the expected type.
60
- # Type expectations are one of:
61
- #
62
- # [:String] the field type must be `Symbol` or `Text`
63
- # [:Int] the field type must be `Integer`
64
- # [:Float] the field type must be `Number`
65
- # [:DateTime] the field type must be 'Date'
66
- # [:Asset] the field must be a link and the `linkType` must be `Asset`
67
- # [:Link] the field must be a link and the `linkType` must be `Entry`.
68
- # [:Location] the field type must be `Location`
69
- # [:Boolean] the field type must be `Boolean`
70
- # [:Json] the field type must be `Json` - a json blob.
71
- # [:Array] the field must be a List.
72
- #
73
- # Additional validation options can be enforced:
74
- #
75
- # [:required] the 'Required Field' checkbox must be checked
76
- # [:optional] the 'Required Field' checkbox must not be checked
77
- # [:link_to] (only `:Link` or `:Array` type) the given content type(s) must be
78
- # checked in the 'Accept only specified entry type' validations
79
- # Example:
80
- # validate_field :button, :Link, link_to: ['button', 'altButton']
81
- #
82
- # [:items] (only `:Array` type) the items of the list must be of the given type.
83
- # Example:
84
- # validate_field :my_strings, :Array, items: :String
85
- #
86
- # Examples:
87
- # see WCC::Contentful::Model::Menu and WCC::Contentful::Model::MenuButton
88
- def validate_field(field, type, *options)
89
- dsl = FieldDsl.new(field, type, options)
90
-
91
- ct = try(:content_type) || name.demodulize.camelize(:lower)
92
- (validations[ct] ||= []) << dsl
93
- end
94
-
95
- def no_validate_field(field)
96
- ct = try(:content_type) || name.demodulize.camelize(:lower)
97
- return unless v = validations[ct]
98
-
99
- field = field.to_s.camelize(:lower) unless field.is_a?(String)
100
- v.reject! { |dsl| dsl.try(:field) == field }
101
- end
102
-
103
- # Accepts a content types response from the API and transforms it
104
- # to be acceptible for the validator.
105
- def self.transform_content_types_for_validation(content_types)
106
- if !content_types.is_a?(Array) && items = content_types.try(:[], 'items')
107
- content_types = items
108
- end
109
-
110
- # Transform the array into a hash keyed by content type ID
111
- content_types.each_with_object({}) do |ct, ct_hash|
112
- # Transform the fields into a hash keyed by field ID
113
- ct['fields'] =
114
- ct['fields'].each_with_object({}) do |f, f_hash|
115
- f_hash[f['id']] = f
116
- end
117
-
118
- ct_hash[ct.dig('sys', 'id')] = ct
119
- end
120
- end
121
- end
@@ -1,166 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module WCC::Contentful::ModelValidators
4
- class ProcDsl
5
- def to_proc
6
- @proc
7
- end
8
-
9
- def initialize(proc)
10
- @proc = proc
11
- end
12
- end
13
-
14
- class FieldDsl
15
- attr_reader :field
16
-
17
- # "sections": {
18
- # "id": "sections",
19
- # "name": "Sections",
20
- # "type": "Array",
21
- # "localized": false,
22
- # "required": false,
23
- # "validations": [],
24
- # "disabled": false,
25
- # "omitted": false,
26
- # "items": {
27
- # "type": "Link",
28
- # "validations": [
29
- # {
30
- # "linkContentType": [
31
- # "Section"
32
- # ]
33
- # }
34
- # ],
35
- # "linkType": "Entry"
36
- # }
37
- # }
38
-
39
- def schema
40
- return @field_schema if @field_schema
41
-
42
- # example: required('type').value(...)
43
- type_pred = parse_type_predicate(@type)
44
-
45
- # example: [required('required').value(eq?: true), ...]
46
- procs =
47
- @options.map do |opt|
48
- if opt.is_a?(Hash)
49
- opt.map { |k, v| parse_option(k, v) }
50
- else
51
- parse_option(opt)
52
- end
53
- end
54
-
55
- @field_schema =
56
- Dry::Validation.Schema do
57
- instance_eval(&type_pred)
58
-
59
- procs.flatten.each { |p| instance_eval(&p) }
60
- end
61
- end
62
-
63
- def to_proc
64
- f = field
65
- s = schema
66
- proc { required(f).schema(s) }
67
- end
68
-
69
- def initialize(field, field_type, options)
70
- @field = field.to_s.camelize(:lower) unless field.is_a?(String)
71
- @type = field_type
72
- @options = options
73
- end
74
-
75
- private
76
-
77
- def parse_type_predicate(type)
78
- case type
79
- when :String
80
- proc { required('type').value(included_in?: %w[Symbol Text]) }
81
- when :Int
82
- proc { required('type').value(eql?: 'Integer') }
83
- when :Float
84
- proc { required('type').value(eql?: 'Number') }
85
- when :DateTime
86
- proc { required('type').value(eql?: 'Date') }
87
- when :Asset
88
- proc {
89
- required('type').value(eql?: 'Link')
90
- required('linkType').value(eql?: 'Asset')
91
- }
92
- else
93
- proc { required('type').value(eql?: type.to_s.camelize) }
94
- end
95
- end
96
-
97
- def parse_option(option, option_arg = nil)
98
- case option
99
- when :required
100
- proc { required('required').value(eql?: true) }
101
- when :optional
102
- proc { required('required').value(eql?: false) }
103
- when :link_to
104
- link_to_proc = parse_field_link_to(option_arg)
105
- return link_to_proc unless @type.to_s.camelize == 'Array'
106
-
107
- proc {
108
- required('items').schema do
109
- required('type').value(eql?: 'Link')
110
- instance_eval(&link_to_proc)
111
- end
112
- }
113
- when :items
114
- type_pred = parse_type_predicate(option_arg)
115
- proc {
116
- required('items').schema do
117
- instance_eval(&type_pred)
118
- end
119
- }
120
- else
121
- raise ArgumentError, "unknown validation requirement: #{option}"
122
- end
123
- end
124
-
125
- def parse_field_link_to(option_arg)
126
- raise ArgumentError, 'validation link_to: requires an argument' unless option_arg
127
-
128
- # this works because a Link can only have one validation in its "validations" array -
129
- # this will fail if Contentful ever changes that.
130
-
131
- # the 'validations' schema needs to be optional because if we get the content
132
- # types from the CDN instead of the management API, sometimes the validations
133
- # don't get sent back.
134
-
135
- # "validations": [
136
- # {
137
- # "linkContentType": [
138
- # "section-CardSearch",
139
- # "section-Faq",
140
- # "section-Testimonials",
141
- # "section-VideoHighlight"
142
- # ]
143
- # }
144
- # ]
145
-
146
- if option_arg.is_a?(Regexp)
147
- return proc {
148
- optional('validations').each do
149
- schema do
150
- required('linkContentType').each(format?: option_arg)
151
- end
152
- end
153
- }
154
- end
155
-
156
- option_arg = [option_arg] unless option_arg.is_a?(Array)
157
- proc {
158
- optional('validations').each do
159
- schema do
160
- required('linkContentType').value(eql?: option_arg)
161
- end
162
- end
163
- }
164
- end
165
- end
166
- 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