shamu 0.0.21 → 0.0.24

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/.ruby-version +1 -1
  4. data/Gemfile +3 -1
  5. data/Gemfile.lock +70 -69
  6. data/app/README +1 -0
  7. data/config/environment.rb +1 -0
  8. data/lib/shamu/attributes.rb +26 -0
  9. data/lib/shamu/attributes/equality.rb +10 -1
  10. data/lib/shamu/entities/entity.rb +11 -0
  11. data/lib/shamu/entities/list_scope.rb +3 -0
  12. data/lib/shamu/entities/list_scope/sorting.rb +6 -1
  13. data/lib/shamu/entities/list_scope/window_paging.rb +1 -1
  14. data/lib/shamu/entities/null_entity.rb +18 -12
  15. data/lib/shamu/entities/opaque_id.rb +9 -8
  16. data/lib/shamu/error.rb +24 -3
  17. data/lib/shamu/events/active_record/migration.rb +0 -2
  18. data/lib/shamu/events/active_record/service.rb +3 -1
  19. data/lib/shamu/events/events_service.rb +2 -2
  20. data/lib/shamu/events/in_memory/async_service.rb +4 -2
  21. data/lib/shamu/events/in_memory/service.rb +3 -1
  22. data/lib/shamu/features/features_service.rb +3 -1
  23. data/lib/shamu/features/support.rb +1 -1
  24. data/lib/shamu/json_api/rails/controller.rb +1 -1
  25. data/lib/shamu/locale/en.yml +14 -2
  26. data/lib/shamu/security/active_record_policy.rb +2 -1
  27. data/lib/shamu/security/error.rb +1 -1
  28. data/lib/shamu/security/policy.rb +14 -4
  29. data/lib/shamu/security/principal.rb +22 -2
  30. data/lib/shamu/security/roles.rb +4 -3
  31. data/lib/shamu/services.rb +3 -1
  32. data/lib/shamu/services/active_record.rb +2 -1
  33. data/lib/shamu/services/active_record_crud.rb +43 -19
  34. data/lib/shamu/services/lazy_association.rb +50 -18
  35. data/lib/shamu/services/observable_support.rb +73 -0
  36. data/lib/shamu/services/observed_request.rb +76 -0
  37. data/lib/shamu/services/request.rb +12 -0
  38. data/lib/shamu/services/request_support.rb +24 -2
  39. data/lib/shamu/services/result.rb +62 -1
  40. data/lib/shamu/services/service.rb +58 -33
  41. data/lib/shamu/sessions/cookie_store.rb +3 -1
  42. data/lib/shamu/sessions/session_store.rb +2 -2
  43. data/lib/shamu/version.rb +1 -1
  44. data/shamu.gemspec +1 -1
  45. data/spec/lib/shamu/entities/entity_lookup_service_spec.rb +1 -1
  46. data/spec/lib/shamu/entities/list_scope/sorting_spec.rb +1 -1
  47. data/spec/lib/shamu/entities/null_entity_spec.rb +6 -1
  48. data/spec/lib/shamu/entities/opaque_id_spec.rb +13 -6
  49. data/spec/lib/shamu/entities/static_repository_spec.rb +2 -2
  50. data/spec/lib/shamu/rails/entity_spec.rb +1 -1
  51. data/spec/lib/shamu/rails/features_spec.rb +1 -1
  52. data/spec/lib/shamu/security/principal_spec.rb +25 -0
  53. data/spec/lib/shamu/services/active_record_crud_spec.rb +39 -4
  54. data/spec/lib/shamu/services/lazy_association_spec.rb +19 -6
  55. data/spec/lib/shamu/services/observable_support_spec.rb +55 -0
  56. data/spec/lib/shamu/services/observed_request_spec.rb +46 -0
  57. data/spec/lib/shamu/services/request_support_spec.rb +54 -3
  58. data/spec/lib/shamu/services/service_spec.rb +16 -27
  59. metadata +15 -5
@@ -2,30 +2,62 @@ module Shamu
2
2
  module Services
3
3
 
4
4
  # Lazily look up an associated resource
5
- class LazyAssociation < Delegator
5
+ module LazyAssociation
6
+ EXCLUDE_PATTERN = /\A(block_given\?|id|send|public_send|iterator|object_id|to_model_id|binding|class|kind_of\?|is_a\?|instance_of\?|respond_to\?|p.+_methods|__.+__)\z/ # rubocop:disable Metrics/LineLength
7
+ MUTEX = Mutex.new
6
8
 
7
- # ============================================================================
8
- # @!group Attributes
9
- #
9
+ def self.class_for( klass ) # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity
10
+ return klass.const_get( :Lazy_ ) if klass.const_defined?( :Lazy_ )
10
11
 
11
- # @!attribute
12
- # @return [Object] the primary key id of the association. Not delegated so
13
- # it is safe to use and will not trigger an unnecessary fetch.
14
- attr_reader :id
12
+ MUTEX.synchronize do
15
13
 
16
- #
17
- # @!endgroup Attributes
14
+ # Check again in case another thread defined it while waiting for the
15
+ # mutex
16
+ return klass.const_get( :Lazy_ ) if klass.const_defined?( :Lazy_ )
18
17
 
19
- def initialize( id, &block )
20
- @id = id
21
- @block = block
22
- end
18
+ lazy_class = Class.new( klass ) do
19
+ # Remove all existing public methods so that they can be delegated
20
+ # with #method_missing.
21
+ klass.public_instance_methods.each do |method|
22
+ next if EXCLUDE_PATTERN =~ method
23
+ undef_method method
24
+ end
25
+
26
+ def initialize( id, &block )
27
+ @id = id
28
+ @block = block
29
+ end
30
+
31
+ # @!attribute
32
+ # @return [Object] the primary key id of the association. Not delegated so
33
+ # it is safe to use and will not trigger an unnecessary fetch.
34
+ attr_reader :id
35
+
36
+ def __getobj__
37
+ return @association if defined? @association
23
38
 
24
- def __getobj__
25
- return @association if defined? @association
39
+ @association = @block.call( @id ) if @block
40
+ end
26
41
 
27
- @association = @block.call
42
+ def method_missing( method, *args, &block )
43
+ if respond_to_missing?( method )
44
+ __getobj__.public_send( method, *args, &block )
45
+ else
46
+ super
47
+ end
48
+ end
49
+
50
+ def respond_to_missing?( method, include_private = false )
51
+ __getobj__.respond_to?( method, include_private ) || super
52
+ end
53
+
54
+ end
55
+
56
+ klass.const_set( :Lazy_, lazy_class )
57
+ lazy_class
58
+ end
28
59
  end
60
+
29
61
  end
30
62
  end
31
- end
63
+ end
@@ -0,0 +1,73 @@
1
+ module Shamu
2
+ module Services
3
+
4
+ # Adds the ability for other services to observe requests on the service.
5
+ #
6
+ # In contrast to {Events} that are async and independent, observers are
7
+ # called immediately when a request is performed and may influence the
8
+ # behavior of the request.
9
+ #
10
+ # See {#ObserverSupport} for details on observing other services.
11
+ module ObservableSupport
12
+
13
+ # Ask to be notified of important actions as they are executed on the
14
+ # service.
15
+ #
16
+ # @yield (observed_request)
17
+ # @yieldparam [ObservedRequest] action
18
+ def observe( &block )
19
+ @observers ||= []
20
+ @observers << block
21
+ end
22
+
23
+ private
24
+
25
+ # @!visibility public
26
+ #
27
+ # Invoke a block notifying observers before the action is performed
28
+ # allowing them to modify inputs or request the action be canceled.
29
+ #
30
+ # @param [Request] request the service request
31
+ # @return [Result]
32
+ # @yield [Request]
33
+ def with_observers( request, &block )
34
+ observed = ObservedRequest.new( request: request )
35
+ notify_observers( observed )
36
+
37
+ returned =
38
+ if observed.cancel_requested?
39
+ request.error :base, :canceled
40
+ else
41
+ yield( request )
42
+ end
43
+
44
+ result = Result.coerce( returned, request: request )
45
+
46
+ observed.complete( result, false )
47
+ end
48
+
49
+ # @!visibility public
50
+ #
51
+ # Notify all registered observers about the pending request.
52
+ # @param [ObservedAction] observed_action
53
+ def notify_observers( observed_action )
54
+ return unless defined? @observers
55
+
56
+ @observers.each do |observer|
57
+ observer.call( observed_action )
58
+ end
59
+ end
60
+
61
+ # Override {Shamu::Services::RequestSupport#with_partial_request} to make all requests observable.
62
+ # {#audit_request audit the request}.
63
+ def with_partial_request( *args, &block )
64
+ super( *args ) do |request|
65
+ with_observers request do |wrapped_request|
66
+ yield wrapped_request
67
+ end
68
+ end
69
+ end
70
+
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,76 @@
1
+ module Shamu
2
+ module Services
3
+
4
+ # Describes request that will be/has been performed by a service and the
5
+ # associated data properties.
6
+ class ObservedRequest
7
+ include Shamu::Attributes
8
+
9
+ # ============================================================================
10
+ # @!group Attributes
11
+ #
12
+
13
+ # @!attribute
14
+ # @return [Request] the original request submitted to the service. The
15
+ # request may be modified by the observers.
16
+ attribute :request
17
+
18
+ # @return [Result] the result of a dependency that asked for the request
19
+ # to be canceled.
20
+ attr_reader :cancel_reason
21
+
22
+ #
23
+ # @!endgroup Attributes
24
+
25
+ # Ask that the service cancel the request.
26
+ #
27
+ # @return [Result] a nested result that should be reported for why the
28
+ # request was canceled.
29
+ def request_cancel( result = Result.new )
30
+ @cancel_reason = result
31
+ end
32
+
33
+ # @return [Boolean] true if an observer has asked the request to be
34
+ # canceled.
35
+ def cancel_requested?
36
+ !!cancel_reason
37
+ end
38
+
39
+ # Execute block if the action was canceled by another observer.
40
+ # @yield(result)
41
+ # @yieldresult [Result]
42
+ def on_canceled( &block )
43
+ @on_cancel_blocks ||= []
44
+ @on_cancel_blocks << block
45
+ end
46
+
47
+ # Mark the action as complete and run any {#on_success} or #{on_fail}
48
+ # callbacks.
49
+ #
50
+ # @param [Result] result the result of the action. If valid success callbacks are invoked.
51
+ # @param [Boolean] canceled true if the action was canceled and not
52
+ # processed.
53
+ #
54
+ # @return [Result] the result of all the observed callbacks.
55
+ def complete( result, canceled )
56
+ invoke_callbacks( result, @on_cancel_blocks ) if canceled
57
+
58
+ result
59
+ end
60
+
61
+ private
62
+
63
+ def invoke_callbacks( result, callbacks )
64
+ return unless callbacks.present?
65
+
66
+ callbacks.each do |callback|
67
+ nested = callback.call( result )
68
+ result.join( nested ) if nested
69
+ end
70
+
71
+ result
72
+ end
73
+
74
+ end
75
+ end
76
+ end
@@ -85,6 +85,18 @@ module Shamu
85
85
  @on_complete_blocks && @on_complete_blocks.each( &:call )
86
86
  end
87
87
 
88
+ # Adds an error to {#errors} and returns self. Used when performing an
89
+ # early return in a service method
90
+ #
91
+ # @example
92
+ # next request.error( :title, "should be clever" ) unless title_is_clever?
93
+ #
94
+ # @return [self]
95
+ def error( *args )
96
+ errors.add( *args )
97
+ self
98
+ end
99
+
88
100
  class << self
89
101
  # Coerces a hash or params object to a proper {Request} object.
90
102
  # @param [Object] params to be coerced.
@@ -58,7 +58,7 @@ module Shamu
58
58
  # order = Models::Order.find( request.id )
59
59
  #
60
60
  # # Custom validation
61
- # next error( :base, "can't do that" ) if order.state == 'processed'
61
+ # next request.error( :base, "can't do that" ) if order.state == 'processed'
62
62
  #
63
63
  # request.apply_to( order )
64
64
  #
@@ -66,7 +66,7 @@ module Shamu
66
66
  # next order unless order.save
67
67
  #
68
68
  # # All good, return an entity for the order
69
- # scorpion.fetch OrderEntity, { order: order }, {}
69
+ # scorpion.fetch OrderEntity, { order: order }
70
70
  # end
71
71
  # end
72
72
  def with_request( params, request_class, &block )
@@ -92,10 +92,32 @@ module Shamu
92
92
 
93
93
  result = Result.coerce( sources, request: request )
94
94
  request.complete( result.valid? )
95
+ recache_entity( result.entity ) if result.valid? && result.entity
95
96
 
96
97
  result
97
98
  end
98
99
 
100
+ # Support convenient calling convention on update/delete style methods
101
+ # that might pass an id and params or a single params hash with
102
+ # associated id.
103
+ #
104
+ # @example
105
+ # users.update user, name: "Changed" # Backend service
106
+ # users.update id: 1, name: "Changed" # HTTP request params
107
+ #
108
+ def extract_params( id, params )
109
+ if !params && !id.respond_to?( :to_model_id )
110
+ params, id = id, id[ :id ] || id[ "id" ]
111
+ end
112
+
113
+ if params
114
+ params = params.symbolize_keys if params.respond_to?( :symbolize_keys )
115
+ params[ :id ] ||= id
116
+ end
117
+
118
+ [ id, params ]
119
+ end
120
+
99
121
  # Static methods added to {RequestSupport}
100
122
  class_methods do
101
123
 
@@ -38,6 +38,12 @@ module Shamu
38
38
  value
39
39
  end
40
40
 
41
+ # @return [Array<Result>] results from calling dependent assemblies that
42
+ # may have caused the request to fail.
43
+ def nested_results
44
+ @nested_results ||= []
45
+ end
46
+
41
47
  #
42
48
  # @!endgroup Attributes
43
49
 
@@ -48,7 +54,7 @@ module Shamu
48
54
  # the first {Request} object found in the `values`.
49
55
  # @param [Entities::Entity] entity submitted to the service. If :not_set,
50
56
  # uses the first {Entity} object found in the `values`.
51
- def initialize( *values, request: :not_set, entity: :not_set ) # rubocop:disable Metrics/LineLength, Metrics/MethodLength, Metrics/PerceivedComplexity
57
+ def initialize( *values, request: :not_set, entity: :not_set ) # rubocop:disable Metrics/LineLength, Metrics/PerceivedComplexity
52
58
  @values = values
53
59
  @value = values.first
54
60
 
@@ -97,6 +103,12 @@ module Shamu
97
103
  self
98
104
  end
99
105
 
106
+ # Joins a dependency's result to the result of the request.
107
+ def join( result )
108
+ nested_results << result
109
+ append_error_source result
110
+ end
111
+
100
112
  # @return [Result] the value coerced to a {Result}.
101
113
  def self.coerce( value, **args )
102
114
  if value.is_a?( Result )
@@ -106,6 +118,55 @@ module Shamu
106
118
  end
107
119
  end
108
120
 
121
+ # @return [String] debug friendly string
122
+ def inspect # rubocop:disable Metrics/AbcSize
123
+ result = "#<#{ self.class } valid: #{ valid? }"
124
+ result << ", errors: #{ errors.inspect }" if errors.any?
125
+ result << ", entity: #{ entity.inspect }" if entity
126
+ result << ", value: #{ value.inspect }" if value && value != entity
127
+ result << ", values: #{ values.inspect }" if values.length > 1
128
+ result << ">"
129
+ result
130
+ end
131
+
132
+ # @return [String] even friendlier debug string.
133
+ def pretty_print( pp ) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
134
+ pp.object_address_group( self ) do
135
+ pp.breakable " "
136
+ pp.text "valid: "
137
+ pp.pp valid?
138
+
139
+ if errors.any?
140
+ pp.comma_breakable
141
+ pp.text "errors:"
142
+ pp.breakable " "
143
+ pp.pp errors
144
+ end
145
+
146
+ if entity
147
+ pp.comma_breakable
148
+ pp.text "entity:"
149
+ pp.breakable " "
150
+ pp.pp entity
151
+ end
152
+
153
+ if !value.nil? && value != entity
154
+ pp.comma_breakable
155
+ pp.text "value:"
156
+ pp.breakable " "
157
+ pp.pp value
158
+ end
159
+
160
+ if values.length > 1
161
+ pp.comma_breakable
162
+ pp.text "values:"
163
+ pp.breakable " "
164
+ pp.pp values - [ value ]
165
+ end
166
+ end
167
+ end
168
+
169
+
109
170
  private
110
171
 
111
172
  def append_error_source( source )
@@ -53,7 +53,7 @@ module Shamu
53
53
  # ```
54
54
  # def report( report_scope = nil )
55
55
  # report_scope = UserReportScope.coerce! report_scope
56
- # scorpion.fetch UserReport, report_scope, {}
56
+ # scorpion.fetch UserReport, report_scope
57
57
  # end
58
58
  # ```
59
59
  class Service
@@ -61,9 +61,6 @@ module Shamu
61
61
  # Support dependency injection for related services.
62
62
  include Scorpion::Object
63
63
 
64
- initialize do
65
- end
66
-
67
64
  private
68
65
 
69
66
  # Maps a single record to an entity. Requires a `build_entities` method
@@ -133,7 +130,7 @@ module Shamu
133
130
  # def lookup( *ids )
134
131
  # records = Models::Favorite.all.where( id: ids )
135
132
  # entity_lookup_list records, ids, NullEntity.for( FavoriteEntity ) do |record|
136
- # scorpion.fetch FavoriteEntity, { record: record }, {}
133
+ # scorpion.fetch FavoriteEntity, { record: record }
137
134
  # end
138
135
  # end
139
136
  #
@@ -141,7 +138,7 @@ module Shamu
141
138
  # records = Models::Favorite.all.where( :name.in( names ) )
142
139
  #
143
140
  # entity_lookup_list records, names, NullEntity.for( FavoriteEntity ), match: :name do |record|
144
- # scorpion.fetch FavoriteEntity, { record: record }, {}
141
+ # scorpion.fetch FavoriteEntity, { record: record }
145
142
  # end
146
143
  # end
147
144
  #
@@ -150,7 +147,7 @@ module Shamu
150
147
  # matcher = ->( record ) { record.name.downcase }
151
148
  #
152
149
  # entity_lookup_list records, names, NullEntity.for( FavoriteEntity ), match: matcher do |record|
153
- # scorpion.fetch FavoriteEntity, { record: record }, {}
150
+ # scorpion.fetch FavoriteEntity, { record: record }
154
151
  # end
155
152
  # end
156
153
  #
@@ -162,19 +159,25 @@ module Shamu
162
159
 
163
160
  list = entity_list records, &transformer
164
161
  matched = ids.map do |id|
165
- list.find { |e| matcher.call( e ) == id } || scorpion.fetch( null_class, { id: id }, {} )
162
+ list.find { |e| matcher.call( e ) == id } || scorpion.fetch( null_class, id: id )
166
163
  end
167
164
 
168
165
  Entities::List.new( matched )
169
166
  end
170
167
 
168
+ ID_MATCHER = ->( record ) { record && record.id }
169
+
171
170
  def entity_lookup_list_matcher( match )
172
171
  if !match.is_a?( Symbol ) && match.respond_to?( :call )
173
172
  match
174
173
  elsif match == :id
175
- ->( record ) { record && record.id }
174
+ ID_MATCHER
176
175
  else
177
- ->( record ) { record && record.send( match ) }
176
+ @@matcher_proc_cache ||= Hash.new do |hash, key| # rubocop:disable Style/ClassVars
177
+ hash[ key ] = ->( record ) { record && record.send( key ) }
178
+ end
179
+
180
+ @@matcher_proc_cache[ match ]
178
181
  end
179
182
  end
180
183
 
@@ -208,10 +211,15 @@ module Shamu
208
211
  # end
209
212
  def find_by_lookup( id )
210
213
  entity = lookup( id ).first
211
- raise Shamu::NotFoundError unless entity.present?
214
+ not_found!( id ) unless entity.present?
212
215
  entity
213
216
  end
214
217
 
218
+ # @exception [Shamu::NotFoundError]
219
+ def not_found!( id = :not_set )
220
+ raise Shamu::NotFoundError, id: id
221
+ end
222
+
215
223
  # @!visibility public
216
224
  #
217
225
  # Find an associated entity from a dependent service. Attempts to
@@ -221,22 +229,23 @@ module Shamu
221
229
  #
222
230
  # @param [Object] id of the associated {Entities::Entity} to find.
223
231
  # @param [Service] service used to locate the associated resource.
232
+ # @param [IdentityCache] cache to store found associations.
224
233
  # @return [Entity] the found entity or a {Entities::NullEntity} if the
225
234
  # association doesn't exist.
226
235
  #
227
236
  # @example
228
237
  #
229
- # def build_entity( record, records = nil )
230
- # owner = lookup_association record.owner_id, users_service do
238
+ # def build_entities( records )
239
+ # cache = cache_for( entity: users_service )
240
+ # owner = lookup_association record.owner_id, users_service, cache do
231
241
  # records.pluck( :owner_id ) if records
232
242
  # end
233
243
  #
234
- # scorpion.fetch UserEntity, { record: record, owner: owner }, {}
244
+ # scorpion.fetch UserEntity, { record: record, owner: owner }
235
245
  # end
236
- def lookup_association( id, service, &block )
246
+ def lookup_association( id, service, cache, &block )
237
247
  return unless id
238
248
 
239
- cache = cache_for( entity: service )
240
249
  cache.fetch( id ) || begin
241
250
  if block_given? && ( ids = yield )
242
251
  service.lookup( *ids ).map do |entity|
@@ -253,20 +262,33 @@ module Shamu
253
262
 
254
263
  # @!visibility public
255
264
  #
256
- # Perform a lazy {#lookup_association} and only load the entity if its
257
- # actually dereferenced by the caller.
265
+ # Build a proxy object that delays yielding to the block until a method
266
+ # on the association is invoked.
267
+ #
268
+ # @example
269
+ # user = lazy_association 10, Users::UserEntity do
270
+ # expensive_lookup_user.find( 10 )
271
+ # end
272
+ #
273
+ # user.id # => 10 expensive lookup not performed
274
+ # user.name # => "Trump" expensive lookup executed, cached, then
275
+ # # method invoked on real object
258
276
  #
259
- # @param (see #lookup_association)
277
+ # @param [Integer] id of the resource.
278
+ # @param [Class] entity_class of the resource.
260
279
  # @return [LazyAssociation<Entity>]
261
- def lazy_association( id, service, &block )
262
- LazyAssociation.new( id ) do
263
- lookup_association id, service, &block
264
- end
280
+ def lazy_association( id, entity_class, &block )
281
+ return nil if id.nil?
282
+
283
+ LazyAssociation.class_for( entity_class ).new( id, &block )
265
284
  end
266
285
 
267
286
  # @!visibility public
268
287
  #
269
288
  # Get the {Entities::IdentityCache} for the given {Entities::Entity} class.
289
+ # @param [Service#entity_class] dependency_service the dependent
290
+ # {Service} to cache results from. Must respond to `#entity_class` that
291
+ # returns the {Entities::Entity} class to cache.
270
292
  # @param [Class] entity the type of entity that will be cached. Only
271
293
  # required if the service manages multiple entities.
272
294
  # @param [Symbol,#call] key the attribute on the entity, or a custom
@@ -275,8 +297,10 @@ module Shamu
275
297
  # to the same type (eg :to_i). If not set, automatically uses :to_i
276
298
  # if key is an 'id' attribute.
277
299
  # @return [Entities::IdentityCache]
278
- def cache_for( key: :id, entity: nil, coerce: :not_set )
300
+ def cache_for( dependency_service = nil, key: :id, entity: nil, coerce: :not_set )
279
301
  coerce = coerce_method( coerce, key )
302
+ entity ||= dependency_service
303
+ entity = entity.entity_class if entity.respond_to?( :entity_class )
280
304
 
281
305
  cache_key = [ entity, key, coerce ]
282
306
  @entity_caches ||= {}
@@ -328,16 +352,17 @@ module Shamu
328
352
  end
329
353
  end
330
354
 
331
- # @!visibility public
355
+ # @!visbility public
332
356
  #
333
- # Return an error {#result} from a service request.
334
- # @overload error( attribute, message )
335
- # @param (see ErrorResult#initialize)
336
- # @return [ErrorResult]
337
- def error( *args )
338
- Result.new.tap do |r|
339
- r.errors.add( *args )
340
- end
357
+ # After a mutation method call to make sure the cache for the entity
358
+ # is updated to reflect the new entity state.
359
+ #
360
+ # @param [Entity] entity in the new modified state.
361
+ def recache_entity( entity, match: :id )
362
+ matcher = entity_lookup_list_matcher( match )
363
+ cache = cache_for( key: match )
364
+
365
+ cache.add( matcher.call( entity ), entity )
341
366
  end
342
367
 
343
368
  end