contentful_redis 0.0.1
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 +7 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +38 -0
- data/.travis.yml +11 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +94 -0
- data/LICENSE.txt +650 -0
- data/README.md +291 -0
- data/Rakefile +8 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/contentful-redis.gemspec +38 -0
- data/lib/contentful_redis/asset.rb +17 -0
- data/lib/contentful_redis/class_finder.rb +13 -0
- data/lib/contentful_redis/configuration.rb +12 -0
- data/lib/contentful_redis/error.rb +10 -0
- data/lib/contentful_redis/key_manager.rb +17 -0
- data/lib/contentful_redis/model_base.rb +150 -0
- data/lib/contentful_redis/request.rb +98 -0
- data/lib/contentful_redis.rb +26 -0
- metadata +190 -0
data/README.md
ADDED
@@ -0,0 +1,291 @@
|
|
1
|
+
# ContentfulRedis
|
2
|
+
A lightweight read-only contentful API wrapper which caches your responses in Redis.
|
3
|
+
|
4
|
+
# Features
|
5
|
+
- Lightweight easy to configure ruby contentful integration.
|
6
|
+
- Faster load times due to having a Redis cache.
|
7
|
+
- All content models responses are cached.
|
8
|
+
- Webhooks update
|
9
|
+
- Multiple space support
|
10
|
+
- Preview and production API support on a single environment
|
11
|
+
|
12
|
+
## WIP
|
13
|
+
- Migrate tests
|
14
|
+
- logger
|
15
|
+
- Experiment Redis size optimization
|
16
|
+
- auto clean up of dead Redis keys
|
17
|
+
- code clean up
|
18
|
+
|
19
|
+
ContentfulRedis also supports multiple API endpoints(preview and published) within a single application.
|
20
|
+
|
21
|
+
## Installation
|
22
|
+
Add this line to your application's Gemfile:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
gem 'redis-store'
|
26
|
+
gem 'contentful_redis'
|
27
|
+
```
|
28
|
+
|
29
|
+
And then execute:
|
30
|
+
|
31
|
+
$ bundle
|
32
|
+
|
33
|
+
Or install it yourself as:
|
34
|
+
|
35
|
+
$ gem install contentful_redis
|
36
|
+
|
37
|
+
## Configuration
|
38
|
+
|
39
|
+
Heres a default example, however, I will go over all of the individual configurations options below
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
# config/initializers/contentful_redis.rb
|
43
|
+
ContentfulRedis.configure do |config|
|
44
|
+
config.default_env = :preview # unless production env
|
45
|
+
config.model_scope = 'Contentful' # models live in a Contentful module
|
46
|
+
config.logging = true
|
47
|
+
|
48
|
+
config.spaces = {
|
49
|
+
test_space: {
|
50
|
+
id: 'xxxx',
|
51
|
+
access_token: 'xxxx',
|
52
|
+
preview_access_token: 'xxxx'
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
config.redis = Redis::Store.new(
|
57
|
+
host: (ENV['REDIS_HOST']) || 'localhost',
|
58
|
+
port: 6379,
|
59
|
+
db: 1,
|
60
|
+
namespace: 'contentful'
|
61
|
+
)
|
62
|
+
|
63
|
+
```
|
64
|
+
|
65
|
+
### Spaces (required)
|
66
|
+
Contentful Redis supports multiple space configurations with your first space being the default
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
# config/initializers/contentful_redis.rb
|
70
|
+
ContentfulRedis.configure do |config|
|
71
|
+
config.spaces = {
|
72
|
+
test_space: {
|
73
|
+
id: 'xxxx',
|
74
|
+
access_token: 'xxxx',
|
75
|
+
preview_access_token: 'xxxx'
|
76
|
+
},
|
77
|
+
|
78
|
+
test_space_2: {
|
79
|
+
id: 'xxxy',
|
80
|
+
access_token: 'xxxx',
|
81
|
+
preview_access_token: 'xxxx'
|
82
|
+
}
|
83
|
+
}
|
84
|
+
```
|
85
|
+
|
86
|
+
To use a different space for a model override the classes `#space` method
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
# app/models/my_model.rb
|
90
|
+
class MyModel < ContentfulRedis::ModelBase
|
91
|
+
|
92
|
+
# override default space
|
93
|
+
def self.space
|
94
|
+
ContentfulRedis.configuration.spaces[:test_space_2]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
```
|
98
|
+
|
99
|
+
### Redis (required)
|
100
|
+
There are various ways you can integrate with Redis.
|
101
|
+
I suggest using [redis-store](https://github.com/redis-store/redis-store) unless your application already has Redis adapter installed.
|
102
|
+
I recommend having a separate Redis database for all of your contentful data so that you can isolate your application Redis from your content.
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
# config/initializers/contentful_redis.rb
|
106
|
+
ContentfulRedis.configure do |config|
|
107
|
+
config.redis = Redis::Store.new(
|
108
|
+
host: (ENV['REDIS_HOST']) || 'localhost',
|
109
|
+
port: 6379,
|
110
|
+
db: 1,
|
111
|
+
namespace: 'contentful'
|
112
|
+
)
|
113
|
+
end
|
114
|
+
```
|
115
|
+
|
116
|
+
### Default env
|
117
|
+
|
118
|
+
If unset the default call is to the `:published` data. however, setting default_env to `:preview` will request to the preview API.
|
119
|
+
The Find methods can have an additional argument to force the non-default endpoint.
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
# config/initializers/contentful_redis.rb
|
123
|
+
ContentfulRedis.configure do |config|
|
124
|
+
# if unset defaults to :published
|
125
|
+
config.default_env = :preview
|
126
|
+
end
|
127
|
+
```
|
128
|
+
|
129
|
+
### Model scope
|
130
|
+
Set the scope for where your models live.
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
# config/initializers/contentful_redis.rb
|
134
|
+
ContentfulRedis.configure do |config|
|
135
|
+
config.model_scope = 'Contentful'
|
136
|
+
end
|
137
|
+
|
138
|
+
# app/models/contentful/page.rb
|
139
|
+
module Contentful
|
140
|
+
class Page < ContentfulRedis::ModelBase
|
141
|
+
|
142
|
+
end
|
143
|
+
end
|
144
|
+
```
|
145
|
+
|
146
|
+
## Models
|
147
|
+
All content models will need to be defined, prior to integration especially when using references.
|
148
|
+
The example model we are going to define has a slug(input field) and a body(references other content models)
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
# app/models/page.rb
|
152
|
+
class Page < ContentfulRedis::ModelBase
|
153
|
+
# allows the field to be queried from
|
154
|
+
define_searchable_fields :slug
|
155
|
+
|
156
|
+
# Set default readers which can return nil
|
157
|
+
attr_reader: :slug
|
158
|
+
|
159
|
+
# define your desired return types manually
|
160
|
+
def body
|
161
|
+
@body || []
|
162
|
+
end
|
163
|
+
end
|
164
|
+
```
|
165
|
+
|
166
|
+
### Querying
|
167
|
+
|
168
|
+
All content models are found by their contentful ID. Contentful Redis only stores only one cache of the content model
|
169
|
+
This Redis key is generated and is unique to a content model, space and endpoint.
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
Contentful::Page.find('<contentful_uid>')
|
173
|
+
```
|
174
|
+
|
175
|
+
Contentful Redis does not store a duplicate object from searchable attributes,
|
176
|
+
Instead, it builds a glossary of searchable attributes mapping to their content models ids.
|
177
|
+
These attributes are defined in the class declaration as `define_searchable_fields :slug`
|
178
|
+
|
179
|
+
```ruby
|
180
|
+
Contentful::Page.find_by(slug: 'about-us')
|
181
|
+
```
|
182
|
+
|
183
|
+
### Content model overriding
|
184
|
+
|
185
|
+
Classes should match their content model name, however, if they don't you can override the classes `#name` method.
|
186
|
+
|
187
|
+
```ruby
|
188
|
+
# app/models/page.rb
|
189
|
+
class Page < ContentfulRedis::ModelBase
|
190
|
+
|
191
|
+
# Overwrite to match contentful model using ruby class syntax
|
192
|
+
def self.name
|
193
|
+
'NameThatMatchesContentfulModel'
|
194
|
+
end
|
195
|
+
end
|
196
|
+
```
|
197
|
+
|
198
|
+
## Webhooks
|
199
|
+
|
200
|
+
Instead of creating rails specific implementation it is up to the developers to create your controllers and manage your webhook into your applications.
|
201
|
+
See the [Contentful webhooks docs](https://www.contentful.com/developers/docs/concepts/webhooks/) creating your own
|
202
|
+
|
203
|
+
Examples below will get you started!
|
204
|
+
|
205
|
+
Required Contentful webhooks to update the Redis cache are:
|
206
|
+
```json
|
207
|
+
{
|
208
|
+
"id": "{ /payload/sys/id }",
|
209
|
+
"environment": "{ /payload/sys/environment/sys/id }",
|
210
|
+
"model": "{ /payload/sys/contentType/sys/id }"
|
211
|
+
}
|
212
|
+
```
|
213
|
+
|
214
|
+
When pushing text attributes make sure you are using the correct language endpoint.
|
215
|
+
```json
|
216
|
+
{
|
217
|
+
"title": "{ /payload/fields/title/en-US }",
|
218
|
+
"slug": "{ /payload/fields/slug/en-US }",
|
219
|
+
}
|
220
|
+
```
|
221
|
+
|
222
|
+
### Webhook Controllers
|
223
|
+
|
224
|
+
#### Rails
|
225
|
+
```ruby
|
226
|
+
# app/controllers/contentful/webhook_controller.rb
|
227
|
+
module Contentful
|
228
|
+
class WebhookController < ApplicationController
|
229
|
+
# before_action :some_auth_layer
|
230
|
+
|
231
|
+
def update
|
232
|
+
payload = JSON.parse request.raw_post
|
233
|
+
|
234
|
+
contentful_model = ContentfulRedis::ClassFinder.search(payload['model'])
|
235
|
+
contentful_model.update(payload['id'])
|
236
|
+
|
237
|
+
render json: { status: :ok }
|
238
|
+
end
|
239
|
+
|
240
|
+
def delete
|
241
|
+
payload = JSON.parse request.raw_post
|
242
|
+
|
243
|
+
contentful_model = ContentfulRedis::ClassFinder.search(payload['model'])
|
244
|
+
contentful_model.destroy(payload['id'])
|
245
|
+
|
246
|
+
render json: { status: :ok }
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
# config/routes
|
252
|
+
#...
|
253
|
+
namespace :contentful do
|
254
|
+
resource 'webhooks', only: :update, :delete
|
255
|
+
end
|
256
|
+
# ...
|
257
|
+
```
|
258
|
+
|
259
|
+
#### Other
|
260
|
+
Feel free to create a PR for other ruby frameworks :)
|
261
|
+
|
262
|
+
## Content Seeding
|
263
|
+
Seeding the data is a great way to get started in building your content models, there is a couple of ways this can be done.
|
264
|
+
|
265
|
+
Create a service object inside your application and get it to fetch the root pages of your content tree by their ID.
|
266
|
+
The find method will build your Redis cache as well as link your content models with their searchable fields.
|
267
|
+
|
268
|
+
```ruby
|
269
|
+
# app/services/seed_content.rb
|
270
|
+
class SeedContent
|
271
|
+
# trigger a cascading content model seeding process
|
272
|
+
def call
|
273
|
+
['xxContentfulModelIdxx'].each do |page|
|
274
|
+
Contentful::Page.find(page)
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
```
|
279
|
+
|
280
|
+
## Development
|
281
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
282
|
+
|
283
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a Git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
284
|
+
|
285
|
+
## Contributing
|
286
|
+
|
287
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/contentful-redis.
|
288
|
+
|
289
|
+
## License
|
290
|
+
|
291
|
+
The gem is available as open source under the terms of the [GNU General Public License](https://www.gnu.org/licenses/#GPL)
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "contentful_redis/rb"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rubocop:disable Metrics/BlockLength
|
4
|
+
lib = File.expand_path('lib', __dir__)
|
5
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
6
|
+
require 'contentful_redis'
|
7
|
+
|
8
|
+
Gem::Specification.new do |spec|
|
9
|
+
spec.name = 'contentful_redis'
|
10
|
+
spec.version = ContentfulRedis::VERSION
|
11
|
+
spec.authors = ['DanHenton']
|
12
|
+
spec.email = ['Dan.Henton@gmail.com']
|
13
|
+
|
14
|
+
spec.summary = 'Contentful api wrapper which caches responses from contentful'
|
15
|
+
spec.homepage = 'https://github.com/DigitalNZ/contentful-redis'
|
16
|
+
spec.license = 'GNU'
|
17
|
+
|
18
|
+
# Specify which files should be added to the gem when it is released.
|
19
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
20
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
21
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
22
|
+
end
|
23
|
+
spec.bindir = 'exe'
|
24
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
25
|
+
spec.require_paths = ['lib']
|
26
|
+
|
27
|
+
spec.add_dependency 'faraday'
|
28
|
+
spec.add_dependency 'redis-store'
|
29
|
+
|
30
|
+
spec.add_development_dependency 'bundler'
|
31
|
+
spec.add_development_dependency 'factory_bot'
|
32
|
+
spec.add_development_dependency 'pry'
|
33
|
+
spec.add_development_dependency 'rake'
|
34
|
+
spec.add_development_dependency 'rspec'
|
35
|
+
spec.add_development_dependency 'rubocop'
|
36
|
+
spec.add_development_dependency 'webmock'
|
37
|
+
end
|
38
|
+
# rubocop:enable Metrics/BlockLength
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ContentfulRedis
|
4
|
+
class Asset
|
5
|
+
attr_reader :id, :title, :description, :url, :details, :file_name, :content_type
|
6
|
+
|
7
|
+
def initialize(model)
|
8
|
+
@id = model.dig('sys', 'id')
|
9
|
+
@title = model.dig('fields', 'title')
|
10
|
+
@description = model.dig('fields', 'description')
|
11
|
+
@url = model.dig('fields', 'file', 'url')
|
12
|
+
@details = model.dig('fields', 'file', 'details')
|
13
|
+
@file_name = model.dig('fields', 'file', 'fileName')
|
14
|
+
@content_type = model.dig('fields', 'file', 'contentType')
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ContentfulRedis
|
4
|
+
module ClassFinder
|
5
|
+
def self.search(type)
|
6
|
+
begin
|
7
|
+
"#{ContentfulRedis.configuration.model_scope}#{type.classify}".constantize
|
8
|
+
rescue NameError => _e
|
9
|
+
raise ContentfulRedis::Error::ClassNotFound, "Content type: #{type} is undefined"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ContentfulRedis
|
4
|
+
module Error
|
5
|
+
class ArgumentError < StandardError; end
|
6
|
+
class RecordNotFound < StandardError; end
|
7
|
+
class ClassNotFound < StandardError; end
|
8
|
+
class InternalServerError < StandardError; end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ContentfulRedis
|
4
|
+
module KeyManager
|
5
|
+
class << self
|
6
|
+
# Links a contentful models attribute to its contentful_id
|
7
|
+
def attribute_index(klass, attribute)
|
8
|
+
"#{klass.space.fetch(:space_id)}/#{klass.content_model}/#{attribute}"
|
9
|
+
end
|
10
|
+
|
11
|
+
# Links content model request to its contentful json response
|
12
|
+
def content_model_key(space, endpoint, parameters)
|
13
|
+
"#{space.fetch(:space_id)}/#{endpoint}/#{parameters.map { |k, v| "#{k}-#{v}" }.join('/')}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'asset'
|
4
|
+
require_relative 'request'
|
5
|
+
require_relative 'key_manager'
|
6
|
+
require_relative 'error'
|
7
|
+
require_relative 'class_finder'
|
8
|
+
|
9
|
+
# Base class for contentful redis intergation.
|
10
|
+
module ContentfulRedis
|
11
|
+
class ModelBase
|
12
|
+
class << self
|
13
|
+
def find(id, env = nil)
|
14
|
+
parameters = { 'sys.id': id, content_type: content_model }
|
15
|
+
|
16
|
+
new(ContentfulRedis::Request.new(space, parameters, :get, request_env(env)).call)
|
17
|
+
end
|
18
|
+
|
19
|
+
def find_by(args, env = ContentfulRedis.configuration.default_env || :published)
|
20
|
+
raise ContentfulRedis::Error::ArgumentError, "#{args} contain fields which are not a declared as a searchable field" unless (args.keys - searchable_fields).empty?
|
21
|
+
|
22
|
+
id = args.values.map do |value|
|
23
|
+
key = ContentfulRedis::KeyManager.attribute_index(self, value)
|
24
|
+
key.nil? || key.empty? ? nil : ContentfulRedis.redis.get(key)
|
25
|
+
end.compact.first
|
26
|
+
|
27
|
+
raise ContentfulRedis::Error::RecordNotFound, 'Missing attribute in glossary' if id.nil?
|
28
|
+
|
29
|
+
find(id, env)
|
30
|
+
end
|
31
|
+
|
32
|
+
def update(id, env = nil)
|
33
|
+
parameters = { 'sys.id': id, content_type: content_model }
|
34
|
+
|
35
|
+
new(ContentfulRedis::Request.new(space, parameters, :update, request_env(env)).call)
|
36
|
+
end
|
37
|
+
|
38
|
+
def destroy(id, env = nil)
|
39
|
+
keys = []
|
40
|
+
keys << ContentfulRedis::KeyManager.content_model_key(space, request_env(env), 'sys.id': id, content_type: content_model)
|
41
|
+
|
42
|
+
searchable_fields.each do |field|
|
43
|
+
keys << ContentfulRedis::KeyManager.attribute_index(self, field)
|
44
|
+
end
|
45
|
+
|
46
|
+
ContentfulRedis.redis.del(*keys)
|
47
|
+
end
|
48
|
+
|
49
|
+
def space
|
50
|
+
ContentfulRedis.configuration.spaces.first[1]
|
51
|
+
end
|
52
|
+
|
53
|
+
def content_model
|
54
|
+
model_name = name.demodulize
|
55
|
+
|
56
|
+
"#{model_name[0].downcase}#{model_name[1..-1]}"
|
57
|
+
end
|
58
|
+
|
59
|
+
def searchable_fields
|
60
|
+
[]
|
61
|
+
end
|
62
|
+
|
63
|
+
def define_searchable_fields(*fields)
|
64
|
+
instance_eval("def searchable_fields; #{fields}; end")
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def request_env(env)
|
70
|
+
env || ContentfulRedis.configuration.default_env || :published
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def initialize(model)
|
75
|
+
instance_variable_set(:@id, model['items'].first.dig('sys', 'id'))
|
76
|
+
self.class.send(:attr_reader, :id)
|
77
|
+
|
78
|
+
entries = entries_as_objects(model)
|
79
|
+
|
80
|
+
model['items'].first['fields'].each do |key, value|
|
81
|
+
value = case value
|
82
|
+
when Array
|
83
|
+
value.map { |val| entries[val.dig('sys', 'id')] || val }
|
84
|
+
when Hash
|
85
|
+
extract_object_from_hash(model, value, entries)
|
86
|
+
else
|
87
|
+
value
|
88
|
+
end
|
89
|
+
|
90
|
+
instance_variable_set("@#{key.underscore}", value)
|
91
|
+
end
|
92
|
+
|
93
|
+
create_searchable_attribute_links if self.class.searchable_fields.any?
|
94
|
+
end
|
95
|
+
|
96
|
+
def content_type
|
97
|
+
self.class.name.demodulize.underscore
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def entries_as_objects(model)
|
103
|
+
entries = model.dig('includes', 'Entry')
|
104
|
+
|
105
|
+
return {} if entries.nil? || entries.empty?
|
106
|
+
|
107
|
+
entries.each_with_object({}) do |entry, hash|
|
108
|
+
type = entry.dig('sys', 'contentType', 'sys', 'id')
|
109
|
+
id = entry.dig('sys', 'id')
|
110
|
+
|
111
|
+
hash[id] = ContentfulRedis::ClassFinder.search(type).find(id)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def extract_object_from_hash(model, value, entries)
|
116
|
+
entry_id = value.dig('sys', 'id')
|
117
|
+
|
118
|
+
assets = model.dig('includes', 'Asset')
|
119
|
+
asset = if !assets.nil? && assets.is_a?(Array)
|
120
|
+
model.dig('includes', 'Asset').first
|
121
|
+
end
|
122
|
+
|
123
|
+
if entries.key?(entry_id)
|
124
|
+
entries[entry_id]
|
125
|
+
elsif !asset.nil?
|
126
|
+
ContentfulRedis::Asset.new(asset)
|
127
|
+
else
|
128
|
+
value
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def create_searchable_attribute_links
|
133
|
+
self.class.searchable_fields.each do |field|
|
134
|
+
begin
|
135
|
+
instance_attribute = send(field)
|
136
|
+
rescue NoMethodError => _e
|
137
|
+
raise ContentfulRedis::Error::ArgumentError, "Undefined attribute: #{field} when creating attribute glossary"
|
138
|
+
end
|
139
|
+
|
140
|
+
raise ContentfulRedis::Error::ArgumentError, 'Searchable fields cannot be blank and must be required' if instance_attribute.nil?
|
141
|
+
raise ContentfulRedis::Error::ArgumentError, 'Searchable fields must be singular and cannot be references' if instance_attribute.is_a?(Array)
|
142
|
+
|
143
|
+
key = ContentfulRedis::KeyManager.attribute_index(self.class, send(field))
|
144
|
+
next if ContentfulRedis.redis.exists(key)
|
145
|
+
|
146
|
+
ContentfulRedis.redis.set(key, id)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'key_manager'
|
4
|
+
require_relative 'error'
|
5
|
+
|
6
|
+
# Request from contentful api and store in redis.
|
7
|
+
# Atempt to fetch response from redis before requesting to the contentful api
|
8
|
+
module ContentfulRedis
|
9
|
+
class Request
|
10
|
+
def initialize(space, params, action = :get, env = ContentfulRedis.configuration.default_env || :published)
|
11
|
+
@space = space
|
12
|
+
@action = action
|
13
|
+
|
14
|
+
if env.to_s.downcase == 'published'
|
15
|
+
@endpoint = 'cdn'
|
16
|
+
@access_token = @space[:access_token]
|
17
|
+
else
|
18
|
+
@endpoint = 'preview'
|
19
|
+
@access_token = @space[:preview_access_token]
|
20
|
+
end
|
21
|
+
|
22
|
+
params[:include] = 1
|
23
|
+
|
24
|
+
@parameters = params
|
25
|
+
end
|
26
|
+
|
27
|
+
def call
|
28
|
+
generated_key = ContentfulRedis::KeyManager.content_model_key(@space, @endpoint, @parameters)
|
29
|
+
|
30
|
+
return fetch_from_origin(generated_key) if @action == :update || !ContentfulRedis.redis.exists(generated_key)
|
31
|
+
|
32
|
+
JSON.parse(ContentfulRedis.redis.get(generated_key))
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def fetch_from_origin(generated_key)
|
38
|
+
response = perform_request
|
39
|
+
|
40
|
+
raise ContentfulRedis::Error::RecordNotFound, 'Contentful entry was not found' if response.match?(/"total":0/)
|
41
|
+
|
42
|
+
ContentfulRedis.redis.set(generated_key, response)
|
43
|
+
|
44
|
+
JSON.parse(response)
|
45
|
+
end
|
46
|
+
|
47
|
+
def perform_request
|
48
|
+
res = faraday_connection.get do |req|
|
49
|
+
req.url "https://#{@endpoint}.contentful.com/spaces/#{@space[:space_id]}/environments/master/entries"
|
50
|
+
|
51
|
+
req.params = @parameters
|
52
|
+
end
|
53
|
+
|
54
|
+
catch_errors(res)
|
55
|
+
|
56
|
+
# decompress then use JSON.parse to remove any blank characters to reduce bytesize
|
57
|
+
# Even when we ask for gzip encoding if content model is small contentfull wont gzib the response body
|
58
|
+
#
|
59
|
+
# Futher storage optimizations can be made to reduce the total redis size.
|
60
|
+
begin
|
61
|
+
JSON.parse(Zlib::GzipReader.new(StringIO.new(res.body)).read).to_json
|
62
|
+
rescue Zlib::GzipFile::Error
|
63
|
+
JSON.parse(res.body).to_json
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def faraday_connection
|
68
|
+
::Faraday.new do |faraday|
|
69
|
+
faraday.request :url_encoded
|
70
|
+
|
71
|
+
if ContentfulRedis.configuration.logging
|
72
|
+
faraday.response :logger do |logger|
|
73
|
+
logger.filter(/(Authorization:)(.*)/, '\1[REMOVED]')
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
faraday.adapter Faraday.default_adapter
|
78
|
+
faraday.headers = {
|
79
|
+
'Authorization': "Bearer #{@access_token}",
|
80
|
+
'Content-Type': 'application/vnd.contentful.delivery.v1+json',
|
81
|
+
'Accept-Encoding': 'gzip'
|
82
|
+
}
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def catch_errors(res)
|
87
|
+
send("__#{res.status.to_s[0]}00_error__") unless res.status == 200
|
88
|
+
end
|
89
|
+
|
90
|
+
def __400_error__
|
91
|
+
raise ContentfulRedis::Error::RecordNotFound, 'Contentful could not find the content entry'
|
92
|
+
end
|
93
|
+
|
94
|
+
def __500_error__
|
95
|
+
raise ContentfulRedis::Error::InternalServerError, 'An external Contentful error has occured'
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|