iknow_view_models 3.3.0 → 3.4.3
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 +4 -4
- data/iknow_view_models.gemspec +1 -1
- data/lib/iknow_view_models/version.rb +1 -1
- data/lib/view_model.rb +5 -5
- data/lib/view_model/active_record.rb +17 -21
- data/lib/view_model/active_record/association_data.rb +18 -1
- data/lib/view_model/active_record/association_manipulation.rb +9 -6
- data/lib/view_model/active_record/cache.rb +6 -9
- data/lib/view_model/active_record/collection_nested_controller.rb +11 -11
- data/lib/view_model/active_record/controller.rb +4 -4
- data/lib/view_model/active_record/nested_controller_base.rb +10 -6
- data/lib/view_model/active_record/singular_nested_controller.rb +6 -6
- data/lib/view_model/active_record/update_operation.rb +5 -6
- data/lib/view_model/migrator.rb +9 -2
- data/test/unit/view_model/active_record/belongs_to_test.rb +41 -6
- data/test/unit/view_model/active_record/has_many_test.rb +170 -0
- data/test/unit/view_model/active_record/has_many_through_poly_test.rb +1 -1
- data/test/unit/view_model/active_record/has_many_through_test.rb +1 -1
- data/test/unit/view_model/callbacks_test.rb +3 -3
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3560a98616dd2f0548ac7e663827e68d86fa9991969ac6ee49633a0090e7d628
|
4
|
+
data.tar.gz: deec44e5132f91dfc1deef0dd889ee20c1b01c6d5d7791d356f1e9dc99ec1c8a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5af2b38b2e8fb8805480bd665ea24e7ad4b489951c8b72660e502a053a945798f18269b60991eb416ab6959fc46e4831a911a15005810b57e288e2eeaaf37fd5
|
7
|
+
data.tar.gz: 5a73108237e3ffae5751f50a6e693e8fd737a4d1936f087095138ec2db47cbd7ec852f81e6ef881f794a38d67ba85429825b2393a850c149f363121f321adf30
|
data/iknow_view_models.gemspec
CHANGED
@@ -25,7 +25,7 @@ Gem::Specification.new do |spec|
|
|
25
25
|
spec.add_dependency 'activesupport', '>= 5.0'
|
26
26
|
|
27
27
|
spec.add_dependency 'acts_as_manual_list'
|
28
|
-
spec.add_dependency 'deep_preloader', '>= 1.0.
|
28
|
+
spec.add_dependency 'deep_preloader', '>= 1.0.2'
|
29
29
|
spec.add_dependency 'iknow_cache'
|
30
30
|
spec.add_dependency 'iknow_params', '~> 2.2.0'
|
31
31
|
spec.add_dependency 'keyword_builder'
|
data/lib/view_model.rb
CHANGED
@@ -134,7 +134,7 @@ class ViewModel
|
|
134
134
|
# If this viewmodel represents an AR model, what associations does it make
|
135
135
|
# use of? Returns a includes spec appropriate for DeepPreloader, either as
|
136
136
|
# AR-style nested hashes or DeepPreloader::Spec.
|
137
|
-
def eager_includes(
|
137
|
+
def eager_includes(include_referenced: true)
|
138
138
|
{}
|
139
139
|
end
|
140
140
|
|
@@ -267,10 +267,10 @@ class ViewModel
|
|
267
267
|
Base64.urlsafe_encode64(hash, padding: false)
|
268
268
|
end
|
269
269
|
|
270
|
-
def preload_for_serialization(viewmodels,
|
270
|
+
def preload_for_serialization(viewmodels, include_referenced: true, lock: nil)
|
271
271
|
Array.wrap(viewmodels).group_by(&:class).each do |type, views|
|
272
272
|
DeepPreloader.preload(views.map(&:model),
|
273
|
-
type.eager_includes(
|
273
|
+
type.eager_includes(include_referenced: include_referenced),
|
274
274
|
lock: lock)
|
275
275
|
end
|
276
276
|
end
|
@@ -354,8 +354,8 @@ class ViewModel
|
|
354
354
|
context.for_child(self, association_name: member_name)
|
355
355
|
end
|
356
356
|
|
357
|
-
def preload_for_serialization(lock: nil
|
358
|
-
ViewModel.preload_for_serialization([self], lock: lock
|
357
|
+
def preload_for_serialization(lock: nil)
|
358
|
+
ViewModel.preload_for_serialization([self], lock: lock)
|
359
359
|
end
|
360
360
|
|
361
361
|
def ==(other)
|
@@ -135,7 +135,7 @@ class ViewModel::ActiveRecord < ViewModel::Record
|
|
135
135
|
end
|
136
136
|
|
137
137
|
## Load instances of the viewmodel by id(s)
|
138
|
-
def find(id_or_ids, scope: nil, lock: nil, eager_include: true
|
138
|
+
def find(id_or_ids, scope: nil, lock: nil, eager_include: true)
|
139
139
|
find_scope = self.model_class.all
|
140
140
|
find_scope = find_scope.order(:id).lock(lock) if lock
|
141
141
|
find_scope = find_scope.merge(scope) if scope
|
@@ -152,19 +152,19 @@ class ViewModel::ActiveRecord < ViewModel::Record
|
|
152
152
|
end
|
153
153
|
|
154
154
|
vms = models.map { |m| self.new(m) }
|
155
|
-
ViewModel.preload_for_serialization(vms, lock: lock
|
155
|
+
ViewModel.preload_for_serialization(vms, lock: lock) if eager_include
|
156
156
|
vms
|
157
157
|
end
|
158
158
|
end
|
159
159
|
|
160
160
|
## Load instances of the viewmodel by scope
|
161
161
|
## TODO: is this too much of a encapsulation violation?
|
162
|
-
def load(scope: nil, eager_include: true, lock: nil
|
162
|
+
def load(scope: nil, eager_include: true, lock: nil)
|
163
163
|
load_scope = self.model_class.all
|
164
164
|
load_scope = load_scope.lock(lock) if lock
|
165
165
|
load_scope = load_scope.merge(scope) if scope
|
166
166
|
vms = load_scope.map { |model| self.new(model) }
|
167
|
-
ViewModel.preload_for_serialization(vms, lock: lock
|
167
|
+
ViewModel.preload_for_serialization(vms, lock: lock) if eager_include
|
168
168
|
vms
|
169
169
|
end
|
170
170
|
|
@@ -184,7 +184,7 @@ class ViewModel::ActiveRecord < ViewModel::Record
|
|
184
184
|
# Constructs a preload specification of the required models for
|
185
185
|
# serializing/deserializing this view. Cycles in the schema will be broken
|
186
186
|
# after two layers of eager loading.
|
187
|
-
def eager_includes(
|
187
|
+
def eager_includes(include_referenced: true, vm_path: [])
|
188
188
|
association_specs = {}
|
189
189
|
|
190
190
|
return nil if vm_path.count(self) > 2
|
@@ -194,19 +194,10 @@ class ViewModel::ActiveRecord < ViewModel::Record
|
|
194
194
|
next unless association_data.is_a?(AssociationData)
|
195
195
|
next if association_data.external?
|
196
196
|
|
197
|
-
child_context =
|
198
|
-
if self.synthetic
|
199
|
-
serialize_context
|
200
|
-
elsif association_data.referenced?
|
201
|
-
serialize_context.for_references
|
202
|
-
else
|
203
|
-
serialize_context.for_child(nil, association_name: assoc_name)
|
204
|
-
end
|
205
|
-
|
206
197
|
case
|
207
198
|
when association_data.through?
|
208
199
|
viewmodel = association_data.direct_viewmodel
|
209
|
-
children = viewmodel.eager_includes(
|
200
|
+
children = viewmodel.eager_includes(include_referenced: include_referenced, vm_path: child_path)
|
210
201
|
|
211
202
|
when !include_referenced && association_data.referenced?
|
212
203
|
children = nil # Load up to the root viewmodel, but no further
|
@@ -215,13 +206,13 @@ class ViewModel::ActiveRecord < ViewModel::Record
|
|
215
206
|
children_by_klass = {}
|
216
207
|
association_data.viewmodel_classes.each do |vm_class|
|
217
208
|
klass = vm_class.model_class.name
|
218
|
-
children_by_klass[klass] = vm_class.eager_includes(
|
209
|
+
children_by_klass[klass] = vm_class.eager_includes(include_referenced: include_referenced, vm_path: child_path)
|
219
210
|
end
|
220
211
|
children = DeepPreloader::PolymorphicSpec.new(children_by_klass)
|
221
212
|
|
222
213
|
else
|
223
214
|
viewmodel = association_data.viewmodel_class
|
224
|
-
children = viewmodel.eager_includes(
|
215
|
+
children = viewmodel.eager_includes(include_referenced: include_referenced, vm_path: child_path)
|
225
216
|
end
|
226
217
|
|
227
218
|
association_specs[association_data.direct_reflection.name.to_s] = children
|
@@ -348,7 +339,7 @@ class ViewModel::ActiveRecord < ViewModel::Record
|
|
348
339
|
# associated here are join-table models; we need to get the far side out
|
349
340
|
join_models = associated
|
350
341
|
|
351
|
-
if association_data.
|
342
|
+
if association_data.ordered?
|
352
343
|
attr = association_data.direct_viewmodel._list_attribute_name
|
353
344
|
join_models = join_models.sort_by { |j| j[attr] }
|
354
345
|
end
|
@@ -359,11 +350,16 @@ class ViewModel::ActiveRecord < ViewModel::Record
|
|
359
350
|
end
|
360
351
|
|
361
352
|
when association_data.collection?
|
362
|
-
|
363
|
-
|
364
|
-
|
353
|
+
associated_viewmodels = associated.map do |x|
|
354
|
+
associated_viewmodel_class = association_data.viewmodel_class_for_model!(x.class)
|
355
|
+
associated_viewmodel_class.new(x)
|
356
|
+
end
|
357
|
+
|
358
|
+
# If any associated type is a list member, they must all be
|
359
|
+
if association_data.ordered?
|
365
360
|
associated_viewmodels.sort_by!(&:_list_attribute)
|
366
361
|
end
|
362
|
+
|
367
363
|
associated_viewmodels
|
368
364
|
|
369
365
|
else
|
@@ -127,7 +127,9 @@ class ViewModel::ActiveRecord::AssociationData
|
|
127
127
|
end
|
128
128
|
|
129
129
|
def polymorphic?
|
130
|
-
|
130
|
+
# STI polymorphism isn't shown on the association reflection, so in that
|
131
|
+
# case we have to infer it by having multiple target viewmodel types.
|
132
|
+
target_reflection.polymorphic? || viewmodel_classes.size > 1
|
131
133
|
end
|
132
134
|
|
133
135
|
# The side of the immediate association that holds the pointer.
|
@@ -196,6 +198,21 @@ class ViewModel::ActiveRecord::AssociationData
|
|
196
198
|
viewmodel_classes.first
|
197
199
|
end
|
198
200
|
|
201
|
+
def ordered?
|
202
|
+
@ordered ||=
|
203
|
+
if through?
|
204
|
+
direct_viewmodel._list_member?
|
205
|
+
else
|
206
|
+
list_members = viewmodel_classes.map { |c| c._list_member? }.uniq
|
207
|
+
|
208
|
+
if list_members.size > 1
|
209
|
+
raise ArgumentError.new('Inconsistent associated views: mixed list membership')
|
210
|
+
end
|
211
|
+
|
212
|
+
list_members[0]
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
199
216
|
def through?
|
200
217
|
@indirect_association_name.present?
|
201
218
|
end
|
@@ -16,11 +16,13 @@ module ViewModel::ActiveRecord::AssociationManipulation
|
|
16
16
|
associated_viewmodel = association_data.viewmodel_class
|
17
17
|
direct_viewmodel = association_data.direct_viewmodel
|
18
18
|
else
|
19
|
+
raise ArgumentError.new('Polymorphic STI relationships not supported yet') if association_data.polymorphic?
|
20
|
+
|
19
21
|
associated_viewmodel = association.klass.try { |k| association_data.viewmodel_class_for_model!(k) }
|
20
22
|
direct_viewmodel = associated_viewmodel
|
21
23
|
end
|
22
24
|
|
23
|
-
if
|
25
|
+
if association_data.ordered?
|
24
26
|
association_scope = association_scope.order(direct_viewmodel._list_attribute_name)
|
25
27
|
end
|
26
28
|
|
@@ -34,10 +36,7 @@ module ViewModel::ActiveRecord::AssociationManipulation
|
|
34
36
|
|
35
37
|
vms = association_scope.map { |model| associated_viewmodel.new(model) }
|
36
38
|
|
37
|
-
if eager_include
|
38
|
-
child_context = self.context_for_child(association_name, context: serialize_context)
|
39
|
-
ViewModel.preload_for_serialization(vms, serialize_context: child_context)
|
40
|
-
end
|
39
|
+
ViewModel.preload_for_serialization(vms) if eager_include
|
41
40
|
|
42
41
|
if association_data.collection?
|
43
42
|
vms
|
@@ -119,6 +118,8 @@ module ViewModel::ActiveRecord::AssociationManipulation
|
|
119
118
|
direct_viewmodel_class = association_data.direct_viewmodel
|
120
119
|
root_update_data, referenced_update_data = construct_indirect_append_updates(association_data, subtree_hashes, references)
|
121
120
|
else
|
121
|
+
raise ArgumentError.new('Polymorphic STI relationships not supported yet') if association_data.polymorphic?
|
122
|
+
|
122
123
|
direct_viewmodel_class = association_data.viewmodel_class
|
123
124
|
root_update_data, referenced_update_data = construct_direct_append_updates(association_data, subtree_hashes, references)
|
124
125
|
end
|
@@ -130,7 +131,7 @@ module ViewModel::ActiveRecord::AssociationManipulation
|
|
130
131
|
update_context.root_updates.each { |update| update.reparent_to = new_parent }
|
131
132
|
|
132
133
|
# Set place in list.
|
133
|
-
if
|
134
|
+
if association_data.ordered?
|
134
135
|
new_positions = select_append_positions(association_data,
|
135
136
|
direct_viewmodel_class._list_attribute_name,
|
136
137
|
update_context.root_updates.count,
|
@@ -241,6 +242,8 @@ module ViewModel::ActiveRecord::AssociationManipulation
|
|
241
242
|
direct_viewmodel = association_data.direct_viewmodel
|
242
243
|
association_scope = association_scope.where(association_data.indirect_reflection.foreign_key => associated_id)
|
243
244
|
else
|
245
|
+
raise ArgumentError.new('Polymorphic STI relationships not supported yet') if association_data.polymorphic?
|
246
|
+
|
244
247
|
# viewmodel type for current association: nil in case of empty polymorphic association
|
245
248
|
direct_viewmodel = association.klass.try { |k| association_data.viewmodel_class_for_model!(k) }
|
246
249
|
|
@@ -196,7 +196,7 @@ class ViewModel::ActiveRecord::Cache
|
|
196
196
|
# {id=>serialized_view}. Any references encountered are added to the
|
197
197
|
# worklist.
|
198
198
|
def load_from_cache(viewmodel_cache, ids)
|
199
|
-
cached_serializations = viewmodel_cache.load(ids, migrated_cache_version(viewmodel_cache)
|
199
|
+
cached_serializations = viewmodel_cache.load(ids, migrated_cache_version(viewmodel_cache))
|
200
200
|
|
201
201
|
cached_serializations.each_with_object({}) do |(id, cached_serialization), result|
|
202
202
|
add_refs_to_worklist(cached_serialization[:ref_cache])
|
@@ -255,8 +255,7 @@ class ViewModel::ActiveRecord::Cache
|
|
255
255
|
if viewmodel.class < CacheableView
|
256
256
|
cacheable_references = referenced_viewmodels.transform_values { |vm| cacheable_reference(vm) }
|
257
257
|
target_cache = viewmodel.class.viewmodel_cache
|
258
|
-
target_cache.store(viewmodel.id, migrated_cache_version(target_cache), data_serialization, cacheable_references
|
259
|
-
serialize_context: serialize_context)
|
258
|
+
target_cache.store(viewmodel.id, migrated_cache_version(target_cache), data_serialization, cacheable_references)
|
260
259
|
end
|
261
260
|
|
262
261
|
result[viewmodel.id] = data_serialization
|
@@ -279,15 +278,13 @@ class ViewModel::ActiveRecord::Cache
|
|
279
278
|
if ids.present?
|
280
279
|
found = viewmodel_class.find(ids,
|
281
280
|
eager_include: false,
|
282
|
-
lock: 'FOR SHARE'
|
283
|
-
serialize_context: serialize_context)
|
281
|
+
lock: 'FOR SHARE')
|
284
282
|
viewmodels.concat(found)
|
285
283
|
end
|
286
284
|
|
287
285
|
ViewModel.preload_for_serialization(viewmodels,
|
288
286
|
include_referenced: false,
|
289
|
-
lock: 'FOR SHARE'
|
290
|
-
serialize_context: serialize_context)
|
287
|
+
lock: 'FOR SHARE')
|
291
288
|
|
292
289
|
viewmodels
|
293
290
|
end
|
@@ -335,12 +332,12 @@ class ViewModel::ActiveRecord::Cache
|
|
335
332
|
end
|
336
333
|
|
337
334
|
# Save the provided serialization and reference data in the cache
|
338
|
-
def store(id, migration_version, data_serialization, ref_cache
|
335
|
+
def store(id, migration_version, data_serialization, ref_cache)
|
339
336
|
key = key_for(id, migration_version)
|
340
337
|
cache_for(migration_version).write(key, { data: data_serialization, ref_cache: ref_cache })
|
341
338
|
end
|
342
339
|
|
343
|
-
def load(ids, migration_version
|
340
|
+
def load(ids, migration_version)
|
344
341
|
keys = ids.map { |id| key_for(id, migration_version) }
|
345
342
|
results = cache_for(migration_version).read_multi(keys)
|
346
343
|
results.transform_keys! { |key| id_for(key) }
|
@@ -22,23 +22,23 @@ module ViewModel::ActiveRecord::CollectionNestedController
|
|
22
22
|
extend ActiveSupport::Concern
|
23
23
|
include ViewModel::ActiveRecord::NestedControllerBase
|
24
24
|
|
25
|
-
def index_associated(scope: nil, serialize_context: new_serialize_context, &block)
|
26
|
-
show_association(scope: scope, serialize_context: serialize_context, &block)
|
25
|
+
def index_associated(scope: nil, serialize_context: new_serialize_context, lock_owner: nil, &block)
|
26
|
+
show_association(scope: scope, serialize_context: serialize_context, lock_owner: lock_owner, &block)
|
27
27
|
end
|
28
28
|
|
29
29
|
# Deserialize items of the associated type and associate them with the owner,
|
30
30
|
# replacing previously associated items.
|
31
|
-
def replace(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, &block)
|
32
|
-
write_association(serialize_context: serialize_context, deserialize_context: deserialize_context, &block)
|
31
|
+
def replace(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, lock_owner: nil, &block)
|
32
|
+
write_association(serialize_context: serialize_context, deserialize_context: deserialize_context, lock_owner: lock_owner, &block)
|
33
33
|
end
|
34
34
|
|
35
|
-
def disassociate_all(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context)
|
36
|
-
destroy_association(true, serialize_context: serialize_context, deserialize_context: deserialize_context)
|
35
|
+
def disassociate_all(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, lock_owner: nil)
|
36
|
+
destroy_association(true, serialize_context: serialize_context, deserialize_context: deserialize_context, lock_owner: lock_owner)
|
37
37
|
end
|
38
38
|
|
39
39
|
# Deserialize items of the associated type and append them to the owner's
|
40
40
|
# collection.
|
41
|
-
def append(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context)
|
41
|
+
def append(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, lock_owner: nil)
|
42
42
|
assoc_view = nil
|
43
43
|
pre_rendered = owner_viewmodel.transaction do
|
44
44
|
update_hash, refs = parse_viewmodel_updates
|
@@ -50,7 +50,7 @@ module ViewModel::ActiveRecord::CollectionNestedController
|
|
50
50
|
raise ViewModel::DeserializationError::InvalidSyntax.new('Can not provide both `before` and `after` anchors for a collection append')
|
51
51
|
end
|
52
52
|
|
53
|
-
owner_view = owner_viewmodel.find(owner_viewmodel_id, eager_include: false,
|
53
|
+
owner_view = owner_viewmodel.find(owner_viewmodel_id, eager_include: false, lock: lock_owner)
|
54
54
|
|
55
55
|
assoc_view = owner_view.append_associated(association_name,
|
56
56
|
update_hash,
|
@@ -60,7 +60,7 @@ module ViewModel::ActiveRecord::CollectionNestedController
|
|
60
60
|
deserialize_context: deserialize_context)
|
61
61
|
ViewModel::Callbacks.wrap_serialize(owner_view, context: serialize_context) do
|
62
62
|
child_context = owner_view.context_for_child(association_name, context: serialize_context)
|
63
|
-
ViewModel.preload_for_serialization(assoc_view
|
63
|
+
ViewModel.preload_for_serialization(assoc_view)
|
64
64
|
assoc_view = yield(assoc_view) if block_given?
|
65
65
|
prerender_viewmodel(assoc_view, serialize_context: child_context)
|
66
66
|
end
|
@@ -69,9 +69,9 @@ module ViewModel::ActiveRecord::CollectionNestedController
|
|
69
69
|
assoc_view
|
70
70
|
end
|
71
71
|
|
72
|
-
def disassociate(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context)
|
72
|
+
def disassociate(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, lock_owner: nil)
|
73
73
|
owner_viewmodel.transaction do
|
74
|
-
owner_view = owner_viewmodel.find(owner_viewmodel_id, eager_include: false,
|
74
|
+
owner_view = owner_viewmodel.find(owner_viewmodel_id, eager_include: false, lock: lock_owner)
|
75
75
|
owner_view.delete_associated(association_name, viewmodel_id, type: viewmodel_class, deserialize_context: deserialize_context)
|
76
76
|
render_viewmodel(nil)
|
77
77
|
end
|
@@ -22,7 +22,7 @@ module ViewModel::ActiveRecord::Controller
|
|
22
22
|
def show(scope: nil, viewmodel_class: self.viewmodel_class, serialize_context: new_serialize_context(viewmodel_class: viewmodel_class))
|
23
23
|
view = nil
|
24
24
|
pre_rendered = viewmodel_class.transaction do
|
25
|
-
view = viewmodel_class.find(viewmodel_id, scope: scope
|
25
|
+
view = viewmodel_class.find(viewmodel_id, scope: scope)
|
26
26
|
view = yield(view) if block_given?
|
27
27
|
prerender_viewmodel(view, serialize_context: serialize_context)
|
28
28
|
end
|
@@ -33,7 +33,7 @@ module ViewModel::ActiveRecord::Controller
|
|
33
33
|
def index(scope: nil, viewmodel_class: self.viewmodel_class, serialize_context: new_serialize_context(viewmodel_class: viewmodel_class))
|
34
34
|
views = nil
|
35
35
|
pre_rendered = viewmodel_class.transaction do
|
36
|
-
views = viewmodel_class.load(scope: scope
|
36
|
+
views = viewmodel_class.load(scope: scope)
|
37
37
|
views = yield(views) if block_given?
|
38
38
|
prerender_viewmodel(views, serialize_context: serialize_context)
|
39
39
|
end
|
@@ -47,7 +47,7 @@ module ViewModel::ActiveRecord::Controller
|
|
47
47
|
view = nil
|
48
48
|
pre_rendered = viewmodel_class.transaction do
|
49
49
|
view = viewmodel_class.deserialize_from_view(update_hash, references: refs, deserialize_context: deserialize_context)
|
50
|
-
ViewModel.preload_for_serialization(view
|
50
|
+
ViewModel.preload_for_serialization(view)
|
51
51
|
view = yield(view) if block_given?
|
52
52
|
prerender_viewmodel(view, serialize_context: serialize_context)
|
53
53
|
end
|
@@ -57,7 +57,7 @@ module ViewModel::ActiveRecord::Controller
|
|
57
57
|
|
58
58
|
def destroy(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context)
|
59
59
|
viewmodel_class.transaction do
|
60
|
-
view = viewmodel_class.find(viewmodel_id, eager_include: false
|
60
|
+
view = viewmodel_class.find(viewmodel_id, eager_include: false)
|
61
61
|
view.destroy!(deserialize_context: deserialize_context)
|
62
62
|
end
|
63
63
|
render_viewmodel(nil)
|
@@ -9,10 +9,10 @@ module ViewModel::ActiveRecord::NestedControllerBase
|
|
9
9
|
|
10
10
|
protected
|
11
11
|
|
12
|
-
def show_association(scope: nil, serialize_context: new_serialize_context)
|
12
|
+
def show_association(scope: nil, serialize_context: new_serialize_context, lock_owner: nil)
|
13
13
|
associated_views = nil
|
14
14
|
pre_rendered = owner_viewmodel.transaction do
|
15
|
-
owner_view = owner_viewmodel.find(owner_viewmodel_id, eager_include: false,
|
15
|
+
owner_view = owner_viewmodel.find(owner_viewmodel_id, eager_include: false, lock: lock_owner)
|
16
16
|
ViewModel::Callbacks.wrap_serialize(owner_view, context: serialize_context) do
|
17
17
|
# Association manipulation methods construct child contexts internally
|
18
18
|
associated_views = owner_view.load_associated(association_name, scope: scope, serialize_context: serialize_context)
|
@@ -27,12 +27,12 @@ module ViewModel::ActiveRecord::NestedControllerBase
|
|
27
27
|
associated_views
|
28
28
|
end
|
29
29
|
|
30
|
-
def write_association(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context)
|
30
|
+
def write_association(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, lock_owner: nil)
|
31
31
|
association_view = nil
|
32
32
|
pre_rendered = owner_viewmodel.transaction do
|
33
33
|
update_hash, refs = parse_viewmodel_updates
|
34
34
|
|
35
|
-
owner_view = owner_viewmodel.find(owner_viewmodel_id, eager_include: false,
|
35
|
+
owner_view = owner_viewmodel.find(owner_viewmodel_id, eager_include: false, lock: lock_owner)
|
36
36
|
|
37
37
|
association_view = owner_view.replace_associated(association_name, update_hash,
|
38
38
|
references: refs,
|
@@ -40,7 +40,7 @@ module ViewModel::ActiveRecord::NestedControllerBase
|
|
40
40
|
|
41
41
|
ViewModel::Callbacks.wrap_serialize(owner_view, context: serialize_context) do
|
42
42
|
child_context = owner_view.context_for_child(association_name, context: serialize_context)
|
43
|
-
ViewModel.preload_for_serialization(association_view
|
43
|
+
ViewModel.preload_for_serialization(association_view)
|
44
44
|
association_view = yield(association_view) if block_given?
|
45
45
|
prerender_viewmodel(association_view, serialize_context: child_context)
|
46
46
|
end
|
@@ -49,7 +49,11 @@ module ViewModel::ActiveRecord::NestedControllerBase
|
|
49
49
|
association_view
|
50
50
|
end
|
51
51
|
|
52
|
-
def destroy_association(collection, serialize_context: new_serialize_context, deserialize_context: new_deserialize_context)
|
52
|
+
def destroy_association(collection, serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, lock_owner: nil)
|
53
|
+
if lock_owner
|
54
|
+
owner_viewmodel.find(owner_viewmodel_id, eager_include: false, lock: lock_owner)
|
55
|
+
end
|
56
|
+
|
53
57
|
empty_update = collection ? [] : nil
|
54
58
|
owner_viewmodel.deserialize_from_view(owner_update_hash(empty_update),
|
55
59
|
deserialize_context: deserialize_context)
|
@@ -20,15 +20,15 @@ module ViewModel::ActiveRecord::SingularNestedController
|
|
20
20
|
extend ActiveSupport::Concern
|
21
21
|
include ViewModel::ActiveRecord::NestedControllerBase
|
22
22
|
|
23
|
-
def show_associated(scope: nil, serialize_context: new_serialize_context,
|
24
|
-
show_association(scope: scope, serialize_context: serialize_context, &block)
|
23
|
+
def show_associated(scope: nil, serialize_context: new_serialize_context, lock_owner: nil, &block)
|
24
|
+
show_association(scope: scope, serialize_context: serialize_context, lock_owner: lock_owner, &block)
|
25
25
|
end
|
26
26
|
|
27
|
-
def create_associated(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, &block)
|
28
|
-
write_association(serialize_context: serialize_context, deserialize_context: deserialize_context, &block)
|
27
|
+
def create_associated(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, lock_owner: nil, &block)
|
28
|
+
write_association(serialize_context: serialize_context, deserialize_context: deserialize_context, lock_owner: lock_owner, &block)
|
29
29
|
end
|
30
30
|
|
31
|
-
def destroy_associated(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context)
|
32
|
-
destroy_association(false, serialize_context: serialize_context, deserialize_context: deserialize_context)
|
31
|
+
def destroy_associated(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, lock_owner: nil)
|
32
|
+
destroy_association(false, serialize_context: serialize_context, deserialize_context: deserialize_context, lock_owner: lock_owner)
|
33
33
|
end
|
34
34
|
end
|
@@ -450,14 +450,13 @@ class ViewModel::ActiveRecord
|
|
450
450
|
parent_data = ParentData.new(association_data.direct_reflection.inverse_of, viewmodel)
|
451
451
|
|
452
452
|
# load children already attached to this model
|
453
|
-
child_viewmodel_class = association_data.viewmodel_class
|
454
|
-
|
455
453
|
previous_child_viewmodels =
|
456
454
|
model.public_send(association_data.direct_reflection.name).map do |child_model|
|
455
|
+
child_viewmodel_class = association_data.viewmodel_class_for_model!(child_model.class)
|
457
456
|
child_viewmodel_class.new(child_model)
|
458
457
|
end
|
459
458
|
|
460
|
-
if
|
459
|
+
if association_data.ordered?
|
461
460
|
previous_child_viewmodels.sort_by!(&:_list_attribute)
|
462
461
|
end
|
463
462
|
|
@@ -604,7 +603,7 @@ class ViewModel::ActiveRecord
|
|
604
603
|
# need to be updated anyway since their parent pointer will change.
|
605
604
|
new_positions = Array.new(resolved_children.length)
|
606
605
|
|
607
|
-
if association_data.
|
606
|
+
if association_data.ordered?
|
608
607
|
set_position = ->(index, pos) { new_positions[index] = pos }
|
609
608
|
|
610
609
|
get_previous_position = ->(index) do
|
@@ -802,7 +801,7 @@ class ViewModel::ActiveRecord
|
|
802
801
|
ReferencedCollectionMember.new(indirect_viewmodel_ref, direct_vm)
|
803
802
|
end
|
804
803
|
|
805
|
-
if
|
804
|
+
if association_data.ordered?
|
806
805
|
previous_members.sort_by!(&:position)
|
807
806
|
end
|
808
807
|
|
@@ -868,7 +867,7 @@ class ViewModel::ActiveRecord
|
|
868
867
|
viewmodel.association_changed!(association_data.association_name)
|
869
868
|
end
|
870
869
|
|
871
|
-
if
|
870
|
+
if association_data.ordered?
|
872
871
|
ActsAsManualList.update_positions(target_collection.members)
|
873
872
|
end
|
874
873
|
|
data/lib/view_model/migrator.rb
CHANGED
@@ -102,11 +102,18 @@ class ViewModel
|
|
102
102
|
class DownMigrator < Migrator
|
103
103
|
private
|
104
104
|
|
105
|
-
def migrate_viewmodel!(view_name,
|
105
|
+
def migrate_viewmodel!(view_name, source_version, view_hash, references)
|
106
106
|
path = @paths[view_name]
|
107
107
|
return false unless path
|
108
108
|
|
109
|
-
|
109
|
+
# In a serialized output, the source version should always be the present
|
110
|
+
# and the current version, unless already modified by a parent migration
|
111
|
+
required_version, current_version = @versions[view_name]
|
112
|
+
return false if source_version == required_version
|
113
|
+
|
114
|
+
unless source_version == current_version
|
115
|
+
raise ViewModel::Migration::UnspecifiedVersionError.new(view_name, source_version)
|
116
|
+
end
|
110
117
|
|
111
118
|
path.reverse_each do |migration|
|
112
119
|
migration.down(view_hash, references)
|
@@ -168,26 +168,61 @@ class ViewModel::ActiveRecord::BelongsToTest < ActiveSupport::TestCase
|
|
168
168
|
|
169
169
|
class GCTests < ActiveSupport::TestCase
|
170
170
|
include ARVMTestUtilities
|
171
|
-
include ViewModelSpecHelpers::
|
171
|
+
include ViewModelSpecHelpers::Base
|
172
172
|
|
173
173
|
def model_attributes
|
174
174
|
super.merge(
|
175
175
|
schema: ->(t) do
|
176
|
+
t.integer :destroyed_child_id
|
176
177
|
t.integer :deleted_child_id
|
177
178
|
t.integer :ignored_child_id
|
179
|
+
t.foreign_key :children, column: :destroyed_child_id
|
180
|
+
t.foreign_key :children, column: :deleted_child_id
|
181
|
+
t.foreign_key :children, column: :ignored_child_id
|
178
182
|
end,
|
179
183
|
model: ->(_m) do
|
180
|
-
belongs_to :
|
181
|
-
belongs_to :
|
184
|
+
belongs_to :destroyed_child, class_name: Child.name, inverse_of: :destroyed_model, dependent: :destroy
|
185
|
+
belongs_to :deleted_child, class_name: Child.name, inverse_of: :deleted_model, dependent: :delete
|
186
|
+
belongs_to :ignored_child, class_name: Child.name, inverse_of: :ignored_model
|
182
187
|
end,
|
183
188
|
viewmodel: ->(_v) do
|
184
|
-
associations :deleted_child, :ignored_child
|
189
|
+
associations :destroyed_child, :deleted_child, :ignored_child
|
185
190
|
end)
|
186
191
|
end
|
187
192
|
|
188
|
-
|
189
|
-
|
193
|
+
def child_attributes
|
194
|
+
super.merge(
|
195
|
+
model: ->(_m) do
|
196
|
+
has_one :destroyed_model, class_name: 'Model', inverse_of: :destroyed_child, foreign_key: 'destroyed_child_id'
|
197
|
+
has_one :deleted_model, class_name: 'Model', inverse_of: :deleted_child, foreign_key: 'deleted_child_id'
|
198
|
+
has_one :ignored_model, class_name: 'Model', inverse_of: :ignored_child, foreign_key: 'ignored_child_id'
|
199
|
+
end)
|
200
|
+
end
|
201
|
+
|
202
|
+
def viewmodel_class
|
203
|
+
child_viewmodel_class
|
204
|
+
super
|
205
|
+
end
|
206
|
+
|
207
|
+
# test belongs_to garbage collection - dependent: destroy
|
208
|
+
def test_gc_dependent_destroy
|
209
|
+
model = model_class.create(destroyed_child: Child.new(name: 'one'))
|
210
|
+
|
211
|
+
old_child = model.destroyed_child
|
212
|
+
|
213
|
+
alter_by_view!(ModelView, model) do |ov, _refs|
|
214
|
+
ov['destroyed_child'] = { '_type' => 'Child', 'name' => 'two' }
|
215
|
+
end
|
216
|
+
|
217
|
+
assert_equal('two', model.destroyed_child.name)
|
218
|
+
refute_equal(old_child, model.destroyed_child)
|
219
|
+
assert(Child.where(id: old_child.id).blank?)
|
220
|
+
end
|
221
|
+
|
222
|
+
# test belongs_to garbage collection - dependent: delete
|
223
|
+
def test_gc_dependent_delete
|
190
224
|
model = model_class.create(deleted_child: Child.new(name: 'one'))
|
225
|
+
|
191
226
|
old_child = model.deleted_child
|
192
227
|
|
193
228
|
alter_by_view!(ModelView, model) do |ov, _refs|
|
@@ -808,6 +808,39 @@ class ViewModel::ActiveRecord::HasManyTest < ActiveSupport::TestCase
|
|
808
808
|
assert_match(/Removed entities must have only _type and id fields/, ex.message)
|
809
809
|
end
|
810
810
|
|
811
|
+
def test_functional_update_move
|
812
|
+
c1_id, c2_id, c3_id = @model1.children.pluck(:id)
|
813
|
+
c4_id, c5_id = @model2.children.pluck(:id)
|
814
|
+
|
815
|
+
remove_fupdate = build_fupdate do
|
816
|
+
remove([{ '_type' => 'Child', 'id' => c2_id }])
|
817
|
+
end
|
818
|
+
|
819
|
+
append_fupdate = build_fupdate do
|
820
|
+
append([{ '_type' => 'Child', 'id' => c2_id }])
|
821
|
+
end
|
822
|
+
|
823
|
+
move_view = [
|
824
|
+
{
|
825
|
+
'_type' => 'Model',
|
826
|
+
'id' => @model1.id,
|
827
|
+
'children' => remove_fupdate
|
828
|
+
},
|
829
|
+
{
|
830
|
+
'_type' => 'Model',
|
831
|
+
'id' => @model2.id,
|
832
|
+
'children' => append_fupdate
|
833
|
+
}
|
834
|
+
]
|
835
|
+
|
836
|
+
viewmodel_class.deserialize_from_view(move_view)
|
837
|
+
@model1.reload
|
838
|
+
@model2.reload
|
839
|
+
|
840
|
+
assert_equal([c1_id, c3_id], @model1.children.pluck(:id))
|
841
|
+
assert_equal([c4_id, c5_id, c2_id], @model2.children.pluck(:id))
|
842
|
+
end
|
843
|
+
|
811
844
|
def test_functional_update_update_success
|
812
845
|
c1_id, c2_id, c3_id = @model1.children.pluck(:id)
|
813
846
|
|
@@ -865,6 +898,143 @@ class ViewModel::ActiveRecord::HasManyTest < ActiveSupport::TestCase
|
|
865
898
|
assert_match(/Duplicate functional update targets\b.*\bChild\b/, ex.message)
|
866
899
|
end
|
867
900
|
|
901
|
+
describe 'sti polymorphic children' do
|
902
|
+
def setup
|
903
|
+
child_viewmodel_class
|
904
|
+
dog_viewmodel_class
|
905
|
+
cat_viewmodel_class
|
906
|
+
enable_logging!
|
907
|
+
end
|
908
|
+
|
909
|
+
def child_attributes
|
910
|
+
super().merge(schema: ->(t) do
|
911
|
+
t.string :type, null: false
|
912
|
+
t.integer :dog_number
|
913
|
+
t.integer :cat_number
|
914
|
+
end)
|
915
|
+
end
|
916
|
+
|
917
|
+
def subject_association_features
|
918
|
+
{ viewmodels: [:Dog, :Cat] }
|
919
|
+
end
|
920
|
+
|
921
|
+
def dog_viewmodel_class
|
922
|
+
@dog_viewmodel_class ||= define_viewmodel_class(:Dog, namespace: namespace, viewmodel_base: viewmodel_base, model_base: child_model_class) do
|
923
|
+
define_model {}
|
924
|
+
define_viewmodel do
|
925
|
+
attribute :dog_number
|
926
|
+
acts_as_list :position
|
927
|
+
end
|
928
|
+
end
|
929
|
+
end
|
930
|
+
|
931
|
+
def cat_viewmodel_class
|
932
|
+
@cat_viewmodel_class ||= define_viewmodel_class(:Cat, namespace: namespace, viewmodel_base: viewmodel_base, model_base: child_model_class) do
|
933
|
+
define_model {}
|
934
|
+
define_viewmodel do
|
935
|
+
attribute :cat_number
|
936
|
+
acts_as_list :position
|
937
|
+
end
|
938
|
+
end
|
939
|
+
end
|
940
|
+
|
941
|
+
def new_model
|
942
|
+
model_class.new(name: 'p', children: [Dog.new(position: 1, dog_number: 1), Cat.new(position: 2, cat_number: 2)])
|
943
|
+
end
|
944
|
+
|
945
|
+
it 'creates the model structure' do
|
946
|
+
m = create_model!
|
947
|
+
m.reload
|
948
|
+
assert(m.is_a?(Model))
|
949
|
+
children = m.children.order(:position)
|
950
|
+
assert_equal(2, children.size)
|
951
|
+
assert_kind_of(Dog, children[0])
|
952
|
+
assert_kind_of(Cat, children[1])
|
953
|
+
end
|
954
|
+
|
955
|
+
it 'serializes' do
|
956
|
+
model = create_model!
|
957
|
+
view = serialize(ModelView.new(model))
|
958
|
+
expected_view = {
|
959
|
+
'id' => 1, '_type' => 'Model', '_version' => 1, 'name' => 'p',
|
960
|
+
'children' => [
|
961
|
+
{ 'id' => 1, '_type' => 'Dog', '_version' => 1, 'dog_number' => 1 },
|
962
|
+
{ 'id' => 2, '_type' => 'Cat', '_version' => 1, 'cat_number' => 2 },
|
963
|
+
]
|
964
|
+
}
|
965
|
+
assert_equal(expected_view, view)
|
966
|
+
end
|
967
|
+
|
968
|
+
it 'creates from view' do
|
969
|
+
view = {
|
970
|
+
'_type' => 'Model',
|
971
|
+
'name' => 'p',
|
972
|
+
'children' => [
|
973
|
+
{ '_type' => 'Dog', 'dog_number' => 1 },
|
974
|
+
{ '_type' => 'Cat', 'cat_number' => 2 },
|
975
|
+
],
|
976
|
+
}
|
977
|
+
|
978
|
+
pv = ModelView.deserialize_from_view(view)
|
979
|
+
p = pv.model
|
980
|
+
|
981
|
+
assert(!p.changed?)
|
982
|
+
assert(!p.new_record?)
|
983
|
+
|
984
|
+
assert_equal('p', p.name)
|
985
|
+
|
986
|
+
children = p.children.order(:position)
|
987
|
+
|
988
|
+
assert_equal(2, children.size)
|
989
|
+
assert_kind_of(Dog, children[0])
|
990
|
+
assert_equal(1, children[0].dog_number)
|
991
|
+
assert_kind_of(Cat, children[1])
|
992
|
+
assert_equal(2, children[1].cat_number)
|
993
|
+
end
|
994
|
+
|
995
|
+
it 'updates with reordering' do
|
996
|
+
model = create_model!
|
997
|
+
|
998
|
+
alter_by_view!(ModelView, model) do |view, _refs|
|
999
|
+
view['children'].reverse!
|
1000
|
+
end
|
1001
|
+
|
1002
|
+
children = model.children.order(:position)
|
1003
|
+
assert_equal(2, children.size)
|
1004
|
+
assert_kind_of(Cat, children[0])
|
1005
|
+
assert_equal(2, children[0].cat_number)
|
1006
|
+
assert_kind_of(Dog, children[1])
|
1007
|
+
assert_equal(1, children[1].dog_number)
|
1008
|
+
end
|
1009
|
+
|
1010
|
+
it 'functional updates' do
|
1011
|
+
model = create_model!
|
1012
|
+
|
1013
|
+
alter_by_view!(ModelView, model) do |view, _refs|
|
1014
|
+
view['children'] = build_fupdate do
|
1015
|
+
append([{ '_type' => 'Cat', 'cat_number' => 100 }])
|
1016
|
+
end
|
1017
|
+
end
|
1018
|
+
|
1019
|
+
assert_equal(3, model.children.size)
|
1020
|
+
new_child = model.children.order(:position).last
|
1021
|
+
assert_kind_of(Cat, new_child)
|
1022
|
+
assert_equal(100, new_child.cat_number)
|
1023
|
+
end
|
1024
|
+
|
1025
|
+
it 'calculates eager_includes' do
|
1026
|
+
includes = viewmodel_class.eager_includes
|
1027
|
+
expected = DeepPreloader::Spec.new(
|
1028
|
+
'children' => DeepPreloader::PolymorphicSpec.new(
|
1029
|
+
{
|
1030
|
+
'Dog' => DeepPreloader::Spec.new,
|
1031
|
+
'Cat' => DeepPreloader::Spec.new,
|
1032
|
+
}))
|
1033
|
+
|
1034
|
+
assert_equal(includes, expected)
|
1035
|
+
end
|
1036
|
+
end
|
1037
|
+
|
868
1038
|
describe 'owned reference children' do
|
869
1039
|
def child_attributes
|
870
1040
|
super.merge(viewmodel: ->(_v) { root! })
|
@@ -118,7 +118,7 @@ class ViewModel::ActiveRecord::HasManyThroughPolyTest < ActiveSupport::TestCase
|
|
118
118
|
def test_loading_batching
|
119
119
|
context = ParentView.new_serialize_context
|
120
120
|
log_queries do
|
121
|
-
parent_views = ParentView.load
|
121
|
+
parent_views = ParentView.load
|
122
122
|
serialize(parent_views, serialize_context: context)
|
123
123
|
end
|
124
124
|
|
@@ -112,7 +112,7 @@ class ViewModel::ActiveRecord::HasManyThroughTest < ActiveSupport::TestCase
|
|
112
112
|
def test_loading_batching
|
113
113
|
context = ParentView.new_serialize_context
|
114
114
|
log_queries do
|
115
|
-
parent_views = ParentView.load
|
115
|
+
parent_views = ParentView.load
|
116
116
|
serialize(parent_views, serialize_context: context)
|
117
117
|
end
|
118
118
|
|
@@ -494,7 +494,7 @@ class ViewModel::CallbacksTest < ActiveSupport::TestCase
|
|
494
494
|
let(:callback) { CallbackCrasher.new }
|
495
495
|
|
496
496
|
it 'raises the callback error' do
|
497
|
-
|
497
|
+
_(-> { serialize(vm) }).must_raise(Crash)
|
498
498
|
end
|
499
499
|
|
500
500
|
describe 'with an access control that rejects' do
|
@@ -503,7 +503,7 @@ class ViewModel::CallbacksTest < ActiveSupport::TestCase
|
|
503
503
|
end
|
504
504
|
|
505
505
|
it 'fails access control first' do
|
506
|
-
|
506
|
+
_(-> { serialize(vm) }).must_raise(ViewModel::AccessControlError)
|
507
507
|
end
|
508
508
|
|
509
509
|
describe 'and a view-mutating callback that crashes' do
|
@@ -514,7 +514,7 @@ class ViewModel::CallbacksTest < ActiveSupport::TestCase
|
|
514
514
|
let(:callback) { MutatingCrasher.new }
|
515
515
|
|
516
516
|
it 'raises the callback error first' do
|
517
|
-
|
517
|
+
_(-> { serialize(vm) }).must_raise(Crash)
|
518
518
|
end
|
519
519
|
end
|
520
520
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: iknow_view_models
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.3
|
4
|
+
version: 3.4.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- iKnow Team
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-06-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -58,14 +58,14 @@ dependencies:
|
|
58
58
|
requirements:
|
59
59
|
- - ">="
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: 1.0.
|
61
|
+
version: 1.0.2
|
62
62
|
type: :runtime
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: 1.0.
|
68
|
+
version: 1.0.2
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: iknow_cache
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|