wcc-contentful 0.3.0 → 1.0.0.pre.rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. checksums.yaml +5 -5
  2. data/.rspec +1 -1
  3. data/Guardfile +43 -0
  4. data/README.md +161 -11
  5. data/Rakefile +3 -6
  6. data/app/controllers/wcc/contentful/webhook_controller.rb +25 -24
  7. data/app/jobs/wcc/contentful/webhook_enable_job.rb +36 -2
  8. data/bin/console +4 -3
  9. data/bin/rails +2 -0
  10. data/config/routes.rb +1 -1
  11. data/doc +1 -0
  12. data/lib/tasks/download_schema.rake +12 -0
  13. data/lib/wcc/contentful.rb +69 -45
  14. data/lib/wcc/contentful/active_record_shim.rb +72 -0
  15. data/lib/wcc/contentful/configuration.rb +177 -46
  16. data/lib/wcc/contentful/content_type_indexer.rb +14 -0
  17. data/lib/wcc/contentful/downloads_schema.rb +112 -0
  18. data/lib/wcc/contentful/engine.rb +33 -14
  19. data/lib/wcc/contentful/event.rb +171 -0
  20. data/lib/wcc/contentful/events.rb +41 -0
  21. data/lib/wcc/contentful/exceptions.rb +3 -33
  22. data/lib/wcc/contentful/indexed_representation.rb +2 -2
  23. data/lib/wcc/contentful/instrumentation.rb +31 -0
  24. data/lib/wcc/contentful/link.rb +28 -0
  25. data/lib/wcc/contentful/link_visitor.rb +122 -0
  26. data/lib/wcc/contentful/middleware.rb +7 -0
  27. data/lib/wcc/contentful/middleware/store.rb +158 -0
  28. data/lib/wcc/contentful/middleware/store/caching_middleware.rb +114 -0
  29. data/lib/wcc/contentful/model.rb +37 -4
  30. data/lib/wcc/contentful/model_builder.rb +1 -0
  31. data/lib/wcc/contentful/model_methods.rb +40 -15
  32. data/lib/wcc/contentful/model_singleton_methods.rb +47 -30
  33. data/lib/wcc/contentful/rake.rb +4 -0
  34. data/lib/wcc/contentful/rspec.rb +46 -0
  35. data/lib/wcc/contentful/services.rb +61 -27
  36. data/lib/wcc/contentful/simple_client.rb +81 -25
  37. data/lib/wcc/contentful/simple_client/management.rb +43 -10
  38. data/lib/wcc/contentful/simple_client/response.rb +61 -22
  39. data/lib/wcc/contentful/simple_client/typhoeus_adapter.rb +17 -17
  40. data/lib/wcc/contentful/store.rb +7 -66
  41. data/lib/wcc/contentful/store/README.md +85 -0
  42. data/lib/wcc/contentful/store/base.rb +34 -119
  43. data/lib/wcc/contentful/store/cdn_adapter.rb +71 -12
  44. data/lib/wcc/contentful/store/factory.rb +186 -0
  45. data/lib/wcc/contentful/store/instrumentation.rb +55 -0
  46. data/lib/wcc/contentful/store/interface.rb +82 -0
  47. data/lib/wcc/contentful/store/memory_store.rb +27 -24
  48. data/lib/wcc/contentful/store/postgres_store.rb +268 -101
  49. data/lib/wcc/contentful/store/postgres_store/schema_1.sql +73 -0
  50. data/lib/wcc/contentful/store/postgres_store/schema_2.sql +21 -0
  51. data/lib/wcc/contentful/store/query.rb +246 -0
  52. data/lib/wcc/contentful/store/query/interface.rb +63 -0
  53. data/lib/wcc/contentful/store/rspec_examples.rb +48 -0
  54. data/lib/wcc/contentful/store/rspec_examples/basic_store.rb +629 -0
  55. data/lib/wcc/contentful/store/rspec_examples/include_param.rb +283 -0
  56. data/lib/wcc/contentful/store/rspec_examples/nested_queries.rb +342 -0
  57. data/lib/wcc/contentful/sync_engine.rb +181 -0
  58. data/lib/wcc/contentful/test.rb +7 -0
  59. data/lib/wcc/contentful/test/attributes.rb +56 -0
  60. data/lib/wcc/contentful/test/double.rb +76 -0
  61. data/lib/wcc/contentful/test/factory.rb +101 -0
  62. data/lib/wcc/contentful/version.rb +1 -1
  63. data/wcc-contentful.gemspec +28 -14
  64. metadata +248 -152
  65. data/.circleci/config.yml +0 -51
  66. data/.gitignore +0 -26
  67. data/.rubocop.yml +0 -242
  68. data/.rubocop_todo.yml +0 -19
  69. data/.travis.yml +0 -5
  70. data/CHANGELOG.md +0 -180
  71. data/CODE_OF_CONDUCT.md +0 -74
  72. data/Gemfile +0 -8
  73. data/LICENSE.txt +0 -21
  74. data/app/jobs/wcc/contentful/delayed_sync_job.rb +0 -63
  75. data/lib/generators/wcc/USAGE +0 -24
  76. data/lib/generators/wcc/model_generator.rb +0 -90
  77. data/lib/generators/wcc/templates/.keep +0 -0
  78. data/lib/generators/wcc/templates/Procfile +0 -3
  79. data/lib/generators/wcc/templates/contentful_shell_wrapper +0 -385
  80. data/lib/generators/wcc/templates/menu/generated_add_menus.ts +0 -192
  81. data/lib/generators/wcc/templates/menu/models/menu.rb +0 -23
  82. data/lib/generators/wcc/templates/menu/models/menu_button.rb +0 -23
  83. data/lib/generators/wcc/templates/page/generated_add_pages.ts +0 -50
  84. data/lib/generators/wcc/templates/page/models/page.rb +0 -23
  85. data/lib/generators/wcc/templates/release +0 -9
  86. data/lib/generators/wcc/templates/wcc_contentful.rb +0 -17
  87. data/lib/wcc/contentful/client_ext.rb +0 -28
  88. data/lib/wcc/contentful/graphql.rb +0 -14
  89. data/lib/wcc/contentful/graphql/builder.rb +0 -177
  90. data/lib/wcc/contentful/graphql/types.rb +0 -54
  91. data/lib/wcc/contentful/model/dropdown_menu.rb +0 -7
  92. data/lib/wcc/contentful/model/menu.rb +0 -6
  93. data/lib/wcc/contentful/model/menu_button.rb +0 -16
  94. data/lib/wcc/contentful/model/page.rb +0 -8
  95. data/lib/wcc/contentful/model/redirect.rb +0 -19
  96. data/lib/wcc/contentful/model_validators.rb +0 -121
  97. data/lib/wcc/contentful/model_validators/dsl.rb +0 -166
  98. data/lib/wcc/contentful/simple_client/http_adapter.rb +0 -24
  99. data/lib/wcc/contentful/store/lazy_cache_store.rb +0 -161
@@ -3,15 +3,15 @@
3
3
  gem 'typhoeus'
4
4
  require 'typhoeus'
5
5
 
6
- class TyphoeusAdapter
7
- def call(url, query, headers = {}, proxy = {})
8
- raise NotImplementedError, 'Proxying Not Yet Implemented' if proxy[:host]
9
-
10
- TyphoeusAdapter::Response.new(
6
+ class WCC::Contentful::SimpleClient::TyphoeusAdapter
7
+ def get(url, params = {}, headers = {})
8
+ req = OpenStruct.new(params: params, headers: headers)
9
+ yield req if block_given?
10
+ Response.new(
11
11
  Typhoeus.get(
12
12
  url,
13
- params: query,
14
- headers: headers
13
+ params: req.params,
14
+ headers: req.headers
15
15
  )
16
16
  )
17
17
  end
@@ -19,7 +19,7 @@ class TyphoeusAdapter
19
19
  def post(url, body, headers = {}, proxy = {})
20
20
  raise NotImplementedError, 'Proxying Not Yet Implemented' if proxy[:host]
21
21
 
22
- TyphoeusAdapter::Response.new(
22
+ Response.new(
23
23
  Typhoeus.post(
24
24
  url,
25
25
  body: body.to_json,
@@ -28,15 +28,15 @@ class TyphoeusAdapter
28
28
  )
29
29
  end
30
30
 
31
- Response =
32
- Struct.new(:raw) do
33
- delegate :body, to: :raw
34
- delegate :to_s, to: :body
35
- delegate :code, to: :raw
36
- delegate :headers, to: :raw
31
+ class Response < SimpleDelegator
32
+ delegate :to_s, to: :body
37
33
 
38
- def status
39
- raw.code
40
- end
34
+ def raw
35
+ __getobj__
41
36
  end
37
+
38
+ def status
39
+ code
40
+ end
41
+ end
42
42
  end
@@ -1,9 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'store/base'
4
- require_relative 'store/memory_store'
5
- require_relative 'store/cdn_adapter'
6
- require_relative 'store/lazy_cache_store'
3
+ require_relative 'store/factory'
7
4
 
8
5
  # The "Store" is the middle layer in the WCC::Contentful gem. It exposes an API
9
6
  # that implements the configured content delivery strategy.
@@ -18,84 +15,28 @@ require_relative 'store/lazy_cache_store'
18
15
  #
19
16
  # lazy_sync:: Uses the Contentful CDN in combination with an ActiveSupport::Cache
20
17
  # 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.
18
+ # saving your CDN quota. The cache is kept up-to-date via the Sync Engine
19
+ # and the WCC::Contentful::SyncEngine::Job. It is correct, but not complete.
23
20
  #
24
21
  # eager_sync:: Uses one of the full store implementations to store the entirety
25
22
  # 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.
23
+ # local copy, which is kept up to date via the Sync Engine and the
24
+ # WCC::Contentful::SyncEngine::Job. The local store is correct and complete.
28
25
  #
29
26
  # The currently configured store is available on WCC::Contentful::Services.instance.store
30
27
  module WCC::Contentful::Store
31
28
  SYNC_STORES = {
32
- memory: ->(_config) { WCC::Contentful::Store::MemoryStore.new },
29
+ memory: ->(_config, *_options) { WCC::Contentful::Store::MemoryStore.new },
33
30
  postgres: ->(config, *options) {
34
31
  require_relative 'store/postgres_store'
35
32
  WCC::Contentful::Store::PostgresStore.new(config, *options)
36
33
  }
37
34
  }.freeze
38
35
 
39
- CDN_METHODS = %i[
36
+ PRESETS = %i[
40
37
  eager_sync
41
38
  lazy_sync
42
39
  direct
43
40
  custom
44
41
  ].freeze
45
-
46
- Factory =
47
- Struct.new(:config, :services, :cdn_method, :content_delivery_params) do
48
- def build_sync_store
49
- unless respond_to?("build_#{cdn_method}")
50
- raise ArgumentError, "Don't know how to build content delivery method #{cdn_method}"
51
- end
52
-
53
- public_send("build_#{cdn_method}", config, *content_delivery_params)
54
- end
55
-
56
- def validate!
57
- unless CDN_METHODS.include?(cdn_method)
58
- raise ArgumentError, "Please use one of #{CDN_METHODS} instead of #{cdn_method}"
59
- end
60
-
61
- return unless respond_to?("validate_#{cdn_method}")
62
-
63
- public_send("validate_#{cdn_method}", config, *content_delivery_params)
64
- end
65
-
66
- def build_eager_sync(config, store = nil, *options)
67
- store = SYNC_STORES[store].call(config, *options) if store.is_a?(Symbol)
68
- store || MemoryStore.new
69
- end
70
-
71
- def build_lazy_sync(_config, *options)
72
- WCC::Contentful::Store::LazyCacheStore.new(
73
- services.client,
74
- cache: ActiveSupport::Cache.lookup_store(*options)
75
- )
76
- end
77
-
78
- def build_direct(_config, *options)
79
- if options.find { |array| array[:preview] == true }
80
- CDNAdapter.new(services.preview_client)
81
- else
82
- CDNAdapter.new(services.client)
83
- end
84
- end
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
-
93
- def validate_eager_sync(_config, store = nil, *_options)
94
- return unless store.is_a?(Symbol)
95
-
96
- return if SYNC_STORES.key?(store)
97
-
98
- raise ArgumentError, "Please use one of #{SYNC_STORES.keys}"
99
- end
100
- end
101
42
  end
@@ -0,0 +1,85 @@
1
+ # Store API
2
+
3
+ The Store layer is used by the Model API to access Contentful data in a raw form.
4
+ The Store layer returns entries as hashes parsed from JSON, conforming to the
5
+ object structure returned from the Contentful CDN.
6
+
7
+ ## Public Interface
8
+
9
+ See WCC::Contentful::Store::Interface in wcc/contentful/store/interface.rb
10
+
11
+ This is the interface consumed by higher layers, such as the Model and GraphQL
12
+ APIs. It implements the following methods:
13
+
14
+ * `index?` - Returns boolean true if the SyncEngine should call the store's `index(json)`
15
+ method with the results of a Sync.
16
+ * `index(json)` - Updates the store with the latest data. The JSON can be an Entry,
17
+ Asset, DeletedEntry, or DeletedAsset.
18
+ * `find(id)` - Finds an entry or asset by it's ID.
19
+ * `find_all(content_type:, options: nil)` - Returns a query object that can be
20
+ enumerated to lazily iterate over all values of a content type. Query conditions
21
+ can be applied on the query object before it is enumerated to restrict the result
22
+ set.
23
+ * `find_by(content_type:, filter: nil, options: nil)` - Returns the first entry
24
+ of the given content type which matches the filter.
25
+ Note: assets have the special content
26
+ type of `Asset` (capital A)
27
+
28
+ ## Implementing your own store
29
+
30
+ The most straightforward way to implement a store is to include the
31
+ WCC::Contentful::Store::Interface module and then override all the defined
32
+ methods. The easiest way however, is to inherit from WCC::Contentful::Store::Base
33
+ and override the `set`, `delete`, `find`, and `execute` methods.
34
+
35
+ Let's take a look at the MemoryStore for a simplistic example. The MemoryStore
36
+ stores entries in a simple Ruby hash keyed by entry ID. `set` is simply
37
+ assigning to the key in the hash and returning the old value. `delete` and `find`
38
+ are likewise simple. The only complex method is `execute`, because it powers the
39
+ `find_by` and `find_all` query methods.
40
+
41
+ The query passed in to `execute` is an instance of WCC::Contentful::Store::Query.
42
+ This object contains a set of WCC::Contentful::Store::Query::Condition structs.
43
+ Each struct is a tuple of `path`, `op`, and `expected`. `op` is one of
44
+ WCC::Contentful::Store::Query::Interface::OPERATORS, `path` is an array of fields
45
+ pointing to a value in the JSON representation of an entry, and `expected` is the
46
+ expected value that should be compared to the value selected by `path`.
47
+
48
+ Since the MemoryStore only implements the equality operator, it simply digs
49
+ out the value at the given path using `val = entry.dig(*condition.path)`
50
+ and compares it using Contentful's definition of equality:
51
+ ```rb
52
+ # For arrays, equality is defined as does the array include the expected value.
53
+ # See https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/array-equality-inequality
54
+ if val.is_a? Array
55
+ val.include?(condition.expected)
56
+ else
57
+ val == condition.expected
58
+ end
59
+ ```
60
+
61
+ ### RSpec shared examples
62
+
63
+ To ensure you have implemented all the appropriate behavior, there
64
+ are a set of rspec shared examples in wcc/contentful/store/rspec_examples.rb which
65
+ you can include in your specs for your store implementation. Let's look at
66
+ spec/wcc/contentful/store/memory_store_spec.rb to see how it's used:
67
+
68
+ ```rb
69
+ require 'wcc/contentful/store/rspec_examples'
70
+
71
+ RSpec.describe WCC::Contentful::Store::MemoryStore do
72
+ subject { WCC::Contentful::Store::MemoryStore.new }
73
+
74
+ it_behaves_like 'contentful store', {
75
+ # memory store does not support JOINs like `Player.find_by(team: { slug: 'dallas-cowboys' })
76
+ nested_queries: false,
77
+ # Memory store supports resolving includes, but it does so in the most naiive
78
+ # way possible (by recursing down the entry's links and calling #find on every one)
79
+ include_param: 0
80
+ }
81
+ ```
82
+
83
+ The hash passed to the shared examples describes the features that the store
84
+ supports. Any key not provided causes the specs to be given the 'pending'
85
+ attribute. You can disable a set of specs by providing `false` for that key.
@@ -1,20 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative './interface'
4
+
3
5
  # @api Store
4
6
  module WCC::Contentful::Store
5
7
  # This is the base class for stores which implement #index, and therefore
6
8
  # must be kept up-to-date via the Sync API.
7
- # @abstract At a minimum subclasses should override {#find}, {#find_all}, {#set},
9
+ # @abstract At a minimum subclasses should override {#find}, {#execute}, {#set},
8
10
  # and #{delete}. As an alternative to overriding set and delete, the subclass
9
11
  # can override {#index}. Index is called when a webhook triggers a sync, to
10
12
  # update the store.
13
+ #
14
+ # To implement a new store, you should include the rspec_examples in your rspec
15
+ # tests for the store. See spec/wcc/contentful/store/memory_store_spec.rb for
16
+ # an example.
11
17
  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.
15
- def find(_id)
16
- raise NotImplementedError, "#{self.class} does not implement #find"
17
- end
18
+ include WCC::Contentful::Store::Interface
18
19
 
19
20
  # Sets the value of the entry with the given ID in the store.
20
21
  # @abstract
@@ -28,6 +29,22 @@ module WCC::Contentful::Store
28
29
  raise NotImplementedError, "#{self.class} does not implement #delete"
29
30
  end
30
31
 
32
+ # Executes a WCC::Contentful::Store::Query object created by {#find_all} or
33
+ # {#find_by}. Implementations should override this to translate the query's
34
+ # conditions into a query against the datastore.
35
+ #
36
+ # For a very naiive implementation see WCC::Contentful::Store::MemoryStore#execute
37
+ # @abstract
38
+ def execute(_query)
39
+ raise NotImplementedError, "#{self.class} does not implement #execute"
40
+ end
41
+
42
+ # Returns true if this store can persist entries and assets which are
43
+ # retrieved from the sync API.
44
+ def index?
45
+ true
46
+ end
47
+
31
48
  # Processes a data point received via the Sync API. This can be a published
32
49
  # entry or asset, or a 'DeletedEntry' or 'DeletedAsset'. The default
33
50
  # implementation calls into #set and #delete to perform the appropriate
@@ -76,17 +93,19 @@ module WCC::Contentful::Store
76
93
 
77
94
  # Finds all entries of the given content type. A content type is required.
78
95
  #
79
- # @abstract Subclasses should implement this at a minimum to provide data
80
- # to the {WCC::Contentful::Model} API.
96
+ # Subclasses may override this to provide their own query implementation,
97
+ # or else override #execute to run the query after it has been parsed.
81
98
  # @param [String] content_type The ID of the content type to search for.
82
99
  # @param [Hash] options An optional set of additional parameters to the query
83
100
  # defining for example include depth. Not all store implementations respect all options.
84
101
  # @return [Query] A query object that exposes methods to apply filters
85
- # rubocop:disable Lint/UnusedMethodArgument
86
102
  def find_all(content_type:, options: nil)
87
- raise NotImplementedError, "#{self.class} does not implement find_all"
103
+ Query.new(
104
+ self,
105
+ content_type: content_type,
106
+ options: options
107
+ )
88
108
  end
89
- # rubocop:enable Lint/UnusedMethodArgument
90
109
 
91
110
  def initialize
92
111
  @mutex = Concurrent::ReentrantReadWriteLock.new
@@ -96,114 +115,10 @@ module WCC::Contentful::Store
96
115
  raise ArgumentError, 'Value must be a Hash' unless val.is_a?(Hash)
97
116
  end
98
117
 
99
- protected
118
+ private
100
119
 
101
120
  attr_reader :mutex
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.
106
- class Query
107
- delegate :first, to: :result
108
- delegate :map, to: :result
109
- delegate :count, to: :result
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.
128
- def result
129
- raise NotImplementedError
130
- end
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.
147
- def apply(filter, context = nil)
148
- filter.reduce(self) do |query, (field, value)|
149
- if value.is_a?(Hash)
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
155
- else
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
181
- end
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
206
- end
207
- end
208
121
  end
209
122
  end
123
+
124
+ require_relative './query'
@@ -2,13 +2,30 @@
2
2
 
3
3
  module WCC::Contentful::Store
4
4
  class CDNAdapter
5
- attr_reader :client
5
+ include WCC::Contentful::Store::Interface
6
+ # Note: CDNAdapter should not instrument store events cause it's not a store.
7
+
8
+ attr_writer :client, :preview_client
9
+
10
+ def client
11
+ @preview ? @preview_client : @client
12
+ end
13
+
14
+ # The CDNAdapter cannot index data coming back from the Sync API.
15
+ def index?
16
+ false
17
+ end
18
+
19
+ def index
20
+ raise NotImplementedError, 'Cannot put data to the CDN!'
21
+ end
6
22
 
7
23
  # Intentionally not implementing write methods
8
24
 
9
- def initialize(client)
25
+ def initialize(client = nil, preview: false)
10
26
  super()
11
27
  @client = client
28
+ @preview = preview
12
29
  end
13
30
 
14
31
  def find(key, hint: nil, **options)
@@ -37,39 +54,61 @@ module WCC::Contentful::Store
37
54
 
38
55
  def find_all(content_type:, options: nil)
39
56
  Query.new(
40
- store: self,
41
- client: @client,
57
+ self,
58
+ client: client,
42
59
  relation: { content_type: content_type },
43
60
  options: options
44
61
  )
45
62
  end
46
63
 
47
- class Query < Base::Query
64
+ class Query
65
+ include WCC::Contentful::Store::Query::Interface
66
+ include Enumerable
67
+
68
+ # by default all enumerable methods delegated to the lazy enumerable
69
+ delegate(*(Enumerable.instance_methods - Module.instance_methods), to: :to_enum)
70
+
71
+ # response.count gets the number of items
48
72
  delegate :count, to: :response
49
73
 
50
- def result
74
+ def to_enum
51
75
  return response.items unless @options[:include]
52
76
 
53
77
  response.items.map { |e| resolve_includes(e, @options[:include]) }
54
78
  end
55
79
 
56
- def initialize(store:, client:, relation:, options: nil, **extra)
80
+ def initialize(store, client:, relation:, options: nil, **extra)
57
81
  raise ArgumentError, 'Client cannot be nil' unless client.present?
58
82
  raise ArgumentError, 'content_type must be provided' unless relation[:content_type].present?
59
83
 
60
- super(store)
84
+ @store = store
61
85
  @client = client
62
86
  @relation = relation
63
87
  @options = options || {}
64
88
  @extra = extra || {}
65
89
  end
66
90
 
91
+ # Called with a filter object by {Base#find_by} in order to apply the filter.
92
+ def apply(filter, context = nil)
93
+ filter.reduce(self) do |query, (field, value)|
94
+ if value.is_a?(Hash)
95
+ if op?(k = value.keys.first)
96
+ query.apply_operator(k.to_sym, field.to_s, value[k], context)
97
+ else
98
+ query.nested_conditions(field, value, context)
99
+ end
100
+ else
101
+ query.apply_operator(:eq, field.to_s, value)
102
+ end
103
+ end
104
+ end
105
+
67
106
  def apply_operator(operator, field, expected, context = nil)
68
107
  op = operator == :eq ? nil : operator
69
108
  param = parameter(field, operator: op, context: context, locale: true)
70
109
 
71
110
  self.class.new(
72
- store: @store,
111
+ @store,
73
112
  client: @client,
74
113
  relation: @relation.merge(param => expected),
75
114
  options: @options,
@@ -85,7 +124,7 @@ module WCC::Contentful::Store
85
124
  end
86
125
  end
87
126
 
88
- Base::Query::OPERATORS.each do |op|
127
+ WCC::Contentful::Store::Query::Interface::OPERATORS.each do |op|
89
128
  define_method(op) do |field, expected, context = nil|
90
129
  apply_operator(op, field, expected, context)
91
130
  end
@@ -93,6 +132,18 @@ module WCC::Contentful::Store
93
132
 
94
133
  private
95
134
 
135
+ def op?(key)
136
+ WCC::Contentful::Store::Query::Interface::OPERATORS.include?(key.to_sym)
137
+ end
138
+
139
+ def sys?(field)
140
+ field.to_s =~ /sys\./
141
+ end
142
+
143
+ def id?(field)
144
+ field.to_sym == :id
145
+ end
146
+
96
147
  def response
97
148
  @response ||=
98
149
  if @relation[:content_type] == 'Asset'
@@ -104,11 +155,19 @@ module WCC::Contentful::Store
104
155
  end
105
156
  end
106
157
 
107
- def resolve_link(val, depth)
158
+ def resolve_includes(entry, depth)
159
+ return entry unless entry && depth && depth > 0
160
+
161
+ WCC::Contentful::LinkVisitor.new(entry, :Link, :Asset, depth: depth - 1).map! do |val|
162
+ resolve_link(val)
163
+ end
164
+ end
165
+
166
+ def resolve_link(val)
108
167
  return val unless val.is_a?(Hash) && val.dig('sys', 'type') == 'Link'
109
168
  return val unless included = response.includes[val.dig('sys', 'id')]
110
169
 
111
- resolve_includes(included, depth - 1)
170
+ included
112
171
  end
113
172
 
114
173
  def parameter(field, operator: nil, context: nil, locale: false)