wcc-contentful 1.3.2 → 1.4.0.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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +80 -14
  3. data/app/jobs/wcc/contentful/webhook_enable_job.rb +0 -1
  4. data/lib/wcc/contentful/configuration.rb +12 -5
  5. data/lib/wcc/contentful/downloads_schema.rb +14 -2
  6. data/lib/wcc/contentful/entry_locale_transformer.rb +107 -0
  7. data/lib/wcc/contentful/exceptions.rb +5 -0
  8. data/lib/wcc/contentful/link_visitor.rb +12 -1
  9. data/lib/wcc/contentful/middleware/store/caching_middleware.rb +34 -10
  10. data/lib/wcc/contentful/middleware/store/locale_middleware.rb +30 -0
  11. data/lib/wcc/contentful/middleware/store.rb +20 -16
  12. data/lib/wcc/contentful/model_builder.rb +10 -3
  13. data/lib/wcc/contentful/model_methods.rb +4 -6
  14. data/lib/wcc/contentful/simple_client/cdn.rb +5 -2
  15. data/lib/wcc/contentful/simple_client/management.rb +16 -0
  16. data/lib/wcc/contentful/simple_client.rb +1 -2
  17. data/lib/wcc/contentful/store/base.rb +6 -1
  18. data/lib/wcc/contentful/store/cdn_adapter.rb +13 -4
  19. data/lib/wcc/contentful/store/factory.rb +8 -1
  20. data/lib/wcc/contentful/store/memory_store.rb +27 -8
  21. data/lib/wcc/contentful/store/postgres_store.rb +4 -3
  22. data/lib/wcc/contentful/store/query/condition.rb +89 -0
  23. data/lib/wcc/contentful/store/query.rb +9 -35
  24. data/lib/wcc/contentful/store/rspec_examples/basic_store.rb +84 -12
  25. data/lib/wcc/contentful/store/rspec_examples/locale_queries.rb +220 -0
  26. data/lib/wcc/contentful/store/rspec_examples/operators/eq.rb +1 -1
  27. data/lib/wcc/contentful/store/rspec_examples.rb +13 -1
  28. data/lib/wcc/contentful/sync_engine.rb +1 -1
  29. data/lib/wcc/contentful/test/double.rb +1 -1
  30. data/lib/wcc/contentful/test/factory.rb +2 -4
  31. data/lib/wcc/contentful/version.rb +1 -1
  32. data/lib/wcc/contentful.rb +17 -6
  33. metadata +80 -48
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3cbb012e1545d674abc930f6513ea342611beb586b2afe5ad5ce703903e5a9cb
4
- data.tar.gz: da23bc050241c0ae55e1952fc48d892d2b2d768f33d314aef34f4a5f159db585
3
+ metadata.gz: 0b1a0aa21bc1bc749fdfc169eecf55d5bf829e0d4ac4272e90649c2d46abf622
4
+ data.tar.gz: 12812d6728c4992ea3763cdbe0dc8f204267a75c3f35a052a663ec8c4f380115
5
5
  SHA512:
6
- metadata.gz: e53a8c32ea3687f8e5e8bfaac3a4a15d940e21873c7068c20b26cca92b5ed2876a8f680949f2bf169ebae258a00c0665f63c314888a1c3379b69b37da2be97d5
7
- data.tar.gz: 24e358dc0f8ec03873ffb27ff6d79c5a64f0fda24b5f66336482ca38f0b4b654227907df96955005fcc4cf04ed811e78070c3688f3c3d0629406666734c8fd93
6
+ metadata.gz: 9226778b6abf97db358f836ed0659e202f13266fbf946acd793cbeed23f69cb3cab475e9750ca64761ba6ceecd38cdaaad7f919ea2a2d04f00fa4d8e940d9653
7
+ data.tar.gz: c4f0bb1b6e44fa8b0a4aa647e246ff7dd6726d62e2905d026f2bb201867fb09af640b266ab95c22e04dab6b23f4c162ec6d88cf3bd45ca41c9a5853eb4842d80
data/README.md CHANGED
@@ -32,7 +32,7 @@ Table of Contents:
32
32
 
33
33
  ## Why did you rewrite the Contentful ruby stack?
34
34
 
35
- We started working with Contentful almost 3 years ago. Since that time, Contentful's ruby stack has improved, but there are still a number of pain points that we feel we have addressed better with our gem. These are:
35
+ We started working with Contentful almost 5 years ago. Since that time, Contentful's ruby stack has improved, but there are still a number of pain points that we feel we have addressed better with our gem. These are:
36
36
 
37
37
  * [Low-level caching](#low-level-caching)
38
38
  * [Better integration with Rails & Rails models](#better-rails-integration)
@@ -160,22 +160,34 @@ The following examples show how to use this API to find entries of the `page`
160
160
  content type:
161
161
 
162
162
  ```ruby
163
+ # app/models/page.rb
164
+ class Page < WCC::Contentful::Model::Page
165
+
166
+ # You can add additional methods here
167
+ end
168
+
163
169
  # Find objects by id
164
- WCC::Contentful::Model::Page.find('1E2ucWSdacxxf233sfa3')
165
- # => #<WCC::Contentful::Model::Page:0x0000000005c71a78 @created_at=2018-04-16 18:41:17 UTC...>
170
+ Page.find('1E2ucWSdacxxf233sfa3')
171
+ # => #<Page:0x0000000005c71a78 @created_at=2018-04-16 18:41:17 UTC...>
166
172
 
167
173
  # Find objects by field
168
- WCC::Contentful::Model::Page.find_by(slug: '/some-slug')
169
- # => #<WCC::Contentful::Model::Page:0x0000000005c71a78 @created_at=2018-04-16 18:41:17 UTC...>
174
+ Page.find_by(slug: '/some-slug')
175
+ # => #<Page:0x0000000005c71a78 @created_at=2018-04-16 18:41:17 UTC...>
170
176
 
171
177
  # Use operators to filter by a field
172
178
  # must use full notation for sys attributes (except ID)
173
- WCC::Contentful::Model::Page.find_all('sys.created_at' => { lte: Date.today })
174
- # => [#<WCC::Contentful::Model::Page:0x0000000005c71a78 @created_at=2018-04-16 18:41:17 UTC...>, ... ]
179
+ Page.find_all('sys.created_at' => { lte: Date.today })
180
+ # => [#<Page:0x0000000005c71a78 @created_at=2018-04-16 18:41:17 UTC...>, ... ]
175
181
 
176
182
  # Nest queries to mimick joins
177
- WCC::Contentful::Model::Page.find_by(subpages: { slug: '/some-slug' })
178
- # => #<WCC::Contentful::Model::Page:0x0000000005c71a78 @created_at=2018-04-16 18:41:17 UTC...>
183
+ Page.find_by(subpages: { slug: '/some-slug' })
184
+ # => #<Page:0x0000000005c71a78 @created_at=2018-04-16 18:41:17 UTC...>
185
+
186
+ # Fetch an entry in a different locale
187
+ spanish_homepage = Page.find_by(slug: '/', options: { locale: 'es-US' })
188
+ # => #<Page:0x0000000005c71a78 @created_at=2018-04-16 18:41:17 UTC...>
189
+ spanish_homepage.title
190
+ # => Esta es la página principal
179
191
 
180
192
  # Pass the preview flag to use the preview client (must have set preview_token config param)
181
193
  preview_redirect = WCC::Contentful::Model::Redirect.find_by({ slug: 'draft-redirect' }, preview: true)
@@ -224,6 +236,18 @@ query.result.force
224
236
  # => [{"sys"=> ...}, {"sys"=> ...}, ...]
225
237
  ```
226
238
 
239
+ The store layer, while superficially similar to the Contentful API, tries to present a different "View" over the data
240
+ which is more compatible with the Model layer. It resolves includes by actually replacing the in-memory `Link` objects
241
+ with their linked `Entry` representations. This lets you traverse the links naturally using `#dig` or `#[]`:
242
+
243
+ ```ruby
244
+ # Include to a depth of 3 to make sure it's included
245
+ homepage = store.find_by(slug: '/', include: 3)
246
+ # Traverse through the top nav menu => menu button 0 => about page
247
+ about_page = homepage.dig('fields', 'nav_menu', 'fields', 'buttons', 0, 'fields', 'page')
248
+ ```
249
+
250
+
227
251
  See the {WCC::Contentful::Store} documentation for more details.
228
252
 
229
253
  ### Direct CDN API (SimpleClient)
@@ -350,7 +374,6 @@ WCC::Contentful::SimpleClient::Cdn.new(
350
374
  space: '1234',
351
375
  # optional
352
376
  environment: 'staging', # omit to use master
353
- default_locale: '*',
354
377
  rate_limit_wait_timeout: 10,
355
378
  instrumentation: ActiveSupport::Notifications,
356
379
  connection: Faraday.new { |builder| ... },
@@ -394,10 +417,35 @@ newest version of an entry, or delete an entry out of the hash.
394
417
  #### Store Middleware
395
418
 
396
419
  The store layer is made up of a base store (which implements {WCC::Contentful::Store::Interface}),
397
- and optional middleware. The middleware
398
- allows custom transformation of received entries via the `#select` and `#transform`
399
- methods. To create your own middleware simply include {WCC::Contentful::Middleware::Store}
400
- and implement those methods, then call `use` when configuring the store:
420
+ and some required middleware. The list of default middleware applied to each store is found in
421
+ {WCC::Contentful::Store::Factory.default_middleware}
422
+
423
+ To create your own middleware simply include {WCC::Contentful::Middleware::Store}. Then you can optionally implement
424
+ the `#transform` and `#select?` methods:
425
+
426
+ ```ruby
427
+ class MyMiddleware
428
+ include WCC::Contentful::Middleware::Store
429
+
430
+ # Called for each entry that is requested out of the backing store. You can modify the entry and return it to the
431
+ # next layer.
432
+ def transform(entry, options)
433
+ # Do something with the entry...
434
+ # Make sure you return it at the end!
435
+ entry
436
+ end
437
+
438
+ def select?(entry, options)
439
+ # Choose whether this entry should exist or not. If you return false here, then the entry will act as though it
440
+ # were archived in Contentful.
441
+ entry.dig('fields', 'hide_until') > Time.zone.now
442
+ end
443
+ end
444
+ ```
445
+
446
+ You can also override any of the standard Store methods.
447
+
448
+ To apply the middleware, call `use` when configuring the store:
401
449
 
402
450
  ```ruby
403
451
  config.store :direct do
@@ -415,6 +463,24 @@ ActiveModel. The models are namespaced under the root class {WCC::Contentful::M
415
463
  Each model's implementation of `.find`, `.find_by`, and `.find_all` simply call
416
464
  into the configured Store.
417
465
 
466
+ Models can be initialized directly with the `.new` method, by passing in a hash:
467
+ ```ruby
468
+ entry = { 'sys' => ..., 'fields' => ... }
469
+ Page.new(entry)
470
+ ```
471
+
472
+ **The initializer must receive a localized entry**. An entry found using a `locale=*` query
473
+ must be transformed to a localized entry using the {WCC::Contentful::EntryLocaleTransformer} before
474
+ passing it to your model:
475
+
476
+ ```ruby
477
+ entry = client.entry('1234', locale: '*').raw
478
+ localized_entry = WCC::Contentful::EntryLocaleTransformer.transform_to_locale(entry, 'en-US')
479
+ Page.new(localized_entry)
480
+ ```
481
+
482
+ The Store layer ensures that localized entries are returned using the {WCC::Contentful::Middleware::Store::LocaleMiddleware}.
483
+
418
484
  The main benefit of the Model layer is lazy link resolution. When a model's
419
485
  property is accessed, if that property is a link that has not been resolved
420
486
  yet (for example using the `include: n` parameter on `.find_by`), the model
@@ -51,7 +51,6 @@ module WCC::Contentful
51
51
  management_token: config.management_token,
52
52
  space: config.space,
53
53
  environment: config.environment,
54
- default_locale: config.default_locale,
55
54
  connection: config.connection,
56
55
  webhook_username: config.webhook_username,
57
56
  webhook_password: config.webhook_password,
@@ -8,6 +8,7 @@ class WCC::Contentful::Configuration
8
8
  connection
9
9
  connection_options
10
10
  default_locale
11
+ locale_fallbacks
11
12
  environment
12
13
  instrumentation_adapter
13
14
  logger
@@ -40,6 +41,11 @@ class WCC::Contentful::Configuration
40
41
  attr_accessor :environment
41
42
  # Sets the default locale. Defaults to 'en-US'.
42
43
  attr_accessor :default_locale
44
+ # Sets up locale fallbacks. This is a Ruby hash which maps locale codes to fallback locale codes.
45
+ # Defaults are loaded from contentful-schema.json but can be overridden here.
46
+ # If data is missing for one locale, we will use data in the "fallback locale".
47
+ # See https://www.contentful.com/developers/docs/tutorials/general/setting-locales/#custom-fallback-locales
48
+ attr_accessor :locale_fallbacks
43
49
  # Sets the Content Preview API access token. Only required if you use the
44
50
  # preview flag.
45
51
  attr_accessor :preview_token
@@ -106,11 +112,11 @@ class WCC::Contentful::Configuration
106
112
  # The block is executed in the context of a WCC::Contentful::Store::Factory.
107
113
  # this can be used to apply middleware, etc.
108
114
  def store(*params, &block)
109
- type, *params = params
110
- if type
115
+ preset, *params = params
116
+ if preset
111
117
  @store_factory = WCC::Contentful::Store::Factory.new(
112
118
  self,
113
- type,
119
+ preset,
114
120
  params
115
121
  )
116
122
  end
@@ -199,7 +205,8 @@ class WCC::Contentful::Configuration
199
205
  @management_token = ENV.fetch('CONTENTFUL_MANAGEMENT_TOKEN', nil)
200
206
  @preview_token = ENV.fetch('CONTENTFUL_PREVIEW_TOKEN', nil)
201
207
  @space = ENV.fetch('CONTENTFUL_SPACE_ID', nil)
202
- @default_locale = nil
208
+ @default_locale = 'en-US'
209
+ @locale_fallbacks = {}
203
210
  @middleware = []
204
211
  @update_schema_file = :if_possible
205
212
  @schema_file = 'db/contentful-schema.json'
@@ -242,7 +249,7 @@ class WCC::Contentful::Configuration
242
249
  def initialize(configuration)
243
250
  ATTRIBUTES.each do |att|
244
251
  val = configuration.public_send(att)
245
- val.freeze if val.is_a?(Hash) || val.is_a?(Array)
252
+ val = val.dup.freeze if val.is_a?(Hash) || val.is_a?(Array)
246
253
  instance_variable_set("@#{att}", val)
247
254
  end
248
255
  end
@@ -25,7 +25,8 @@ class WCC::Contentful::DownloadsSchema
25
25
 
26
26
  File.write(@file, format_json({
27
27
  'contentTypes' => content_types,
28
- 'editorInterfaces' => editor_interfaces
28
+ 'editorInterfaces' => editor_interfaces,
29
+ 'locales' => locales
29
30
  }))
30
31
  end
31
32
 
@@ -45,8 +46,11 @@ class WCC::Contentful::DownloadsSchema
45
46
 
46
47
  existing_eis = contents['editorInterfaces'].sort_by { |i| i.dig('sys', 'contentType', 'sys', 'id') }
47
48
  return true unless editor_interfaces.count == existing_eis.count
49
+ return true unless deep_contains_all(editor_interfaces, existing_eis)
48
50
 
49
- !deep_contains_all(editor_interfaces, existing_eis)
51
+ existing_locales = contents['locales'].sort_by { |i| i.dig('sys', 'contentType', 'sys', 'id') }
52
+ return true unless locales.count == existing_locales.count
53
+ return true unless deep_contains_all(locales, existing_locales)
50
54
  end
51
55
 
52
56
  def content_types
@@ -65,6 +69,14 @@ class WCC::Contentful::DownloadsSchema
65
69
  .sort_by { |i| i.dig('sys', 'contentType', 'sys', 'id') }
66
70
  end
67
71
 
72
+ def locales
73
+ @locales ||=
74
+ @client.locales(limit: 1000)
75
+ .items
76
+ .map { |l| strip_sys(l) }
77
+ .sort_by { |l| l.dig('sys', 'code') }
78
+ end
79
+
68
80
  private
69
81
 
70
82
  def strip_sys(obj)
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # This class provides helper methods to transform Entry and Asset hashes from
5
+ # the "locale=*" format to a specific locale, and vice versa.
6
+ module WCC::Contentful::EntryLocaleTransformer
7
+ extend self
8
+
9
+ # Attribute reader falling back to WCC::Contentful configuration
10
+ # needed for locale fallbacks
11
+ def configuration
12
+ @configuration || WCC::Contentful.configuration
13
+ end
14
+
15
+ ##
16
+ # Takes an entry which represents a specific 'sys.locale' and transforms it
17
+ # to the 'locale=*' format
18
+ def transform_to_star(entry)
19
+ # locale=* entries have a nil sys.locale
20
+ unless entry_locale = entry.dig('sys', 'locale')
21
+ # nothing to do
22
+ return entry
23
+ end
24
+
25
+ sys = entry['sys'].except('locale').merge({
26
+ 'WCC::Contentful::EntryLocaleTransformer:locales_included' => [entry_locale]
27
+ })
28
+ fields =
29
+ entry['fields'].transform_values do |value|
30
+ h = {}
31
+ h[entry_locale] = value
32
+ h
33
+ end
34
+
35
+ {
36
+ 'sys' => sys,
37
+ 'fields' => fields
38
+ }
39
+ end
40
+
41
+ ##
42
+ # Takes an entry in the 'locale=*' format and transforms it to a specific locale
43
+ def transform_to_locale(entry, locale)
44
+ # If the backing store already returned a localized entry, nothing to do
45
+ if entry_locale = entry.dig('sys', 'locale')
46
+ unless entry_locale == locale
47
+ raise WCC::Contentful::LocaleMismatchError,
48
+ "expected #{locale} but was #{entry_locale}"
49
+ end
50
+
51
+ return entry
52
+ end
53
+ return entry unless entry['fields']
54
+
55
+ # Transform the store's "locale=*" entry into a localized one
56
+ locale ||= default_locale
57
+
58
+ sys = entry['sys'].deep_dup
59
+ sys['locale'] = locale
60
+ fields =
61
+ entry['fields'].transform_values do |value|
62
+ next if value.nil?
63
+
64
+ # replace the all-locales value with the localized value
65
+ l = locale
66
+ v = nil
67
+ while l
68
+ v = value[l]
69
+ break if v
70
+
71
+ l = configuration.locale_fallbacks[l]
72
+ end
73
+
74
+ v
75
+ end
76
+
77
+ {
78
+ 'sys' => sys,
79
+ 'fields' => fields
80
+ }
81
+ end
82
+
83
+ ##
84
+ # Takes an entry in a specific 'sys.locale' and merges it into an entry that is
85
+ # in the 'locale=*' format
86
+ def reduce_to_star(memo, entry)
87
+ if memo_locale = memo.dig('sys', 'locale')
88
+ raise WCC::Contentful::LocaleMismatchError, "expected locale: * but was #{memo_locale}"
89
+ end
90
+ unless entry_locale = entry.dig('sys', 'locale')
91
+ raise WCC::Contentful::LocaleMismatchError, 'expected a specific locale but got locale: *'
92
+ end
93
+
94
+ if memo.dig('sys', 'id') != entry.dig('sys', 'id')
95
+ raise ArgumentError,
96
+ "IDs of memo and entry must match! were (#{memo.dig('sys',
97
+ 'id').inspect} and #{entry.dig('sys', 'id').inspect})"
98
+ end
99
+
100
+ entry['fields'].each do |key, value|
101
+ memo_field = memo['fields'][key] ||= {}
102
+ memo_field[entry_locale] = value
103
+ end
104
+
105
+ memo
106
+ end
107
+ end
@@ -36,4 +36,9 @@ module WCC::Contentful
36
36
 
37
37
  class InitializationError < StandardError
38
38
  end
39
+
40
+ # Raised by {WCC::Contentful::Middleware::Store::LocaleMiddleware} when the
41
+ # backing store loads an entry for the wrong locale.
42
+ class LocaleMismatchError < StandardError
43
+ end
39
44
  end
@@ -83,6 +83,7 @@ class WCC::Contentful::LinkVisitor
83
83
  end
84
84
  yield(raw_value, locale)
85
85
  else
86
+ # yield each locale in turn
86
87
  raw_value&.each_with_object({}) do |(l, val), h|
87
88
  h[l] = yield(val, l)
88
89
  end
@@ -105,8 +106,18 @@ class WCC::Contentful::LinkVisitor
105
106
  end
106
107
 
107
108
  def set_field(field, locale, index, val)
108
- current_field = (entry['fields'][field] ||= {})
109
+ # default entry
110
+ if locale == entry.dig('sys', 'locale')
111
+ if index.nil?
112
+ entry['fields'][field] = val
113
+ else
114
+ (entry['fields'][field] ||= [])[index] = val
115
+ end
116
+ return
117
+ end
109
118
 
119
+ # locale=* entry
120
+ current_field = (entry['fields'][field] ||= {})
110
121
  if index.nil?
111
122
  current_field[locale] = val
112
123
  else
@@ -6,7 +6,11 @@ module WCC::Contentful::Middleware::Store
6
6
  # include instrumentation, but not specifically store stack instrumentation
7
7
  include WCC::Contentful::Instrumentation
8
8
 
9
- attr_accessor :expires_in
9
+ attr_accessor :expires_in, :configuration
10
+
11
+ def default_locale
12
+ @default_locale ||= configuration&.default_locale&.to_s || 'en-US'
13
+ end
10
14
 
11
15
  def initialize(cache = nil)
12
16
  @cache = cache || ActiveSupport::Cache::MemoryStore.new
@@ -22,22 +26,36 @@ module WCC::Contentful::Middleware::Store
22
26
  # Store a nil object if we can't find the object on the CDN.
23
27
  (store.find(key, **options) || nil_obj(key)) if key =~ /^\w+$/
24
28
  end
25
- _instrument(event, key: key, options: options)
26
29
 
27
- case found.try(:dig, 'sys', 'type')
28
- when 'Nil', 'DeletedEntry', 'DeletedAsset'
29
- nil
30
- else
31
- found
30
+ return unless found
31
+ return if %w[Nil DeletedEntry DeletedAsset].include?(found.dig('sys', 'type'))
32
+
33
+ # If what we found in the cache is for the wrong Locale, go hit the store directly.
34
+ # Now that the one locale is in the cache, when we index next time we'll index the
35
+ # all-locales version and we'll be fine.
36
+ locale = options[:locale]&.to_s || default_locale
37
+ found_locale = found.dig('sys', 'locale')&.to_s
38
+ if found_locale && (found_locale != locale)
39
+ event = 'miss'
40
+ return store.find(key, **options)
32
41
  end
42
+
43
+ found
44
+ ensure
45
+ _instrument(event, key: key, options: options)
33
46
  end
34
47
 
35
48
  # TODO: https://github.com/watermarkchurch/wcc-contentful/issues/18
36
49
  # figure out how to cache the results of a find_by query, ex:
37
50
  # `find_by('slug' => '/about')`
38
51
  def find_by(content_type:, filter: nil, options: nil)
52
+ options ||= {}
39
53
  if filter&.keys == ['sys.id'] && found = @cache.read(filter['sys.id'])
40
- return found
54
+ # This is equivalent to a find, usually this is done by the resolver to
55
+ # try to include deeper relationships. Since we already have this object,
56
+ # don't hit the API again.
57
+ return if %w[Nil DeletedEntry DeletedAsset].include?(found.dig('sys', 'type'))
58
+ return found if found.dig('sys', 'locale') == options[:locale]
41
59
  end
42
60
 
43
61
  store.find_by(content_type: content_type, filter: filter, options: options)
@@ -73,15 +91,21 @@ module WCC::Contentful::Middleware::Store
73
91
  id = json.dig('sys', 'id')
74
92
  type = json.dig('sys', 'type')
75
93
  prev = @cache.read(id)
76
- return if prev.nil? && LAZILY_CACHEABLE_TYPES.include?(type)
94
+ if prev.nil? && LAZILY_CACHEABLE_TYPES.include?(type)
95
+ _instrument('miss.index', key: id, type: type, prev: nil, next: nil)
96
+ return
97
+ end
77
98
 
78
99
  if (prev_rev = prev&.dig('sys', 'revision')) && (next_rev = json.dig('sys', 'revision')) && (next_rev < prev_rev)
100
+ _instrument('miss.index', key: id, type: type, prev: prev_rev, next: next_rev)
79
101
  return prev
80
102
  end
81
103
 
82
104
  # we also set DeletedEntry objects in the cache - no need to go hit the API when we know
83
105
  # this is a nil object
84
- @cache.write(id, json, expires_in: expires_in)
106
+ _instrument('write.index', key: id, type: type, prev: prev_rev, next: next_rev) do
107
+ @cache.write(id, json, expires_in: expires_in)
108
+ end
85
109
 
86
110
  case type
87
111
  when 'DeletedEntry', 'DeletedAsset'
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WCC::Contentful::Middleware::Store
4
+ ##
5
+ # This middleware enforces that all entries returned by the store layer are properly localized.
6
+ # It does this by transforming entries from the store's "locale=*" format into the specified locale (or default).
7
+ #
8
+ # Stores keep entries in the "locale=*" format, which is a hash of all locales for each field. This is convenient
9
+ # because the Sync API returns them in this format. However, the Model layer requires localized entries. So, to
10
+ # separate concerns, this middleware handles the transformation.
11
+ class LocaleMiddleware
12
+ include WCC::Contentful::Middleware::Store
13
+ include WCC::Contentful::EntryLocaleTransformer
14
+
15
+ attr_accessor :configuration
16
+
17
+ def default_locale
18
+ @default_locale ||= configuration&.default_locale&.to_s || 'en-US'
19
+ end
20
+
21
+ def transform(entry, options)
22
+ locale = options[:locale]&.to_s || default_locale
23
+ if locale == '*'
24
+ transform_to_star(entry)
25
+ else
26
+ transform_to_locale(entry, locale)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -33,18 +33,20 @@ module WCC::Contentful::Middleware::Store
33
33
 
34
34
  def find(id, **options)
35
35
  found = store.find(id, **options)
36
- return transform(found) if found && (!has_select? || select?(found))
36
+ return transform(found, options) if found && (!has_select? || select?(found, options))
37
37
  end
38
38
 
39
39
  def find_by(options: nil, **args)
40
+ options ||= {}
40
41
  result = store.find_by(**args.merge(options: options))
41
- return unless result && (!has_select? || select?(result))
42
+ return unless result && (!has_select? || select?(result, options))
42
43
 
43
- result = resolve_includes(result, options[:include]) if options && options[:include]
44
- transform(result)
44
+ result = resolve_includes(result, options[:include], options) if options && options[:include]
45
+ transform(result, options)
45
46
  end
46
47
 
47
48
  def find_all(options: nil, **args)
49
+ options ||= {}
48
50
  DelegatingQuery.new(
49
51
  store.find_all(**args.merge(options: options)),
50
52
  middleware: self,
@@ -52,20 +54,20 @@ module WCC::Contentful::Middleware::Store
52
54
  )
53
55
  end
54
56
 
55
- def resolve_includes(entry, depth)
57
+ def resolve_includes(entry, depth, options)
56
58
  return entry unless entry && depth && depth > 0
57
59
 
58
60
  # We only care about entries (see #resolved_link?)
59
- WCC::Contentful::LinkVisitor.new(entry, :Entry, depth: depth).map! do |val|
60
- resolve_link(val)
61
+ WCC::Contentful::LinkVisitor.new(entry, :Entry, :Asset, depth: depth).map! do |val|
62
+ resolve_link(val, options)
61
63
  end
62
64
  end
63
65
 
64
- def resolve_link(val)
66
+ def resolve_link(val, options)
65
67
  return val unless resolved_link?(val)
66
68
 
67
- if !has_select? || select?(val)
68
- transform(val)
69
+ if !has_select? || select?(val, options)
70
+ transform(val, options)
69
71
  else
70
72
  # Pretend it's an unresolved link -
71
73
  # matches the behavior of a store when the link cannot be retrieved
@@ -74,7 +76,7 @@ module WCC::Contentful::Middleware::Store
74
76
  end
75
77
 
76
78
  def resolved_link?(value)
77
- value.is_a?(Hash) && value.dig('sys', 'type') == 'Entry'
79
+ value.is_a?(Hash) && %w[Entry Asset].include?(value.dig('sys', 'type'))
78
80
  end
79
81
 
80
82
  def has_select? # rubocop:disable Naming/PredicateName
@@ -83,7 +85,7 @@ module WCC::Contentful::Middleware::Store
83
85
 
84
86
  # The default version of `#transform` just returns the entry.
85
87
  # Override this with your own implementation.
86
- def transform(entry)
88
+ def transform(entry, _options)
87
89
  entry
88
90
  end
89
91
 
@@ -110,11 +112,13 @@ module WCC::Contentful::Middleware::Store
110
112
 
111
113
  def to_enum
112
114
  result = wrapped_query.to_enum
113
- result = result.select { |x| middleware.select?(x) } if middleware.has_select?
115
+ result = result.select { |x| middleware.select?(x, options) } if middleware.has_select?
114
116
 
115
- result = result.map { |x| middleware.resolve_includes(x, options[:include]) } if options && options[:include]
117
+ if options && options[:include]
118
+ result = result.map { |x| middleware.resolve_includes(x, options[:include], options) }
119
+ end
116
120
 
117
- result.map { |x| middleware.transform(x) }
121
+ result.map { |x| middleware.transform(x, options) }
118
122
  end
119
123
 
120
124
  def apply(filter, context = nil)
@@ -150,7 +154,7 @@ module WCC::Contentful::Middleware::Store
150
154
  def initialize(wrapped_query, middleware:, options: nil, **extra)
151
155
  @wrapped_query = wrapped_query
152
156
  @middleware = middleware
153
- @options = options
157
+ @options = options || {}
154
158
  @extra = extra
155
159
  end
156
160
  end
@@ -59,10 +59,16 @@ module WCC::Contentful
59
59
 
60
60
  define_method(:initialize) do |raw, context = nil|
61
61
  ct = content_type_from_raw(raw)
62
- if ct != typedef.content_type
62
+ if ct.present? && ct != typedef.content_type
63
63
  raise ArgumentError, 'Wrong Content Type - ' \
64
64
  "'#{raw.dig('sys', 'id')}' is a #{ct}, expected #{typedef.content_type}"
65
65
  end
66
+ if raw.dig('sys', 'locale').blank? && %w[Entry Asset].include?(raw.dig('sys', 'type'))
67
+ raise ArgumentError, 'Model layer cannot represent "locale=*" entries. ' \
68
+ "Please use a specific locale in your query. \n" \
69
+ "(Error occurred with entry id: #{raw.dig('sys', 'id')})"
70
+ end
71
+
66
72
  @raw = raw.freeze
67
73
 
68
74
  created_at = raw.dig('sys', 'createdAt')
@@ -72,7 +78,7 @@ module WCC::Contentful
72
78
  @sys = WCC::Contentful::Sys.new(
73
79
  raw.dig('sys', 'id'),
74
80
  raw.dig('sys', 'type'),
75
- raw.dig('sys', 'locale') || context.try(:[], :locale) || 'en-US',
81
+ raw.dig('sys', 'locale'),
76
82
  raw.dig('sys', 'space', 'sys', 'id'),
77
83
  created_at,
78
84
  updated_at,
@@ -81,7 +87,8 @@ module WCC::Contentful
81
87
  )
82
88
 
83
89
  typedef.fields.each_value do |f|
84
- raw_value = raw.dig('fields', f.name, @sys.locale)
90
+ raw_value = raw.dig('fields', f.name)
91
+
85
92
  if raw_value.present?
86
93
  case f.type
87
94
  # DateTime is intentionally not parsed!