wcc-contentful 1.3.0 → 1.4.0.rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +105 -14
- data/lib/wcc/contentful/configuration.rb +12 -5
- data/lib/wcc/contentful/downloads_schema.rb +14 -2
- data/lib/wcc/contentful/entry_locale_transformer.rb +107 -0
- data/lib/wcc/contentful/exceptions.rb +5 -0
- data/lib/wcc/contentful/link_visitor.rb +12 -1
- data/lib/wcc/contentful/middleware/store/caching_middleware.rb +25 -8
- data/lib/wcc/contentful/middleware/store/locale_middleware.rb +30 -0
- data/lib/wcc/contentful/middleware/store.rb +20 -16
- data/lib/wcc/contentful/model_api.rb +2 -2
- data/lib/wcc/contentful/model_builder.rb +9 -2
- data/lib/wcc/contentful/model_methods.rb +4 -6
- data/lib/wcc/contentful/simple_client/cdn.rb +5 -2
- data/lib/wcc/contentful/simple_client/management.rb +16 -0
- data/lib/wcc/contentful/simple_client.rb +1 -0
- data/lib/wcc/contentful/store/base.rb +6 -1
- data/lib/wcc/contentful/store/cdn_adapter.rb +13 -4
- data/lib/wcc/contentful/store/factory.rb +8 -1
- data/lib/wcc/contentful/store/memory_store.rb +27 -8
- data/lib/wcc/contentful/store/postgres_store.rb +4 -3
- data/lib/wcc/contentful/store/query/condition.rb +89 -0
- data/lib/wcc/contentful/store/query.rb +9 -35
- data/lib/wcc/contentful/store/rspec_examples/basic_store.rb +84 -12
- data/lib/wcc/contentful/store/rspec_examples/locale_queries.rb +220 -0
- data/lib/wcc/contentful/store/rspec_examples/operators/eq.rb +1 -1
- data/lib/wcc/contentful/store/rspec_examples.rb +13 -1
- data/lib/wcc/contentful/sync_engine.rb +1 -1
- data/lib/wcc/contentful/test/double.rb +1 -1
- data/lib/wcc/contentful/test/factory.rb +2 -4
- data/lib/wcc/contentful/version.rb +1 -1
- data/lib/wcc/contentful.rb +17 -6
- metadata +112 -62
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 40c4c53ea7b5054745f8cd28841d59934a2e83ea65e4658e64124533412500bb
|
4
|
+
data.tar.gz: 53150802f51676dafc356812578159c23670335b8066c0536ad0d0b89fb094fd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
-
|
165
|
-
# => #<
|
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
|
-
|
169
|
-
# => #<
|
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
|
-
|
174
|
-
# => [#<
|
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
|
-
|
178
|
-
# => #<
|
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
|
398
|
-
|
399
|
-
|
400
|
-
|
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
|
@@ -565,7 +632,10 @@ to multiple spaces within the same ruby process! You just have to create and in
|
|
565
632
|
The {WCC::Contentful::ModelAPI} concern makes this straightforward. Start by creating your Namespace
|
566
633
|
and including the concern:
|
567
634
|
```ruby
|
568
|
-
#
|
635
|
+
# lib/my_second_space.rb
|
636
|
+
|
637
|
+
# Note: This class must be in the "lib" folder in :zeitwerk mode, otherwise Rails 6+ will unload all your constants
|
638
|
+
# that were created in the initializer. Your models which subclass this namespace may reside in the app/models directory.
|
569
639
|
class MySecondSpace
|
570
640
|
include WCC::Contentful::ModelAPI
|
571
641
|
end
|
@@ -587,6 +657,11 @@ MySecondSpace.configure do |config|
|
|
587
657
|
config.space = ENV['SECOND_CONTENTFUL_SPACE_ID']
|
588
658
|
config.environment = ENV['CONTENTFUL_ENVIRONMENT']
|
589
659
|
end
|
660
|
+
|
661
|
+
# Ensure that models are reloaded in Rails development mode
|
662
|
+
Rails.application.config.to_prepare do
|
663
|
+
MySecondSpace.reload!
|
664
|
+
end
|
590
665
|
```
|
591
666
|
|
592
667
|
Finally, use it:
|
@@ -614,6 +689,22 @@ sync_engine = MySecondSpace.services.sync_engine
|
|
614
689
|
Note that the above services are not accessible on {WCC::Contentful::Services.instance}
|
615
690
|
or via the {WCC::Contentful::ServiceAccessors}.
|
616
691
|
|
692
|
+
#### Important Note when using Zeitwerk with Rails 6+
|
693
|
+
When using Rails >= 6 with `config.autoloader = :zeitwerk`, Rails will remove any models defined in `app/models` after
|
694
|
+
initialization and then load them again when they are referenced. If you `include WCC::Contentful::ModelAPI` in a class
|
695
|
+
defined inside the `app` directory, this will have the effect of deleting all configuration that was set in the initializer
|
696
|
+
as well as the constants generated from your schema.
|
697
|
+
This will result in one of two errors:
|
698
|
+
|
699
|
+
* `NameError (uninitialized constant MySecondSpace::MyContentType)`
|
700
|
+
if you try to reference a subclass such as `MyContentType < MySecondSpace::MyContentType`
|
701
|
+
* `ArgumentError (Not yet configured!)`
|
702
|
+
if you try to `MySecondSpace.find('xxxx')` to load an Entry or Asset
|
703
|
+
|
704
|
+
The solution is to have your secondary namespace in a folder which is not in the `autoload_paths`.
|
705
|
+
We suggest using `lib`, which will work so long as you have not added the `lib` folder to the `autoload_paths` as some
|
706
|
+
uninformed StackOverflow answers suggest you do.
|
707
|
+
|
617
708
|
### Using a sync store with a second space
|
618
709
|
|
619
710
|
If you use something other than the CDNAdapter with your second space, you will
|
@@ -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
|
-
|
110
|
-
if
|
115
|
+
preset, *params = params
|
116
|
+
if preset
|
111
117
|
@store_factory = WCC::Contentful::Store::Factory.new(
|
112
118
|
self,
|
113
|
-
|
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 =
|
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
|
-
|
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
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
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')
|
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
|
-
|
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
|
@@ -96,7 +96,7 @@ module WCC::Contentful::ModelAPI
|
|
96
96
|
loop do
|
97
97
|
begin
|
98
98
|
# The app may have defined a model and we haven't loaded it yet
|
99
|
-
const = parent.
|
99
|
+
const = parent.const_get(const_name)
|
100
100
|
return const if const && const < self
|
101
101
|
rescue NameError => e
|
102
102
|
raise e unless e.message =~ /uninitialized constant (.+::)*#{const_name}$/
|
@@ -156,7 +156,7 @@ module WCC::Contentful::ModelAPI
|
|
156
156
|
const_name = klass.name
|
157
157
|
begin
|
158
158
|
# the const_name is fully qualified so search from root
|
159
|
-
const = Object.
|
159
|
+
const = Object.const_get(const_name)
|
160
160
|
register_for_content_type(content_type, klass: const) if const
|
161
161
|
rescue NameError => e
|
162
162
|
msg = "Error when reloading constant #{const_name} - #{e}"
|
@@ -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')
|
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
|
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!
|