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.
- checksums.yaml +4 -4
- data/README.md +80 -13
- data/lib/wcc/contentful/active_record_shim.rb +2 -2
- 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_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 +78 -46
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
|
@@ -33,7 +33,7 @@ module WCC::Contentful::ActiveRecordShim
|
|
33
33
|
|
34
34
|
class_methods do
|
35
35
|
def model_name
|
36
|
-
|
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.
|
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
|
-
|
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
|
@@ -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!
|
@@ -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
|
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
|
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.
|
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' =>
|
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
|
-
|
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
|
-
|
93
|
+
).merge(query)
|
91
94
|
|
92
95
|
_instrument 'sync', sync_token: sync_token, query: query do
|
93
96
|
resp = get('sync', query)
|