wcc-contentful 1.3.2 → 1.4.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +80 -13
  3. data/lib/wcc/contentful/active_record_shim.rb +2 -2
  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 +25 -8
  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 +9 -2
  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 -0
  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 +78 -46
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3cbb012e1545d674abc930f6513ea342611beb586b2afe5ad5ce703903e5a9cb
4
- data.tar.gz: da23bc050241c0ae55e1952fc48d892d2b2d768f33d314aef34f4a5f159db585
3
+ metadata.gz: 40c4c53ea7b5054745f8cd28841d59934a2e83ea65e4658e64124533412500bb
4
+ data.tar.gz: 53150802f51676dafc356812578159c23670335b8066c0536ad0d0b89fb094fd
5
5
  SHA512:
6
- metadata.gz: e53a8c32ea3687f8e5e8bfaac3a4a15d940e21873c7068c20b26cca92b5ed2876a8f680949f2bf169ebae258a00c0665f63c314888a1c3379b69b37da2be97d5
7
- data.tar.gz: 24e358dc0f8ec03873ffb27ff6d79c5a64f0fda24b5f66336482ca38f0b4b654227907df96955005fcc4cf04ed811e78070c3688f3c3d0629406666734c8fd93
6
+ metadata.gz: cd9ad6f0e34c388c12bc55fb7a62e5a55463f83fa0b09c0964e9f990d373ef142c890d54b8d77d6cd5e4e8e53a3c199919163268b2fddba7ec79e6f39e040a18
7
+ data.tar.gz: 1102e99319ded82fdef9293b06f68dae7d06b978696f6c12afe094f467d8f8cfff7242ada67e2adebed4b2903e279bd637ac468569dbf79a786e3ed07244337d
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)
@@ -394,10 +418,35 @@ newest version of an entry, or delete an entry out of the hash.
394
418
  #### Store Middleware
395
419
 
396
420
  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:
421
+ and some required middleware. The list of default middleware applied to each store is found in
422
+ {WCC::Contentful::Store::Factory.default_middleware}
423
+
424
+ To create your own middleware simply include {WCC::Contentful::Middleware::Store}. Then you can optionally implement
425
+ the `#transform` and `#select?` methods:
426
+
427
+ ```ruby
428
+ class MyMiddleware
429
+ include WCC::Contentful::Middleware::Store
430
+
431
+ # Called for each entry that is requested out of the backing store. You can modify the entry and return it to the
432
+ # next layer.
433
+ def transform(entry, options)
434
+ # Do something with the entry...
435
+ # Make sure you return it at the end!
436
+ entry
437
+ end
438
+
439
+ def select?(entry, options)
440
+ # Choose whether this entry should exist or not. If you return false here, then the entry will act as though it
441
+ # were archived in Contentful.
442
+ entry.dig('fields', 'hide_until') > Time.zone.now
443
+ end
444
+ end
445
+ ```
446
+
447
+ You can also override any of the standard Store methods.
448
+
449
+ To apply the middleware, call `use` when configuring the store:
401
450
 
402
451
  ```ruby
403
452
  config.store :direct do
@@ -415,6 +464,24 @@ ActiveModel. The models are namespaced under the root class {WCC::Contentful::M
415
464
  Each model's implementation of `.find`, `.find_by`, and `.find_all` simply call
416
465
  into the configured Store.
417
466
 
467
+ Models can be initialized directly with the `.new` method, by passing in a hash:
468
+ ```ruby
469
+ entry = { 'sys' => ..., 'fields' => ... }
470
+ Page.new(entry)
471
+ ```
472
+
473
+ **The initializer must receive a localized entry**. An entry found using a `locale=*` query
474
+ must be transformed to a localized entry using the {WCC::Contentful::EntryLocaleTransformer} before
475
+ passing it to your model:
476
+
477
+ ```ruby
478
+ entry = client.entry('1234', locale: '*').raw
479
+ localized_entry = WCC::Contentful::EntryLocaleTransformer.transform_to_locale(entry, 'en-US')
480
+ Page.new(localized_entry)
481
+ ```
482
+
483
+ The Store layer ensures that localized entries are returned using the {WCC::Contentful::Middleware::Store::LocaleMiddleware}.
484
+
418
485
  The main benefit of the Model layer is lazy link resolution. When a model's
419
486
  property is accessed, if that property is a link that has not been resolved
420
487
  yet (for example using the `include: n` parameter on `.find_by`), the model
@@ -33,7 +33,7 @@ module WCC::Contentful::ActiveRecordShim
33
33
 
34
34
  class_methods do
35
35
  def model_name
36
- ActiveModel::Name.new(self, nil, WCC::Contentful::Helpers.constant_from_content_type(content_type))
36
+ WCC::Contentful::Helpers.constant_from_content_type(content_type)
37
37
  end
38
38
 
39
39
  def const_get(name)
@@ -46,7 +46,7 @@ module WCC::Contentful::ActiveRecordShim
46
46
  end
47
47
 
48
48
  def table_name
49
- model_name.plural.tableize
49
+ model_name.tableize
50
50
  end
51
51
 
52
52
  def unscoped
@@ -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,35 @@ 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
+ if found.dig('sys', 'locale') != locale
38
+ event = 'miss'
39
+ return store.find(key, **options)
32
40
  end
41
+
42
+ found
43
+ ensure
44
+ _instrument(event, key: key, options: options)
33
45
  end
34
46
 
35
47
  # TODO: https://github.com/watermarkchurch/wcc-contentful/issues/18
36
48
  # figure out how to cache the results of a find_by query, ex:
37
49
  # `find_by('slug' => '/about')`
38
50
  def find_by(content_type:, filter: nil, options: nil)
51
+ options ||= {}
39
52
  if filter&.keys == ['sys.id'] && found = @cache.read(filter['sys.id'])
40
- return found
53
+ # This is equivalent to a find, usually this is done by the resolver to
54
+ # try to include deeper relationships. Since we already have this object,
55
+ # don't hit the API again.
56
+ return if %w[Nil DeletedEntry DeletedAsset].include?(found.dig('sys', 'type'))
57
+ return found if found.dig('sys', 'locale') == options[:locale]
41
58
  end
42
59
 
43
60
  store.find_by(content_type: content_type, filter: filter, options: options)
@@ -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
@@ -63,6 +63,12 @@ module WCC::Contentful
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?
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!
@@ -41,7 +41,7 @@ module WCC::Contentful::ModelMethods
41
41
  store = context[:preview] ? self.class.services.preview_store : self.class.services.store
42
42
 
43
43
  raw_link_ids =
44
- links.map { |field_name| raw.dig('fields', field_name, sys.locale) }
44
+ links.map { |field_name| raw.dig('fields', field_name) }
45
45
  .flat_map do |raw_value|
46
46
  _try_map(raw_value) { |v| v.dig('sys', 'id') if v.dig('sys', 'type') == 'Link' }
47
47
  end
@@ -62,7 +62,7 @@ module WCC::Contentful::ModelMethods
62
62
  raise WCC::Contentful::ResolveError, "Cannot find #{self.class.content_type} with ID #{id}" unless raw
63
63
 
64
64
  @raw = raw.freeze
65
- links.each { |f| instance_variable_set("@#{f}", raw.dig('fields', f, sys.locale)) }
65
+ links.each { |f| instance_variable_set("@#{f}", raw.dig('fields', f)) }
66
66
  end
67
67
 
68
68
  links.each { |f| _resolve_field(f, depth, context, options) }
@@ -85,9 +85,7 @@ module WCC::Contentful::ModelMethods
85
85
  # the Contentful API.
86
86
  #
87
87
  # This differs from `#raw` in that it recursively includes the `#to_h`
88
- # of resolved links. It also sets the fields to the value for the entry's `#sys.locale`,
89
- # as though the entry had been retrieved from the API with `locale={#sys.locale}` rather
90
- # than `locale=*`.
88
+ # of resolved links.
91
89
  def to_h(stack = nil)
92
90
  raise WCC::Contentful::CircularReferenceError.new(stack, id) if stack&.include?(id)
93
91
 
@@ -122,7 +120,7 @@ module WCC::Contentful::ModelMethods
122
120
  end
123
121
 
124
122
  {
125
- 'sys' => { 'locale' => @sys.locale }.merge!(@raw['sys']),
123
+ 'sys' => @raw['sys'].dup,
126
124
  'fields' => fields
127
125
  }
128
126
  end
@@ -81,13 +81,16 @@ class WCC::Contentful::SimpleClient::Cdn < WCC::Contentful::SimpleClient
81
81
  def sync(sync_token: nil, **query, &block)
82
82
  return sync_old(sync_token: sync_token, **query) unless block_given?
83
83
 
84
- sync_token =
84
+ query = {
85
+ # override default locale for sync queries
86
+ locale: nil
87
+ }.merge(
85
88
  if sync_token
86
89
  { sync_token: sync_token }
87
90
  else
88
91
  { initial: true }
89
92
  end
90
- query = query.merge(sync_token)
93
+ ).merge(query)
91
94
 
92
95
  _instrument 'sync', sync_token: sync_token, query: query do
93
96
  resp = get('sync', query)