wcc-contentful 0.2.2 → 0.3.0.pre.rc
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +0 -1
- data/README.md +181 -8
- data/app/controllers/wcc/contentful/webhook_controller.rb +42 -2
- data/app/jobs/wcc/contentful/delayed_sync_job.rb +52 -3
- data/app/jobs/wcc/contentful/webhook_enable_job.rb +43 -0
- data/bin/console +4 -3
- data/bin/rails +2 -0
- data/config/initializers/mime_types.rb +10 -1
- data/lib/wcc/contentful.rb +14 -142
- data/lib/wcc/contentful/client_ext.rb +17 -4
- data/lib/wcc/contentful/configuration.rb +25 -84
- data/lib/wcc/contentful/engine.rb +19 -0
- data/lib/wcc/contentful/exceptions.rb +25 -28
- data/lib/wcc/contentful/graphql.rb +0 -1
- data/lib/wcc/contentful/graphql/types.rb +1 -1
- data/lib/wcc/contentful/helpers.rb +3 -2
- data/lib/wcc/contentful/indexed_representation.rb +6 -0
- data/lib/wcc/contentful/model.rb +68 -34
- data/lib/wcc/contentful/model_builder.rb +65 -67
- data/lib/wcc/contentful/model_methods.rb +189 -0
- data/lib/wcc/contentful/model_singleton_methods.rb +83 -0
- data/lib/wcc/contentful/services.rb +146 -0
- data/lib/wcc/contentful/simple_client.rb +35 -33
- data/lib/wcc/contentful/simple_client/http_adapter.rb +9 -0
- data/lib/wcc/contentful/simple_client/management.rb +81 -0
- data/lib/wcc/contentful/simple_client/response.rb +61 -37
- data/lib/wcc/contentful/simple_client/typhoeus_adapter.rb +12 -0
- data/lib/wcc/contentful/store.rb +45 -18
- data/lib/wcc/contentful/store/base.rb +128 -8
- data/lib/wcc/contentful/store/cdn_adapter.rb +92 -22
- data/lib/wcc/contentful/store/lazy_cache_store.rb +94 -9
- data/lib/wcc/contentful/store/memory_store.rb +13 -8
- data/lib/wcc/contentful/store/postgres_store.rb +44 -11
- data/lib/wcc/contentful/sys.rb +28 -0
- data/lib/wcc/contentful/version.rb +1 -1
- data/wcc-contentful.gemspec +3 -9
- metadata +87 -107
- data/.circleci/config.yml +0 -51
- data/.gitignore +0 -26
- data/.rubocop.yml +0 -243
- data/.rubocop_todo.yml +0 -13
- data/.travis.yml +0 -5
- data/CHANGELOG.md +0 -45
- data/CODE_OF_CONDUCT.md +0 -74
- data/Guardfile +0 -58
- data/LICENSE.txt +0 -21
- data/Rakefile +0 -8
- data/lib/generators/wcc/USAGE +0 -24
- data/lib/generators/wcc/model_generator.rb +0 -90
- data/lib/generators/wcc/templates/.keep +0 -0
- data/lib/generators/wcc/templates/Procfile +0 -3
- data/lib/generators/wcc/templates/contentful_shell_wrapper +0 -385
- data/lib/generators/wcc/templates/menu/generated_add_menus.ts +0 -90
- data/lib/generators/wcc/templates/menu/models/menu.rb +0 -23
- data/lib/generators/wcc/templates/menu/models/menu_button.rb +0 -23
- data/lib/generators/wcc/templates/page/generated_add_pages.ts +0 -50
- data/lib/generators/wcc/templates/page/models/page.rb +0 -23
- data/lib/generators/wcc/templates/release +0 -9
- data/lib/generators/wcc/templates/wcc_contentful.rb +0 -17
- data/lib/wcc/contentful/model/menu.rb +0 -7
- data/lib/wcc/contentful/model/menu_button.rb +0 -15
- data/lib/wcc/contentful/model/page.rb +0 -8
- data/lib/wcc/contentful/model/redirect.rb +0 -19
- data/lib/wcc/contentful/model_validators.rb +0 -115
- data/lib/wcc/contentful/model_validators/dsl.rb +0 -165
@@ -22,6 +22,24 @@ class WCC::Contentful::SimpleClient
|
|
22
22
|
raw.dig('message') || "#{code}: #{raw_response.message}"
|
23
23
|
end
|
24
24
|
|
25
|
+
def next_page?
|
26
|
+
return unless raw.key? 'items'
|
27
|
+
|
28
|
+
raw['items'].length + raw['skip'] < raw['total']
|
29
|
+
end
|
30
|
+
|
31
|
+
def next_page
|
32
|
+
return unless next_page?
|
33
|
+
|
34
|
+
@next_page ||= @client.get(
|
35
|
+
@request[:url],
|
36
|
+
(@request[:query] || {}).merge({
|
37
|
+
skip: raw['items'].length + raw['skip']
|
38
|
+
})
|
39
|
+
)
|
40
|
+
@next_page.assert_ok!
|
41
|
+
end
|
42
|
+
|
25
43
|
def initialize(client, request, raw_response)
|
26
44
|
@client = client
|
27
45
|
@request = request
|
@@ -31,33 +49,20 @@ class WCC::Contentful::SimpleClient
|
|
31
49
|
|
32
50
|
def assert_ok!
|
33
51
|
return self if code >= 200 && code < 300
|
52
|
+
|
34
53
|
raise ApiError[code], self
|
35
54
|
end
|
36
55
|
|
37
56
|
def each_page(&block)
|
38
57
|
raise ArgumentError, 'Not a collection response' unless raw['items']
|
39
58
|
|
40
|
-
memoized_pages = (@memoized_pages ||= [self])
|
41
59
|
ret =
|
42
60
|
Enumerator.new do |y|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
skip_amt = current_page.raw['items'].length + current_page.raw['skip']
|
49
|
-
break if current_page.raw['items'].empty? || skip_amt >= current_page.raw['total']
|
50
|
-
|
51
|
-
page_index += 1
|
52
|
-
if page_index < memoized_pages.length
|
53
|
-
current_page = memoized_pages[page_index]
|
54
|
-
else
|
55
|
-
current_page = @client.get(
|
56
|
-
@request[:url],
|
57
|
-
(@request[:query] || {}).merge({ skip: skip_amt })
|
58
|
-
)
|
59
|
-
current_page.assert_ok!
|
60
|
-
memoized_pages.push(current_page)
|
61
|
+
y << self
|
62
|
+
|
63
|
+
if next_page?
|
64
|
+
next_page.each_page.each do |page|
|
65
|
+
y << page
|
61
66
|
end
|
62
67
|
end
|
63
68
|
end
|
@@ -81,8 +86,21 @@ class WCC::Contentful::SimpleClient
|
|
81
86
|
|
82
87
|
def first
|
83
88
|
raise ArgumentError, 'Not a collection response' unless raw['items']
|
89
|
+
|
84
90
|
raw['items'].first
|
85
91
|
end
|
92
|
+
|
93
|
+
def includes
|
94
|
+
@includes ||=
|
95
|
+
raw.dig('includes')&.each_with_object({}) do |(_t, entries), h|
|
96
|
+
entries.each { |e| h[e.dig('sys', 'id')] = e }
|
97
|
+
end || {}
|
98
|
+
|
99
|
+
return @includes unless @next_page
|
100
|
+
|
101
|
+
# This could be more efficient - maybe not worth worrying about
|
102
|
+
@includes.merge(@next_page.includes)
|
103
|
+
end
|
86
104
|
end
|
87
105
|
|
88
106
|
class SyncResponse < Response
|
@@ -90,31 +108,36 @@ class WCC::Contentful::SimpleClient
|
|
90
108
|
super(response.client, response.request, response.raw_response)
|
91
109
|
end
|
92
110
|
|
111
|
+
def next_page?
|
112
|
+
raw['nextPageUrl'].present?
|
113
|
+
end
|
114
|
+
|
115
|
+
def next_page
|
116
|
+
return unless next_page?
|
117
|
+
|
118
|
+
@next_page ||= SyncResponse.new(@client.get(raw['nextPageUrl']))
|
119
|
+
@next_page.assert_ok!
|
120
|
+
end
|
121
|
+
|
93
122
|
def next_sync_token
|
94
|
-
|
123
|
+
# If we haven't grabbed the next page yet, then our next "sync" will be getting
|
124
|
+
# the next page. We could just as easily call sync again with that token.
|
125
|
+
@next_page&.next_sync_token ||
|
126
|
+
@next_sync_token ||= SyncResponse.parse_sync_token(
|
127
|
+
raw['nextPageUrl'] || raw['nextSyncUrl']
|
128
|
+
)
|
95
129
|
end
|
96
130
|
|
97
131
|
def each_page
|
98
132
|
raise ArgumentError, 'Not a collection response' unless raw['items']
|
99
133
|
|
100
|
-
memoized_pages = (@memoized_pages ||= [self])
|
101
134
|
ret =
|
102
135
|
Enumerator.new do |y|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
break if current_page.raw['items'].empty?
|
109
|
-
|
110
|
-
page_index += 1
|
111
|
-
if page_index < memoized_pages.length
|
112
|
-
current_page = memoized_pages[page_index]
|
113
|
-
else
|
114
|
-
current_page = @client.get(raw['nextSyncUrl'])
|
115
|
-
current_page.assert_ok!
|
116
|
-
@next_sync_token = SyncResponse.parse_sync_token(current_page.raw['nextSyncUrl'])
|
117
|
-
memoized_pages.push(current_page)
|
136
|
+
y << self
|
137
|
+
|
138
|
+
if next_page?
|
139
|
+
next_page.each_page.each do |page|
|
140
|
+
y << page
|
118
141
|
end
|
119
142
|
end
|
120
143
|
end
|
@@ -127,7 +150,8 @@ class WCC::Contentful::SimpleClient
|
|
127
150
|
end
|
128
151
|
|
129
152
|
def count
|
130
|
-
|
153
|
+
raise NotImplementedError,
|
154
|
+
'Sync does not return an accurate total. Use #items.count instead.'
|
131
155
|
end
|
132
156
|
|
133
157
|
def self.parse_sync_token(url)
|
@@ -16,6 +16,18 @@ class TyphoeusAdapter
|
|
16
16
|
)
|
17
17
|
end
|
18
18
|
|
19
|
+
def post(url, body, headers = {}, proxy = {})
|
20
|
+
raise NotImplementedError, 'Proxying Not Yet Implemented' if proxy[:host]
|
21
|
+
|
22
|
+
TyphoeusAdapter::Response.new(
|
23
|
+
Typhoeus.post(
|
24
|
+
url,
|
25
|
+
body: body.to_json,
|
26
|
+
headers: headers
|
27
|
+
)
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
19
31
|
Response =
|
20
32
|
Struct.new(:raw) do
|
21
33
|
delegate :body, to: :raw
|
data/lib/wcc/contentful/store.rb
CHANGED
@@ -1,20 +1,38 @@
|
|
1
|
-
|
2
1
|
# frozen_string_literal: true
|
3
2
|
|
4
3
|
require_relative 'store/base'
|
5
4
|
require_relative 'store/memory_store'
|
6
|
-
require_relative 'store/lazy_cache_store'
|
7
5
|
require_relative 'store/cdn_adapter'
|
6
|
+
require_relative 'store/lazy_cache_store'
|
8
7
|
|
9
|
-
#
|
10
|
-
#
|
11
|
-
|
8
|
+
# The "Store" is the middle layer in the WCC::Contentful gem. It exposes an API
|
9
|
+
# that implements the configured content delivery strategy.
|
10
|
+
#
|
11
|
+
# The different content delivery strategies require different store implementations.
|
12
|
+
#
|
13
|
+
# direct:: Uses the WCC::Contentful::Store::CDNAdapter to wrap the Contentful CDN,
|
14
|
+
# providing an API consistent with the other stores. Any query made to
|
15
|
+
# the CDNAdapter will be immediately passed through to the API.
|
16
|
+
# The CDNAdapter does not implement #index because it does not care about
|
17
|
+
# updates coming from the Sync API.
|
18
|
+
#
|
19
|
+
# lazy_sync:: Uses the Contentful CDN in combination with an ActiveSupport::Cache
|
20
|
+
# implementation in order to respond with the cached data where possible,
|
21
|
+
# saving your CDN quota. The cache is kept up-to-date via the Sync API
|
22
|
+
# and the WCC::Contentful::DelayedSyncJob. It is correct, but not complete.
|
23
|
+
#
|
24
|
+
# eager_sync:: Uses one of the full store implementations to store the entirety
|
25
|
+
# of the Contentful space locally. All queries are run against this
|
26
|
+
# local copy, which is kept up to date via the Sync API and the
|
27
|
+
# WCC::Contentful::DelayedSyncJob. The local store is correct and complete.
|
28
|
+
#
|
29
|
+
# The currently configured store is available on WCC::Contentful::Services.instance.store
|
12
30
|
module WCC::Contentful::Store
|
13
31
|
SYNC_STORES = {
|
14
32
|
memory: ->(_config) { WCC::Contentful::Store::MemoryStore.new },
|
15
|
-
postgres: ->(
|
33
|
+
postgres: ->(config, *options) {
|
16
34
|
require_relative 'store/postgres_store'
|
17
|
-
WCC::Contentful::Store::PostgresStore.new(
|
35
|
+
WCC::Contentful::Store::PostgresStore.new(config, *options)
|
18
36
|
}
|
19
37
|
}.freeze
|
20
38
|
|
@@ -22,10 +40,11 @@ module WCC::Contentful::Store
|
|
22
40
|
eager_sync
|
23
41
|
lazy_sync
|
24
42
|
direct
|
43
|
+
custom
|
25
44
|
].freeze
|
26
45
|
|
27
46
|
Factory =
|
28
|
-
Struct.new(:config, :cdn_method, :content_delivery_params) do
|
47
|
+
Struct.new(:config, :services, :cdn_method, :content_delivery_params) do
|
29
48
|
def build_sync_store
|
30
49
|
unless respond_to?("build_#{cdn_method}")
|
31
50
|
raise ArgumentError, "Don't know how to build content delivery method #{cdn_method}"
|
@@ -36,38 +55,46 @@ module WCC::Contentful::Store
|
|
36
55
|
|
37
56
|
def validate!
|
38
57
|
unless CDN_METHODS.include?(cdn_method)
|
39
|
-
raise ArgumentError, "Please use one of #{CDN_METHODS}
|
58
|
+
raise ArgumentError, "Please use one of #{CDN_METHODS} instead of #{cdn_method}"
|
40
59
|
end
|
41
60
|
|
42
61
|
return unless respond_to?("validate_#{cdn_method}")
|
62
|
+
|
43
63
|
public_send("validate_#{cdn_method}", config, *content_delivery_params)
|
44
64
|
end
|
45
65
|
|
46
|
-
def build_eager_sync(config, store = nil, *
|
47
|
-
|
48
|
-
store = SYNC_STORES[store].call(config) if store.is_a?(Symbol)
|
66
|
+
def build_eager_sync(config, store = nil, *options)
|
67
|
+
store = SYNC_STORES[store].call(config, *options) if store.is_a?(Symbol)
|
49
68
|
store || MemoryStore.new
|
50
69
|
end
|
51
70
|
|
52
|
-
def build_lazy_sync(
|
71
|
+
def build_lazy_sync(_config, *options)
|
53
72
|
WCC::Contentful::Store::LazyCacheStore.new(
|
54
|
-
|
73
|
+
services.client,
|
55
74
|
cache: ActiveSupport::Cache.lookup_store(*options)
|
56
75
|
)
|
57
76
|
end
|
58
77
|
|
59
|
-
def build_direct(
|
78
|
+
def build_direct(_config, *options)
|
60
79
|
if options.find { |array| array[:preview] == true }
|
61
|
-
CDNAdapter.new(
|
80
|
+
CDNAdapter.new(services.preview_client)
|
62
81
|
else
|
63
|
-
CDNAdapter.new(
|
82
|
+
CDNAdapter.new(services.client)
|
64
83
|
end
|
65
84
|
end
|
66
85
|
|
86
|
+
def build_custom(config, *options)
|
87
|
+
store = config.store
|
88
|
+
return store unless store&.respond_to?(:new)
|
89
|
+
|
90
|
+
store.new(config, options)
|
91
|
+
end
|
92
|
+
|
67
93
|
def validate_eager_sync(_config, store = nil, *_options)
|
68
94
|
return unless store.is_a?(Symbol)
|
69
95
|
|
70
|
-
return if SYNC_STORES.
|
96
|
+
return if SYNC_STORES.key?(store)
|
97
|
+
|
71
98
|
raise ArgumentError, "Please use one of #{SYNC_STORES.keys}"
|
72
99
|
end
|
73
100
|
end
|
@@ -1,20 +1,37 @@
|
|
1
|
-
|
2
1
|
# frozen_string_literal: true
|
3
2
|
|
3
|
+
# @api Store
|
4
4
|
module WCC::Contentful::Store
|
5
|
+
# This is the base class for stores which implement #index, and therefore
|
6
|
+
# must be kept up-to-date via the Sync API.
|
7
|
+
# @abstract At a minimum subclasses should override {#find}, {#find_all}, {#set},
|
8
|
+
# and #{delete}. As an alternative to overriding set and delete, the subclass
|
9
|
+
# can override {#index}. Index is called when a webhook triggers a sync, to
|
10
|
+
# update the store.
|
5
11
|
class Base
|
12
|
+
# Finds an entry by it's ID. The returned entry is a JSON hash
|
13
|
+
# @abstract Subclasses should implement this at a minimum to provide data
|
14
|
+
# to the WCC::Contentful::Model API.
|
6
15
|
def find(_id)
|
7
16
|
raise NotImplementedError, "#{self.class} does not implement #find"
|
8
17
|
end
|
9
18
|
|
19
|
+
# Sets the value of the entry with the given ID in the store.
|
20
|
+
# @abstract
|
10
21
|
def set(_id, _value)
|
11
22
|
raise NotImplementedError, "#{self.class} does not implement #set"
|
12
23
|
end
|
13
24
|
|
25
|
+
# Removes the entry by ID from the store.
|
26
|
+
# @abstract
|
14
27
|
def delete(_id)
|
15
28
|
raise NotImplementedError, "#{self.class} does not implement #delete"
|
16
29
|
end
|
17
30
|
|
31
|
+
# Processes a data point received via the Sync API. This can be a published
|
32
|
+
# entry or asset, or a 'DeletedEntry' or 'DeletedAsset'. The default
|
33
|
+
# implementation calls into #set and #delete to perform the appropriate
|
34
|
+
# operations in the store.
|
18
35
|
def index(json)
|
19
36
|
# Subclasses can override to do this in a more performant thread-safe way.
|
20
37
|
# Example: postgres_store could do this in a stored procedure for speed
|
@@ -43,15 +60,30 @@ module WCC::Contentful::Store
|
|
43
60
|
end
|
44
61
|
end
|
45
62
|
|
46
|
-
|
63
|
+
# Finds the first entry matching the given filter. A content type is required.
|
64
|
+
#
|
65
|
+
# @param [String] content_type The ID of the content type to search for.
|
66
|
+
# @param [Hash] filter A set of key-value pairs defining filter operations.
|
67
|
+
# See WCC::Contentful::Store::Base::Query
|
68
|
+
# @param [Hash] options An optional set of additional parameters to the query
|
69
|
+
# defining for example include depth. Not all store implementations respect all options.
|
70
|
+
def find_by(content_type:, filter: nil, options: nil)
|
47
71
|
# default implementation - can be overridden
|
48
|
-
q = find_all(content_type: content_type)
|
72
|
+
q = find_all(content_type: content_type, options: { limit: 1 }.merge!(options || {}))
|
49
73
|
q = q.apply(filter) if filter
|
50
74
|
q.first
|
51
75
|
end
|
52
76
|
|
77
|
+
# Finds all entries of the given content type. A content type is required.
|
78
|
+
#
|
79
|
+
# @abstract Subclasses should implement this at a minimum to provide data
|
80
|
+
# to the {WCC::Contentful::Model} API.
|
81
|
+
# @param [String] content_type The ID of the content type to search for.
|
82
|
+
# @param [Hash] options An optional set of additional parameters to the query
|
83
|
+
# defining for example include depth. Not all store implementations respect all options.
|
84
|
+
# @return [Query] A query object that exposes methods to apply filters
|
53
85
|
# rubocop:disable Lint/UnusedMethodArgument
|
54
|
-
def find_all(content_type:)
|
86
|
+
def find_all(content_type:, options: nil)
|
55
87
|
raise NotImplementedError, "#{self.class} does not implement find_all"
|
56
88
|
end
|
57
89
|
# rubocop:enable Lint/UnusedMethodArgument
|
@@ -60,29 +92,117 @@ module WCC::Contentful::Store
|
|
60
92
|
@mutex = Concurrent::ReentrantReadWriteLock.new
|
61
93
|
end
|
62
94
|
|
95
|
+
def ensure_hash(val)
|
96
|
+
raise ArgumentError, 'Value must be a Hash' unless val.is_a?(Hash)
|
97
|
+
end
|
98
|
+
|
63
99
|
protected
|
64
100
|
|
65
101
|
attr_reader :mutex
|
66
102
|
|
103
|
+
# The base class for query objects returned by find_all. Subclasses should
|
104
|
+
# override the #result method to return an array-like containing the query
|
105
|
+
# results.
|
67
106
|
class Query
|
68
107
|
delegate :first, to: :result
|
69
108
|
delegate :map, to: :result
|
70
109
|
delegate :count, to: :result
|
71
110
|
|
111
|
+
OPERATORS = %i[
|
112
|
+
eq
|
113
|
+
ne
|
114
|
+
all
|
115
|
+
in
|
116
|
+
nin
|
117
|
+
exists
|
118
|
+
lt
|
119
|
+
lte
|
120
|
+
gt
|
121
|
+
gte
|
122
|
+
query
|
123
|
+
match
|
124
|
+
].freeze
|
125
|
+
|
126
|
+
# @abstract Subclasses should provide this in order to fetch the results
|
127
|
+
# of the query.
|
72
128
|
def result
|
73
129
|
raise NotImplementedError
|
74
130
|
end
|
75
131
|
|
132
|
+
def initialize(store)
|
133
|
+
@store = store
|
134
|
+
end
|
135
|
+
|
136
|
+
# @abstract Subclasses can either override this method to properly respond
|
137
|
+
# to find_by query objects, or they can define a method for each supported
|
138
|
+
# operator. Ex. `#eq`, `#ne`, `#gt`.
|
139
|
+
def apply_operator(operator, field, expected, context = nil)
|
140
|
+
respond_to?(operator) ||
|
141
|
+
raise(ArgumentError, "Operator not implemented: #{operator}")
|
142
|
+
|
143
|
+
public_send(operator, field, expected, context)
|
144
|
+
end
|
145
|
+
|
146
|
+
# Called with a filter object by {Base#find_by} in order to apply the filter.
|
76
147
|
def apply(filter, context = nil)
|
77
148
|
filter.reduce(self) do |query, (field, value)|
|
78
149
|
if value.is_a?(Hash)
|
79
|
-
k = value.keys.first
|
80
|
-
|
81
|
-
|
150
|
+
if op?(k = value.keys.first)
|
151
|
+
query.apply_operator(k.to_sym, field.to_s, value[k], context)
|
152
|
+
else
|
153
|
+
query.nested_conditions(field, value, context)
|
154
|
+
end
|
82
155
|
else
|
83
|
-
query.eq
|
156
|
+
query.apply_operator(:eq, field.to_s, value)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
protected
|
162
|
+
|
163
|
+
# naive implementation recursively descends the graph to turns links into
|
164
|
+
# the actual entry data. This calls {Base#find} for each link and so it is
|
165
|
+
# very inefficient.
|
166
|
+
#
|
167
|
+
# @abstract Override this to provide a more efficient implementation for
|
168
|
+
# a given store.
|
169
|
+
def resolve_includes(entry, depth)
|
170
|
+
return entry unless entry && depth && depth > 0 && fields = entry['fields']
|
171
|
+
|
172
|
+
fields.each do |(_name, locales)|
|
173
|
+
# TODO: handle non-* locale
|
174
|
+
locales.each do |(locale, val)|
|
175
|
+
locales[locale] =
|
176
|
+
if val.is_a? Array
|
177
|
+
val.map { |e| resolve_link(e, depth) }
|
178
|
+
else
|
179
|
+
resolve_link(val, depth)
|
180
|
+
end
|
84
181
|
end
|
85
182
|
end
|
183
|
+
|
184
|
+
entry
|
185
|
+
end
|
186
|
+
|
187
|
+
def resolve_link(val, depth)
|
188
|
+
return val unless val.is_a?(Hash) && val.dig('sys', 'type') == 'Link'
|
189
|
+
return val unless included = @store.find(val.dig('sys', 'id'))
|
190
|
+
|
191
|
+
resolve_includes(included, depth - 1)
|
192
|
+
end
|
193
|
+
|
194
|
+
private
|
195
|
+
|
196
|
+
def op?(key)
|
197
|
+
OPERATORS.include?(key.to_sym)
|
198
|
+
end
|
199
|
+
|
200
|
+
def sys?(field)
|
201
|
+
field.to_s =~ /sys\./
|
202
|
+
end
|
203
|
+
|
204
|
+
def id?(field)
|
205
|
+
field.to_sym == :id
|
86
206
|
end
|
87
207
|
end
|
88
208
|
end
|