wcc-contentful 1.3.2 → 1.4.0.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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +80 -14
  3. data/app/jobs/wcc/contentful/webhook_enable_job.rb +0 -1
  4. data/lib/wcc/contentful/configuration.rb +12 -5
  5. data/lib/wcc/contentful/downloads_schema.rb +14 -2
  6. data/lib/wcc/contentful/entry_locale_transformer.rb +107 -0
  7. data/lib/wcc/contentful/exceptions.rb +5 -0
  8. data/lib/wcc/contentful/link_visitor.rb +12 -1
  9. data/lib/wcc/contentful/middleware/store/caching_middleware.rb +34 -10
  10. data/lib/wcc/contentful/middleware/store/locale_middleware.rb +30 -0
  11. data/lib/wcc/contentful/middleware/store.rb +20 -16
  12. data/lib/wcc/contentful/model_builder.rb +10 -3
  13. data/lib/wcc/contentful/model_methods.rb +4 -6
  14. data/lib/wcc/contentful/simple_client/cdn.rb +5 -2
  15. data/lib/wcc/contentful/simple_client/management.rb +16 -0
  16. data/lib/wcc/contentful/simple_client.rb +1 -2
  17. data/lib/wcc/contentful/store/base.rb +6 -1
  18. data/lib/wcc/contentful/store/cdn_adapter.rb +13 -4
  19. data/lib/wcc/contentful/store/factory.rb +8 -1
  20. data/lib/wcc/contentful/store/memory_store.rb +27 -8
  21. data/lib/wcc/contentful/store/postgres_store.rb +4 -3
  22. data/lib/wcc/contentful/store/query/condition.rb +89 -0
  23. data/lib/wcc/contentful/store/query.rb +9 -35
  24. data/lib/wcc/contentful/store/rspec_examples/basic_store.rb +84 -12
  25. data/lib/wcc/contentful/store/rspec_examples/locale_queries.rb +220 -0
  26. data/lib/wcc/contentful/store/rspec_examples/operators/eq.rb +1 -1
  27. data/lib/wcc/contentful/store/rspec_examples.rb +13 -1
  28. data/lib/wcc/contentful/sync_engine.rb +1 -1
  29. data/lib/wcc/contentful/test/double.rb +1 -1
  30. data/lib/wcc/contentful/test/factory.rb +2 -4
  31. data/lib/wcc/contentful/version.rb +1 -1
  32. data/lib/wcc/contentful.rb +17 -6
  33. metadata +80 -48
@@ -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, sys.locale) }
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, sys.locale)) }
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. It also sets the fields to the value for the entry's `#sys.locale`,
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' => { 'locale' => @sys.locale }.merge!(@raw['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
- sync_token =
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
- query = query.merge(sync_token)
93
+ ).merge(query)
91
94
 
92
95
  _instrument 'sync', sync_token: sync_token, query: query do
93
96
  resp = get('sync', query)
@@ -34,6 +34,22 @@ class WCC::Contentful::SimpleClient::Management < WCC::Contentful::SimpleClient
34
34
  resp.assert_ok!
35
35
  end
36
36
 
37
+ def locales(**query)
38
+ resp =
39
+ _instrument 'locales', query: query do
40
+ get('locales', query)
41
+ end
42
+ resp.assert_ok!
43
+ end
44
+
45
+ def locale(key, query = {})
46
+ resp =
47
+ _instrument 'locales', content_type: key, query: query do
48
+ get("locales/#{key}", query)
49
+ end
50
+ resp.assert_ok!
51
+ end
52
+
37
53
  def editor_interface(content_type_id, query = {})
38
54
  resp =
39
55
  _instrument 'editor_interfaces', content_type: content_type_id, query: query do
@@ -36,7 +36,6 @@ module WCC::Contentful
36
36
  # @param [Hash] options The remaining optional parameters, defined below
37
37
  # @option options [Symbol, Object] connection The Faraday connection to use to make requests.
38
38
  # Auto-discovered based on what gems are installed if this is not provided.
39
- # @option options [String] default_locale The locale query param to set by default.
40
39
  # @option options [String] environment The contentful environment to access. Defaults to 'master'.
41
40
  # @option options [Boolean] no_follow_redirects If true, do not follow 300 level redirects.
42
41
  # @option options [Number] rate_limit_wait_timeout The maximum time to block the thread waiting
@@ -51,7 +50,6 @@ module WCC::Contentful
51
50
  @options = options
52
51
  @_instrumentation = @options[:instrumentation]
53
52
  @query_defaults = {}
54
- @query_defaults[:locale] = @options[:default_locale] if @options[:default_locale]
55
53
  # default 1.5 so that we retry one time then fail if still rate limited
56
54
  # https://www.contentful.com/developers/docs/references/content-preview-api/#/introduction/api-rate-limits
57
55
  @rate_limit_wait_timeout = @options[:rate_limit_wait_timeout] || 1.5
@@ -124,6 +122,7 @@ module WCC::Contentful
124
122
 
125
123
  q = @query_defaults.dup
126
124
  q = q.merge(query) if query
125
+ q.compact!
127
126
 
128
127
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
129
128
  loop do
@@ -17,6 +17,10 @@ module WCC::Contentful::Store
17
17
  class Base
18
18
  include WCC::Contentful::Store::Interface
19
19
 
20
+ def initialize(configuration = nil)
21
+ @configuration = configuration || WCC::Contentful.configuration
22
+ end
23
+
20
24
  # Sets the value of the entry with the given ID in the store.
21
25
  # @abstract
22
26
  def set(_id, _value)
@@ -103,7 +107,8 @@ module WCC::Contentful::Store
103
107
  Query.new(
104
108
  self,
105
109
  content_type: content_type,
106
- options: options
110
+ options: options,
111
+ configuration: @configuration
107
112
  )
108
113
  end
109
114
 
@@ -29,7 +29,7 @@ module WCC::Contentful::Store
29
29
  end
30
30
 
31
31
  def find(key, hint: nil, **options)
32
- options = { locale: '*' }.merge!(options || {})
32
+ options = options&.dup || {}
33
33
  entry =
34
34
  if hint
35
35
  client.public_send(hint.underscore, key, options)
@@ -114,7 +114,7 @@ module WCC::Contentful::Store
114
114
  op = :in if op.nil?
115
115
  end
116
116
 
117
- param = parameter(field, operator: op, context: context, locale: true)
117
+ param = parameter(field, operator: op, context: context, locale: false)
118
118
 
119
119
  self.class.new(
120
120
  @store,
@@ -157,10 +157,10 @@ module WCC::Contentful::Store
157
157
  @response ||=
158
158
  if @relation[:content_type] == 'Asset'
159
159
  @client.assets(
160
- { locale: '*' }.merge!(@relation.reject { |k| k == :content_type }).merge!(@options)
160
+ @relation.reject { |k| k == :content_type }.merge(@options)
161
161
  )
162
162
  else
163
- @client.entries({ locale: '*' }.merge!(@relation).merge!(@options))
163
+ @client.entries(@relation.merge(@options))
164
164
  end
165
165
  end
166
166
 
@@ -180,6 +180,15 @@ module WCC::Contentful::Store
180
180
  included
181
181
  end
182
182
 
183
+ # Constructs the CDN query parameter from a structured field definition and
184
+ # operator.
185
+ # Notes:
186
+ # * "eq" can be omitted, e.g. 'fields.slug=/' is equivalent to 'fields.slug[eq]=/'
187
+ # * If "locale" is specified in the query, matching is done against that locale,
188
+ # unless the query explicitly specifies the locale. Examples:
189
+ # 'locale=es-US&fields.title=página principal' matches on the es locale
190
+ # 'locale=en-US&fields.title=página principal' returns nothing
191
+ # 'locale=en-US&fields.title.es-US=página principal' returns the page, but in the english locale.
183
192
  def parameter(field, operator: nil, context: nil, locale: false)
184
193
  if sys?(field)
185
194
  "#{field}#{op_param(operator)}"
@@ -5,6 +5,7 @@ require_relative 'memory_store'
5
5
  require_relative 'cdn_adapter'
6
6
  require_relative '../middleware/store'
7
7
  require_relative '../middleware/store/caching_middleware'
8
+ require_relative '../middleware/store/locale_middleware'
8
9
 
9
10
  module WCC::Contentful::Store
10
11
  # This factory presents a DSL for configuring the store stack. The store stack
@@ -46,6 +47,8 @@ module WCC::Contentful::Store
46
47
  self.middleware << [middleware, middleware_params, configure_proc]
47
48
  end
48
49
 
50
+ # Replaces a middleware in the chain. The middleware to replace is selected
51
+ # by matching the class.
49
52
  def replace(middleware, *middleware_params, &block)
50
53
  idx = self.middleware.find_index { |m| m[0] == middleware }
51
54
  raise ArgumentError, "Middleware #{middleware} not present" if idx.nil?
@@ -54,6 +57,8 @@ module WCC::Contentful::Store
54
57
  self.middleware[idx] = [middleware, middleware_params, configure_proc]
55
58
  end
56
59
 
60
+ # Removes a middleware from the chain, finding it by matching the class
61
+ # constant.
57
62
  def unuse(middleware)
58
63
  idx = self.middleware.find_index { |m| m[0] == middleware }
59
64
  return if idx.nil?
@@ -174,7 +179,9 @@ module WCC::Contentful::Store
174
179
  # The middleware that by default lives at the top of the middleware stack.
175
180
  def default_middleware
176
181
  [
177
- [WCC::Contentful::Store::InstrumentationMiddleware]
182
+ [WCC::Contentful::Store::InstrumentationMiddleware],
183
+ # Stores do not guarantee that the entry is resolved to the locale
184
+ [WCC::Contentful::Middleware::Store::LocaleMiddleware]
178
185
  ].freeze
179
186
  end
180
187
  end
@@ -7,9 +7,12 @@ module WCC::Contentful::Store
7
7
  # point for more useful implementations. It only implements equality queries
8
8
  # and does not support querying through an association.
9
9
  class MemoryStore < Base
10
- def initialize
10
+ delegate :locale_fallbacks, to: :@configuration
11
+
12
+ def initialize(configuration = nil)
11
13
  super
12
14
 
15
+ @configuration = configuration
13
16
  @mutex = Concurrent::ReentrantReadWriteLock.new
14
17
  @hash = {}
15
18
  end
@@ -36,7 +39,7 @@ module WCC::Contentful::Store
36
39
 
37
40
  def find(key, **_options)
38
41
  @mutex.with_read_lock do
39
- @hash[key]
42
+ @hash[key].deep_dup
40
43
  end
41
44
  end
42
45
 
@@ -64,9 +67,12 @@ module WCC::Contentful::Store
64
67
 
65
68
  # For each condition, we apply a new Enumerable#select with a block that
66
69
  # enforces the condition.
67
- query.conditions.reduce(relation) do |memo, condition|
68
- __send__("apply_#{condition.op}", memo, condition)
69
- end
70
+ relation =
71
+ query.conditions.reduce(relation) do |memo, condition|
72
+ __send__("apply_#{condition.op}", memo, condition)
73
+ end
74
+
75
+ relation.map(&:deep_dup)
70
76
  end
71
77
 
72
78
  private
@@ -80,8 +86,7 @@ module WCC::Contentful::Store
80
86
  end
81
87
 
82
88
  def eq?(entry, condition)
83
- # The condition's path tells us where to find the value in the JSON object
84
- val = entry.dig(*condition.path)
89
+ val = select_value_for_compare(entry, condition)
85
90
 
86
91
  # For arrays, equality is defined as does the array include the expected value.
87
92
  # See https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/array-equality-inequality
@@ -101,7 +106,7 @@ module WCC::Contentful::Store
101
106
  end
102
107
 
103
108
  def in?(entry, condition)
104
- val = entry.dig(*condition.path)
109
+ val = select_value_for_compare(entry, condition)
105
110
 
106
111
  if val.is_a? Array
107
112
  # TODO: detect if in ruby 3.1 and use val.intersect?(condition.expected)
@@ -110,5 +115,19 @@ module WCC::Contentful::Store
110
115
  condition.expected.include?(val)
111
116
  end
112
117
  end
118
+
119
+ # Selects the value for the condition from the entry, taking into account locale fallbacks
120
+ def select_value_for_compare(entry, condition)
121
+ condition.each_locale_fallback do |cond|
122
+ # The condition's path tells us where to find the value in the JSON object
123
+ val = entry.dig(*cond.path)
124
+
125
+ # If the object has no value for this locale, try the fallbacks
126
+ next if val.nil?
127
+
128
+ # The object has a value for this locale, so we must compare against it and not the fallbacks
129
+ return val
130
+ end
131
+ end
113
132
  end
114
133
  end
@@ -17,8 +17,8 @@ module WCC::Contentful::Store
17
17
  attr_reader :connection_pool
18
18
  attr_accessor :logger
19
19
 
20
- def initialize(_config = nil, connection_options = nil, pool_options = nil)
21
- super()
20
+ def initialize(configuration = nil, connection_options = nil, pool_options = nil)
21
+ super(configuration)
22
22
  @schema_ensured = false
23
23
  connection_options ||= { dbname: 'postgres' }
24
24
  pool_options ||= {}
@@ -100,7 +100,8 @@ module WCC::Contentful::Store
100
100
  Query.new(
101
101
  self,
102
102
  content_type: content_type,
103
- options: options
103
+ options: options,
104
+ configuration: @configuration
104
105
  )
105
106
  end
106
107
 
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ class WCC::Contentful::Store::Query
4
+ Condition =
5
+ Struct.new(:path, :op, :expected, :locale_fallbacks) do
6
+ LINK_KEYS = %w[id type linkType].freeze # rubocop:disable Lint/ConstantDefinitionInBlock
7
+
8
+ ##
9
+ # Breaks the path into an array of tuples, where each tuple represents an
10
+ # entry subquery.
11
+ # If the query is a simple query on a field in an entry, there will be one
12
+ # tuple in the array:
13
+ # { 'title' => 'foo' } becomes
14
+ # [['fields', 'title', 'en-US']]
15
+ #
16
+ # If the query is a query through a link, there will be multiple tuples:
17
+ # { 'page' => { 'title' => 'foo' } } becomes
18
+ # [['fields', 'page', 'en-US'], ['fields', 'title', 'en-US']]
19
+ def path_tuples
20
+ return @path_tuples if @path_tuples
21
+
22
+ arr = []
23
+ remaining = path.dup
24
+ until remaining.empty?
25
+ locale = nil
26
+ link_sys = nil
27
+ link_field = nil
28
+
29
+ sys_or_fields = remaining.shift
30
+ field = remaining.shift
31
+ locale = remaining.shift if sys_or_fields == 'fields'
32
+
33
+ if remaining[0] == 'sys' && LINK_KEYS.include?(remaining[1])
34
+ link_sys = remaining.shift
35
+ link_field = remaining.shift
36
+ end
37
+
38
+ arr << [sys_or_fields, field, locale, link_sys, link_field].compact
39
+ end
40
+ @path_tuples = arr.freeze
41
+ end
42
+
43
+ ##
44
+ # Starting with the last part of the path that is a locale, iterates all the
45
+ # combinations of potential locale fallbacks.
46
+ # e.g. if the path is ['fields', 'page', 'es-MX', 'fields', 'title', 'es-MX']
47
+ # then we get:
48
+ # ['fields', 'page', 'es-MX', 'fields', 'title', 'es-MX'] (self)
49
+ # ['fields', 'page', 'es-MX', 'fields', 'title', 'es-US']
50
+ # ['fields', 'page', 'es-MX', 'fields', 'title', 'en-US']
51
+ # ['fields', 'page', 'es-US', 'fields', 'title', 'es-MX']
52
+ # ['fields', 'page', 'es-US', 'fields', 'title', 'es-US']
53
+ # ['fields', 'page', 'es-US', 'fields', 'title', 'en-US']
54
+ # ['fields', 'page', 'en-US', 'fields', 'title', 'es-MX']
55
+ # ['fields', 'page', 'en-US', 'fields', 'title', 'es-US']
56
+ # ['fields', 'page', 'en-US', 'fields', 'title', 'en-US']
57
+ def each_locale_fallback(&block)
58
+ return to_enum(:each_locale_fallback) unless block_given?
59
+
60
+ _each_locale_fallback(path_tuples, 0, &block)
61
+ end
62
+
63
+ private
64
+
65
+ # Find the next fallback tuples from this set of tuples
66
+ def _each_locale_fallback(original_tuples, start_at, &block)
67
+ tuples = original_tuples.deep_dup
68
+ varying = tuples[start_at]
69
+
70
+ if varying[2].nil?
71
+ # This is a non-localizable query, so just yield it
72
+ yield Condition.new(tuples.flatten, op, expected, locale_fallbacks)
73
+ return
74
+ end
75
+
76
+ while varying[2]
77
+ if tuples.length > start_at + 1
78
+ # There's more locales that we need to vary to the right, so recurse into those
79
+ _each_locale_fallback(tuples, start_at + 1, &block)
80
+ else
81
+ # We're the tail of the condition, so yield it.
82
+ yield Condition.new(tuples.flatten, op, expected, locale_fallbacks)
83
+ end
84
+
85
+ varying[2] = locale_fallbacks[varying[2]]
86
+ end
87
+ end
88
+ end
89
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative '../../contentful'
4
4
  require_relative './query/interface'
5
+ require_relative './query/condition'
5
6
 
6
7
  module WCC::Contentful::Store
7
8
  # The default query object returned by Stores that extend WCC::Contentful::Store::Base.
@@ -26,11 +27,12 @@ module WCC::Contentful::Store
26
27
 
27
28
  attr_reader :store, :content_type, :conditions
28
29
 
29
- def initialize(store, content_type:, conditions: nil, options: nil, **extra)
30
+ def initialize(store, content_type:, conditions: nil, options: nil, configuration: nil, **extra) # rubocop:disable Metrics/ParameterLists
30
31
  @store = store
31
32
  @content_type = content_type
32
33
  @conditions = conditions || []
33
34
  @options = options || {}
35
+ @configuration = configuration || WCC::Contentful.configuration
34
36
  @extra = extra
35
37
  end
36
38
 
@@ -60,7 +62,7 @@ module WCC::Contentful::Store
60
62
  # Can be an array, symbol, or dotted-notation path specification.
61
63
  # @expected The expected value to compare the field's value against.
62
64
  # @context A context object optionally containing `context[:locale]`
63
- def apply_operator(operator, field, expected, context = nil)
65
+ def apply_operator(operator, field, expected, _context = nil)
64
66
  operator ||= expected.is_a?(Array) ? :in : :eq
65
67
  raise ArgumentError, "Operator #{operator} not supported" unless respond_to?(operator)
66
68
  raise ArgumentError, 'value cannot be nil (try using exists: false)' if expected.nil?
@@ -75,10 +77,10 @@ module WCC::Contentful::Store
75
77
  field = field.to_s if field.is_a? Symbol
76
78
  path = field.is_a?(Array) ? field : field.split('.')
77
79
 
78
- path = self.class.normalize_condition_path(path, context)
80
+ path = self.class.normalize_condition_path(path, @options)
79
81
 
80
82
  _append_condition(
81
- Condition.new(path, operator, expected)
83
+ Condition.new(path, operator, expected, @configuration&.locale_fallbacks || {})
82
84
  )
83
85
  end
84
86
 
@@ -177,15 +179,15 @@ module WCC::Contentful::Store
177
179
  end
178
180
 
179
181
  def known_locales
180
- @known_locales = WCC::Contentful.locales.keys
182
+ @known_locales ||= WCC::Contentful.locales&.keys || ['en-US']
181
183
  end
182
184
  RESERVED_NAMES = %w[fields sys].freeze
183
185
 
184
186
  # Takes a path array in non-normal form and inserts 'sys', 'fields',
185
187
  # and the current locale as appropriate to normalize it.
186
188
  # rubocop:disable Metrics/BlockNesting
187
- def normalize_condition_path(path, context = nil)
188
- context_locale = context[:locale] if context.present?
189
+ def normalize_condition_path(path, options = nil)
190
+ context_locale = options[:locale]&.to_s if options.present?
189
191
  context_locale ||= 'en-US'
190
192
 
191
193
  rev_path = path.reverse
@@ -234,33 +236,5 @@ module WCC::Contentful::Store
234
236
  end
235
237
  # rubocop:enable Metrics/BlockNesting
236
238
  end
237
-
238
- Condition =
239
- Struct.new(:path, :op, :expected) do
240
- LINK_KEYS = %w[id type linkType].freeze # rubocop:disable Lint/ConstantDefinitionInBlock
241
-
242
- def path_tuples
243
- @path_tuples ||=
244
- [].tap do |arr|
245
- remaining = path.dup
246
- until remaining.empty?
247
- locale = nil
248
- link_sys = nil
249
- link_field = nil
250
-
251
- sys_or_fields = remaining.shift
252
- field = remaining.shift
253
- locale = remaining.shift if sys_or_fields == 'fields'
254
-
255
- if remaining[0] == 'sys' && LINK_KEYS.include?(remaining[1])
256
- link_sys = remaining.shift
257
- link_field = remaining.shift
258
- end
259
-
260
- arr << [sys_or_fields, field, locale, link_sys, link_field].compact
261
- end
262
- end
263
- end
264
- end
265
239
  end
266
240
  end
@@ -105,7 +105,7 @@ RSpec.shared_examples 'basic store' do
105
105
  "type": "Asset",
106
106
  "createdAt": "2018-02-12T19:53:39.309Z",
107
107
  "updatedAt": "2018-02-12T19:53:39.309Z",
108
- "revision": 1
108
+ "revision": 2
109
109
  },
110
110
  "fields": {
111
111
  "title": {
@@ -133,7 +133,10 @@ RSpec.shared_examples 'basic store' do
133
133
  describe '#set/#find' do
134
134
  describe 'ensures that the stored value is of type Hash' do
135
135
  it 'should not raise an error if value is a Hash' do
136
- data = { token: 'jenny_8675309' }
136
+ data = {
137
+ 'sys' => { 'id' => 'sync:token', 'type' => 'token' },
138
+ 'token' => 'state'
139
+ }
137
140
 
138
141
  # assert
139
142
  expect { subject.set('sync:token', data) }.to_not raise_error
@@ -146,7 +149,11 @@ RSpec.shared_examples 'basic store' do
146
149
  end
147
150
 
148
151
  it 'stores and finds data by ID' do
149
- data = { 'key' => 'val', '1' => { 'deep' => 9 } }
152
+ data = {
153
+ 'sys' => { 'id' => '1234' },
154
+ 'key' => 'val',
155
+ '1' => { 'deep' => 9 }
156
+ }
150
157
 
151
158
  # act
152
159
  subject.set('1234', data)
@@ -157,7 +164,11 @@ RSpec.shared_examples 'basic store' do
157
164
  end
158
165
 
159
166
  it 'find returns nil if key doesnt exist' do
160
- data = { 'key' => 'val', '1' => { 'deep' => 9 } }
167
+ data = {
168
+ 'sys' => { 'id' => '1234' },
169
+ 'key' => 'val',
170
+ '1' => { 'deep' => 9 }
171
+ }
161
172
  subject.set('1234', data)
162
173
 
163
174
  # act
@@ -181,8 +192,16 @@ RSpec.shared_examples 'basic store' do
181
192
  end
182
193
 
183
194
  it 'set returns prior value if exists' do
184
- data = { 'key' => 'val', '1' => { 'deep' => 9 } }
185
- data2 = { 'key' => 'val', '2' => { 'deep' => 11 } }
195
+ data = {
196
+ 'sys' => { 'id' => '1234', 'revision' => 1 },
197
+ 'key' => 'val',
198
+ '1' => { 'deep' => 9 }
199
+ }
200
+ data2 = {
201
+ 'sys' => { 'id' => '1234', 'revision' => 2 },
202
+ 'key' => 'val',
203
+ '1' => { 'deep' => 11 }
204
+ }
186
205
 
187
206
  # act
188
207
  prior1 = subject.set('1234', data)
@@ -193,11 +212,27 @@ RSpec.shared_examples 'basic store' do
193
212
  expect(prior2).to eq(data)
194
213
  expect(subject.find('1234')).to eq(data2)
195
214
  end
215
+
216
+ it 'modifying found entry does not modify underlying data' do
217
+ subject.index(entry)
218
+
219
+ # act
220
+ found = subject.find('1qLdW7i7g4Ycq6i4Cckg44')
221
+ found['fields']['slug']['en-US'] = 'new slug'
222
+
223
+ # assert
224
+ found2 = subject.find('1qLdW7i7g4Ycq6i4Cckg44')
225
+ expect(found2.dig('fields', 'slug', 'en-US')).to eq('redirect-with-slug-and-url')
226
+ end
196
227
  end
197
228
 
198
229
  describe '#delete' do
199
230
  it 'deletes an item out of the store' do
200
- data = { 'key' => 'val', '1' => { 'deep' => 9 } }
231
+ data = {
232
+ 'sys' => { 'id' => '1234' },
233
+ 'key' => 'val',
234
+ '1' => { 'deep' => 9 }
235
+ }
201
236
  subject.set('9999', data)
202
237
 
203
238
  # act
@@ -209,7 +244,11 @@ RSpec.shared_examples 'basic store' do
209
244
  end
210
245
 
211
246
  it "returns nil if item doesn't exist" do
212
- data = { 'key' => 'val', '1' => { 'deep' => 9 } }
247
+ data = {
248
+ 'sys' => { 'id' => '9999' },
249
+ 'key' => 'val',
250
+ '1' => { 'deep' => 9 }
251
+ }
213
252
  subject.set('9999', data)
214
253
 
215
254
  # act
@@ -327,7 +366,10 @@ RSpec.shared_examples 'basic store' do
327
366
  end
328
367
 
329
368
  it 'updates an "Asset" when exists' do
330
- existing = { 'test' => { 'data' => 'asdf' } }
369
+ existing = {
370
+ 'sys' => { 'id' => '3pWma8spR62aegAWAWacyA', 'revision' => 1 },
371
+ 'test' => { 'data' => 'asdf' }
372
+ }
331
373
  subject.set('3pWma8spR62aegAWAWacyA', existing)
332
374
 
333
375
  # act
@@ -341,7 +383,7 @@ RSpec.shared_examples 'basic store' do
341
383
  it 'does not overwrite an asset if revision is lower' do
342
384
  initial = asset
343
385
  updated = asset.deep_dup
344
- updated['sys']['revision'] = 2
386
+ updated['sys']['revision'] = 3
345
387
  updated['fields']['title']['en-US'] = 'test title'
346
388
 
347
389
  subject.index(updated)
@@ -355,7 +397,10 @@ RSpec.shared_examples 'basic store' do
355
397
  end
356
398
 
357
399
  it 'removes a "DeletedEntry"' do
358
- existing = { 'test' => { 'data' => 'asdf' } }
400
+ existing = {
401
+ 'sys' => { 'id' => '6HQsABhZDiWmi0ekCouUuy' },
402
+ 'test' => { 'data' => 'asdf' }
403
+ }
359
404
  subject.set('6HQsABhZDiWmi0ekCouUuy', existing)
360
405
 
361
406
  # act
@@ -381,7 +426,10 @@ RSpec.shared_examples 'basic store' do
381
426
  end
382
427
 
383
428
  it 'removes a "DeletedAsset"' do
384
- existing = { 'test' => { 'data' => 'asdf' } }
429
+ existing = {
430
+ 'sys' => { 'id' => '3pWma8spR62aegAWAWacyA' },
431
+ 'test' => { 'data' => 'asdf' }
432
+ }
385
433
  subject.set('3pWma8spR62aegAWAWacyA', existing)
386
434
 
387
435
  # act
@@ -528,6 +576,18 @@ RSpec.shared_examples 'basic store' do
528
576
  expect(found.dig('sys', 'id')).to eq('idTwo')
529
577
  expect(found.dig('fields', 'system', 'en-US')).to eq('Two')
530
578
  end
579
+
580
+ it 'modifying found entry does not modify underlying data' do
581
+ subject.index(entry)
582
+
583
+ # act
584
+ found = subject.find_by(filter: { 'sys.id' => '1qLdW7i7g4Ycq6i4Cckg44' }, content_type: 'redirect')
585
+ found['fields']['slug']['en-US'] = 'new slug'
586
+
587
+ # assert
588
+ found2 = subject.find_by(filter: { 'sys.id' => '1qLdW7i7g4Ycq6i4Cckg44' }, content_type: 'redirect')
589
+ expect(found2.dig('fields', 'slug', 'en-US')).to eq('redirect-with-slug-and-url')
590
+ end
531
591
  end
532
592
 
533
593
  describe '#find_all' do
@@ -572,6 +632,18 @@ RSpec.shared_examples 'basic store' do
572
632
  %w[k1 k5 k9]
573
633
  )
574
634
  end
635
+
636
+ it 'modifying found entry does not modify underlying data' do
637
+ subject.index(entry)
638
+
639
+ # act
640
+ found = subject.find_all(content_type: 'redirect').eq('sys.id', '1qLdW7i7g4Ycq6i4Cckg44').first
641
+ found['fields']['slug']['en-US'] = 'new slug'
642
+
643
+ # assert
644
+ found2 = subject.find_all(content_type: 'redirect').eq('sys.id', '1qLdW7i7g4Ycq6i4Cckg44').first
645
+ expect(found2.dig('fields', 'slug', 'en-US')).to eq('redirect-with-slug-and-url')
646
+ end
575
647
  end
576
648
 
577
649
  def make_link_to(id, link_type = 'Entry')