shamu 0.0.21 → 0.0.24

Sign up to get free protection for your applications and to get access to all the features.
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