iknow_view_models 3.4.4 → 3.5.0
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/lib/iknow_view_models/version.rb +1 -1
- data/lib/view_model.rb +12 -0
- data/lib/view_model/active_record.rb +46 -5
- data/lib/view_model/active_record/association_manipulation.rb +181 -44
- data/lib/view_model/active_record/collection_nested_controller.rb +5 -2
- data/lib/view_model/active_record/controller_base.rb +45 -6
- data/lib/view_model/active_record/nested_controller_base.rb +125 -13
- data/lib/view_model/active_record/singular_nested_controller.rb +5 -2
- data/lib/view_model/callbacks.rb +1 -1
- data/lib/view_model/controller.rb +17 -0
- data/lib/view_model/record.rb +1 -1
- data/lib/view_model/schemas.rb +44 -0
- data/test/helpers/arvm_test_utilities.rb +65 -0
- data/test/helpers/controller_test_helpers.rb +65 -34
- data/test/unit/view_model/active_record/controller_nested_test.rb +599 -0
- data/test/unit/view_model/active_record/controller_test.rb +6 -362
- data/test/unit/view_model/active_record/has_many_test.rb +10 -5
- data/test/unit/view_model/active_record/has_many_through_test.rb +28 -12
- data/test/unit/view_model/traversal_context_test.rb +15 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0cb094f674e990ef9a67bbcb7b0ee260065356cdb6a56ee6e1b19582c320e96c
|
4
|
+
data.tar.gz: 5bb5c175e7fd30442794c2ff87687eb5dec132fa7da0d0128a1b012bd50131d6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f196d4af404bcb91d88519e04ed2afcd7d73c590af4838e16b32b4f73171d20500a4a092cb76d693d7a683bf4ca6ad337d5e79a10a5233870f9c244acfffe969
|
7
|
+
data.tar.gz: bfdd726357dc50f72732498d112274301089ab82719e46d7c5d7bfc5414a6bcf3dc12459bb9dee6ffa9c174dea1c81e2a0499649d9bbe1d1618702bfad18ef04
|
data/lib/view_model.rb
CHANGED
@@ -12,6 +12,10 @@ class ViewModel
|
|
12
12
|
VERSION_ATTRIBUTE = '_version'
|
13
13
|
NEW_ATTRIBUTE = '_new'
|
14
14
|
|
15
|
+
BULK_UPDATE_TYPE = '_bulk_update'
|
16
|
+
BULK_UPDATES_ATTRIBUTE = 'updates'
|
17
|
+
BULK_UPDATE_ATTRIBUTE = 'update'
|
18
|
+
|
15
19
|
# Migrations leave a metadata attribute _migrated on any views that they
|
16
20
|
# alter. This attribute is accessible as metadata when deserializing migrated
|
17
21
|
# input, and is included in the output serialization sent to clients.
|
@@ -27,6 +31,14 @@ class ViewModel
|
|
27
31
|
attr_reader :view_aliases
|
28
32
|
attr_writer :view_name
|
29
33
|
|
34
|
+
# Boolean to indicate if the viewmodel is synthetic. Synthetic
|
35
|
+
# viewmodels are nearly-invisible glue. They're full viewmodels,
|
36
|
+
# but do not participate in hooks or registration. For example, a
|
37
|
+
# join table connecting A and B through T has a synthetic
|
38
|
+
# viewmodel T to represent the join model, but the external
|
39
|
+
# interface is a relationship of A to a list of Bs.
|
40
|
+
attr_accessor :synthetic
|
41
|
+
|
30
42
|
def inherited(subclass)
|
31
43
|
super
|
32
44
|
subclass.initialize_as_viewmodel
|
@@ -35,14 +35,9 @@ class ViewModel::ActiveRecord < ViewModel::Record
|
|
35
35
|
|
36
36
|
class << self
|
37
37
|
attr_reader :_list_attribute_name
|
38
|
-
attr_accessor :synthetic
|
39
38
|
|
40
39
|
delegate :transaction, to: :model_class
|
41
40
|
|
42
|
-
def should_register?
|
43
|
-
super && !synthetic
|
44
|
-
end
|
45
|
-
|
46
41
|
# Specifies that the model backing this viewmodel is a member of an
|
47
42
|
# `acts_as_manual_list` collection.
|
48
43
|
def acts_as_list(attr = :position)
|
@@ -368,6 +363,52 @@ class ViewModel::ActiveRecord < ViewModel::Record
|
|
368
363
|
end
|
369
364
|
end
|
370
365
|
|
366
|
+
# Rails 6.1 introduced "previously_new_record?", but this library still
|
367
|
+
# supports activerecord >= 5.0. This is an approximation.
|
368
|
+
def self.model_previously_new?(model)
|
369
|
+
if (id_changes = model.saved_change_to_id)
|
370
|
+
old_id, _new_id = id_changes
|
371
|
+
return true if old_id.nil?
|
372
|
+
end
|
373
|
+
false
|
374
|
+
end
|
375
|
+
|
376
|
+
# Helper to return entities that were part of the last deserialization. The
|
377
|
+
# interface is complex due to the data requirements, and the implementation is
|
378
|
+
# inefficient.
|
379
|
+
#
|
380
|
+
# Intended to be used by replace_associated style methods which may touch very
|
381
|
+
# large collections that must not be returned fully. Since the collection is
|
382
|
+
# not being returned, order is also ignored.
|
383
|
+
def _read_association_touched(association_name, touched_ids:)
|
384
|
+
association_data = self.class._association_data(association_name)
|
385
|
+
|
386
|
+
associated = model.public_send(association_data.direct_reflection.name)
|
387
|
+
return nil if associated.nil?
|
388
|
+
|
389
|
+
case
|
390
|
+
when association_data.through?
|
391
|
+
# associated here are join-table models; we need to get the far side out
|
392
|
+
associated.map do |through_model|
|
393
|
+
model = through_model.public_send(association_data.indirect_reflection.name)
|
394
|
+
|
395
|
+
next unless self.class.model_previously_new?(through_model) || touched_ids.include?(model.id)
|
396
|
+
|
397
|
+
association_data.viewmodel_class_for_model!(model.class).new(model)
|
398
|
+
end.reject(&:nil?)
|
399
|
+
when association_data.collection?
|
400
|
+
associated.map do |model|
|
401
|
+
next unless self.class.model_previously_new?(model) || touched_ids.include?(model.id)
|
402
|
+
|
403
|
+
association_data.viewmodel_class_for_model!(model.class).new(model)
|
404
|
+
end.reject(&:nil?)
|
405
|
+
else
|
406
|
+
# singleton always touched by definition
|
407
|
+
model = associated
|
408
|
+
association_data.viewmodel_class_for_model!(model.class).new(model)
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
371
412
|
def _serialize_association(association_name, json, serialize_context:)
|
372
413
|
associated = self.public_send(association_name)
|
373
414
|
association_data = self.class._association_data(association_name)
|
@@ -3,6 +3,8 @@
|
|
3
3
|
# Mix-in for VM::ActiveRecord providing direct manipulation of
|
4
4
|
# directly-associated entities. Avoids loading entire collections.
|
5
5
|
module ViewModel::ActiveRecord::AssociationManipulation
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
6
8
|
def load_associated(association_name, scope: nil, eager_include: true, serialize_context: self.class.new_serialize_context)
|
7
9
|
association_data = self.class._association_data(association_name)
|
8
10
|
direct_reflection = association_data.direct_reflection
|
@@ -49,49 +51,63 @@ module ViewModel::ActiveRecord::AssociationManipulation
|
|
49
51
|
end
|
50
52
|
end
|
51
53
|
|
52
|
-
# Replace the current member(s) of an association with the provided
|
54
|
+
# Replace the current member(s) of an association with the provided
|
55
|
+
# hash(es). Only mentioned member(s) will be returned.
|
56
|
+
#
|
57
|
+
# This interface deals with associations directly where reasonable,
|
58
|
+
# with the notable exception of referenced+shared associations. That
|
59
|
+
# is to say, that owned associations should be presented in the form
|
60
|
+
# of direct update hashes, regardless of their
|
61
|
+
# referencing. Reference and shared associations are excluded to
|
62
|
+
# ensure that the update hash for a shared entity is unique, and
|
63
|
+
# that edits may only be specified once.
|
53
64
|
def replace_associated(association_name, update_hash, references: {}, deserialize_context: self.class.new_deserialize_context)
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
action_type_name = action[ViewModel::ActiveRecord::TYPE_ATTRIBUTE]
|
65
|
-
if action_type_name == ViewModel::ActiveRecord::FunctionalUpdate::Remove::NAME
|
66
|
-
# Remove actions are always type/id refs; others need to be translated to proper refs
|
67
|
-
next
|
68
|
-
end
|
65
|
+
_updated_parent, changed_children =
|
66
|
+
self.class.replace_associated_bulk(
|
67
|
+
association_name,
|
68
|
+
{ self.id => update_hash },
|
69
|
+
references: references,
|
70
|
+
deserialize_context: deserialize_context
|
71
|
+
).first
|
72
|
+
|
73
|
+
changed_children
|
74
|
+
end
|
69
75
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
76
|
+
class_methods do
|
77
|
+
# Replace the current member(s) of an association with the provided
|
78
|
+
# hash(es) for many viewmodels. Only mentioned members will be returned.
|
79
|
+
#
|
80
|
+
# This is an interim implementation that requires loading the contents of
|
81
|
+
# all collections into memory and filtering for the mentioned entities,
|
82
|
+
# even for functional updates. This is in contrast to append_associated,
|
83
|
+
# which only operates on the new entities.
|
84
|
+
def replace_associated_bulk(association_name, updates_by_parent_id, references:, deserialize_context: self.class.new_deserialize_context)
|
85
|
+
association_data = _association_data(association_name)
|
86
|
+
|
87
|
+
touched_ids = updates_by_parent_id.each_with_object({}) do |(parent_id, update_hash), acc|
|
88
|
+
acc[parent_id] =
|
89
|
+
mentioned_children(
|
90
|
+
update_hash,
|
91
|
+
references: references,
|
92
|
+
association_data: association_data,
|
93
|
+
).to_set
|
83
94
|
end
|
84
|
-
end
|
85
95
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
96
|
+
root_update_hashes = updates_by_parent_id.map do |parent_id, update_hash|
|
97
|
+
{
|
98
|
+
ViewModel::ID_ATTRIBUTE => parent_id,
|
99
|
+
ViewModel::TYPE_ATTRIBUTE => view_name,
|
100
|
+
association_name.to_s => update_hash,
|
101
|
+
}
|
102
|
+
end
|
91
103
|
|
92
|
-
|
104
|
+
root_update_viewmodels = deserialize_from_view(
|
105
|
+
root_update_hashes, references: references, deserialize_context: deserialize_context)
|
93
106
|
|
94
|
-
|
107
|
+
root_update_viewmodels.each_with_object({}) do |updated, acc|
|
108
|
+
acc[updated] = updated._read_association_touched(association_name, touched_ids: touched_ids.fetch(updated.id))
|
109
|
+
end
|
110
|
+
end
|
95
111
|
end
|
96
112
|
|
97
113
|
# Create or update members of a associated collection. For an ordered
|
@@ -313,7 +329,10 @@ module ViewModel::ActiveRecord::AssociationManipulation
|
|
313
329
|
indirect_update_data, referenced_update_data = ViewModel::ActiveRecord::UpdateData.parse_hashes(subtree_hashes, references)
|
314
330
|
|
315
331
|
# Convert associated update data to references
|
316
|
-
indirect_references =
|
332
|
+
indirect_references =
|
333
|
+
self.class.convert_updates_to_references(
|
334
|
+
indirect_update_data, key: 'indirect_append')
|
335
|
+
|
317
336
|
referenced_update_data.merge!(indirect_references)
|
318
337
|
|
319
338
|
# Find any existing models for the direct association: need to re-use any
|
@@ -352,12 +371,6 @@ module ViewModel::ActiveRecord::AssociationManipulation
|
|
352
371
|
return direct_update_data, referenced_update_data
|
353
372
|
end
|
354
373
|
|
355
|
-
def convert_updates_to_references(indirect_update_data, key:)
|
356
|
-
indirect_update_data.each.with_index.with_object({}) do |(update, i), indirect_references|
|
357
|
-
indirect_references["__#{key}_ref_#{i}"] = update
|
358
|
-
end
|
359
|
-
end
|
360
|
-
|
361
374
|
# TODO: this functionality could reasonably be extracted into `acts_as_manual_list`.
|
362
375
|
def select_append_positions(association_data, position_attr, append_count, before:, after:)
|
363
376
|
direct_reflection = association_data.direct_reflection
|
@@ -401,4 +414,128 @@ module ViewModel::ActiveRecord::AssociationManipulation
|
|
401
414
|
"Need to specify target viewmodel type for polymorphic association '#{association_data.association_name}'")
|
402
415
|
end
|
403
416
|
end
|
417
|
+
|
418
|
+
class_methods do
|
419
|
+
def convert_updates_to_references(indirect_update_data, key:)
|
420
|
+
indirect_update_data.each.with_index.with_object({}) do |(update, i), indirect_references|
|
421
|
+
indirect_references["__#{key}_ref_#{i}"] = update
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
def add_reference_indirection(update_hash, association_data:, references:, key:)
|
426
|
+
raise ArgumentError.new('Not a referenced association') unless association_data.referenced?
|
427
|
+
|
428
|
+
is_fupdate =
|
429
|
+
association_data.collection? &&
|
430
|
+
update_hash.is_a?(Hash) &&
|
431
|
+
update_hash[ViewModel::ActiveRecord::TYPE_ATTRIBUTE] == ViewModel::ActiveRecord::FUNCTIONAL_UPDATE_TYPE
|
432
|
+
|
433
|
+
if is_fupdate
|
434
|
+
update_hash[ViewModel::ActiveRecord::ACTIONS_ATTRIBUTE].each_with_index do |action, i|
|
435
|
+
action_type_name = action[ViewModel::ActiveRecord::TYPE_ATTRIBUTE]
|
436
|
+
if action_type_name == ViewModel::ActiveRecord::FunctionalUpdate::Remove::NAME
|
437
|
+
# Remove actions are always type/id refs; others need to be translated to proper refs
|
438
|
+
next
|
439
|
+
end
|
440
|
+
|
441
|
+
association_references = convert_updates_to_references(
|
442
|
+
action[ViewModel::ActiveRecord::VALUES_ATTRIBUTE],
|
443
|
+
key: "#{key}_#{action_type_name}_#{i}")
|
444
|
+
references.merge!(association_references)
|
445
|
+
action[ViewModel::ActiveRecord::VALUES_ATTRIBUTE] =
|
446
|
+
association_references.each_key.map { |ref| { ViewModel::REFERENCE_ATTRIBUTE => ref } }
|
447
|
+
end
|
448
|
+
|
449
|
+
update_hash
|
450
|
+
else
|
451
|
+
ViewModel::Utils.wrap_one_or_many(update_hash) do |sh|
|
452
|
+
association_references = convert_updates_to_references(sh, key: "#{key}_replace")
|
453
|
+
references.merge!(association_references)
|
454
|
+
association_references.each_key.map { |ref| { ViewModel::REFERENCE_ATTRIBUTE => ref } }
|
455
|
+
end
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
# Traverses literals and fupdates to return referenced children.
|
460
|
+
#
|
461
|
+
# Runs before the main parser, so must be defensive
|
462
|
+
def each_child_hash(assoc_update, association_data:)
|
463
|
+
return enum_for(__method__, assoc_update, association_data: association_data) unless block_given?
|
464
|
+
|
465
|
+
is_fupdate =
|
466
|
+
association_data.collection? &&
|
467
|
+
assoc_update.is_a?(Hash) &&
|
468
|
+
assoc_update[ViewModel::ActiveRecord::TYPE_ATTRIBUTE] == ViewModel::ActiveRecord::FUNCTIONAL_UPDATE_TYPE
|
469
|
+
|
470
|
+
if is_fupdate
|
471
|
+
assoc_update.fetch(ViewModel::ActiveRecord::ACTIONS_ATTRIBUTE).each do |action|
|
472
|
+
action_type_name = action[ViewModel::ActiveRecord::TYPE_ATTRIBUTE]
|
473
|
+
if action_type_name.nil?
|
474
|
+
raise ViewModel::DeserializationError::InvalidSyntax.new(
|
475
|
+
"Functional update missing '#{ViewModel::ActiveRecord::TYPE_ATTRIBUTE}'"
|
476
|
+
)
|
477
|
+
end
|
478
|
+
|
479
|
+
if action_type_name == ViewModel::ActiveRecord::FunctionalUpdate::Remove::NAME
|
480
|
+
# Remove actions are not considered children of the action.
|
481
|
+
next
|
482
|
+
end
|
483
|
+
|
484
|
+
values = action.fetch(ViewModel::ActiveRecord::VALUES_ATTRIBUTE) {
|
485
|
+
raise ViewModel::DeserializationError::InvalidSyntax.new(
|
486
|
+
"Functional update missing '#{ViewModel::ActiveRecord::VALUES_ATTRIBUTE}'"
|
487
|
+
)
|
488
|
+
}
|
489
|
+
values.each { |x| yield x }
|
490
|
+
end
|
491
|
+
else
|
492
|
+
ViewModel::Utils.wrap_one_or_many(assoc_update) do |assoc_updates|
|
493
|
+
assoc_updates.each { |u| yield u }
|
494
|
+
end
|
495
|
+
end
|
496
|
+
end
|
497
|
+
|
498
|
+
# Collects the ids of children that are mentioned in the update data.
|
499
|
+
#
|
500
|
+
# Runs before the main parser, so must be defensive.
|
501
|
+
def mentioned_children(assoc_update, references:, association_data:)
|
502
|
+
return enum_for(__method__, assoc_update, references: references, association_data: association_data) unless block_given?
|
503
|
+
|
504
|
+
each_child_hash(assoc_update, association_data: association_data).each do |child_hash|
|
505
|
+
unless child_hash.is_a?(Hash)
|
506
|
+
raise ViewModel::DeserializationError::InvalidSyntax.new(
|
507
|
+
"Expected update hash, received: #{child_hash}"
|
508
|
+
)
|
509
|
+
end
|
510
|
+
|
511
|
+
if association_data.referenced?
|
512
|
+
ref_handle = child_hash.fetch(ViewModel::REFERENCE_ATTRIBUTE) {
|
513
|
+
raise ViewModel::DeserializationError::InvalidSyntax.new(
|
514
|
+
"Reference hash missing '#{ViewModel::REFERENCE_ATTRIBUTE}'"
|
515
|
+
)
|
516
|
+
}
|
517
|
+
|
518
|
+
ref_update_hash = references.fetch(ref_handle) {
|
519
|
+
raise ViewModel::DeserializationError::InvalidSyntax.new(
|
520
|
+
"Reference '#{ref_handle}' does not exist in references"
|
521
|
+
)
|
522
|
+
}
|
523
|
+
|
524
|
+
unless ref_update_hash.is_a?(Hash)
|
525
|
+
raise ViewModel::DeserializationError::InvalidSyntax.new(
|
526
|
+
"Expected update hash, received: #{child_hash}"
|
527
|
+
)
|
528
|
+
end
|
529
|
+
|
530
|
+
if (id = ref_update_hash[ViewModel::ID_ATTRIBUTE])
|
531
|
+
yield id
|
532
|
+
end
|
533
|
+
else
|
534
|
+
if (id = child_hash[ViewModel::ID_ATTRIBUTE])
|
535
|
+
yield id
|
536
|
+
end
|
537
|
+
end
|
538
|
+
end
|
539
|
+
end
|
540
|
+
end
|
404
541
|
end
|
@@ -3,8 +3,7 @@
|
|
3
3
|
require 'view_model/active_record/nested_controller_base'
|
4
4
|
|
5
5
|
# Controller mixin for accessing a root ViewModel which can be accessed in a
|
6
|
-
# collection by a parent model.
|
7
|
-
# :children` on the viewmodel controller
|
6
|
+
# collection by a parent model.
|
8
7
|
|
9
8
|
# Contributes the following routes:
|
10
9
|
# PUT /parents/:parent_id/children #append -- deserialize (possibly existing) children and append to collection
|
@@ -32,6 +31,10 @@ module ViewModel::ActiveRecord::CollectionNestedController
|
|
32
31
|
write_association(serialize_context: serialize_context, deserialize_context: deserialize_context, lock_owner: lock_owner, &block)
|
33
32
|
end
|
34
33
|
|
34
|
+
def replace_bulk(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, &block)
|
35
|
+
write_association_bulk(serialize_context: serialize_context, deserialize_context: deserialize_context, &block)
|
36
|
+
end
|
37
|
+
|
35
38
|
def disassociate_all(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, lock_owner: nil)
|
36
39
|
destroy_association(true, serialize_context: serialize_context, deserialize_context: deserialize_context, lock_owner: lock_owner)
|
37
40
|
end
|
@@ -102,15 +102,35 @@ module ActionDispatch
|
|
102
102
|
def arvm_resources(resource_name, options = {}, &block)
|
103
103
|
except = options.delete(:except) { [] }
|
104
104
|
add_shallow_routes = options.delete(:add_shallow_routes) { true }
|
105
|
+
defaults = options.delete(:defaults) { {} }
|
105
106
|
|
106
107
|
nested = shallow_nesting_depth > 0
|
107
108
|
|
109
|
+
association_name = options.delete(:association_name) { resource_name.to_s }
|
110
|
+
owner_viewmodel = options.delete(:owner_viewmodel) do
|
111
|
+
if nested
|
112
|
+
parent_resource.name.to_s.singularize.camelize
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
defaults = {
|
117
|
+
association_name: association_name,
|
118
|
+
owner_viewmodel: owner_viewmodel,
|
119
|
+
}.merge(defaults)
|
120
|
+
|
108
121
|
only_routes = []
|
109
122
|
only_routes += [:create] unless nested
|
110
123
|
only_routes += [:show, :destroy] if add_shallow_routes
|
111
124
|
only_routes -= except
|
112
125
|
|
113
|
-
|
126
|
+
# Bulk replace
|
127
|
+
if nested && !except.include?(:replace_bulk)
|
128
|
+
collection do
|
129
|
+
post resource_name, controller: resource_name, action: :replace_bulk, as: nil, defaults: defaults
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
resources resource_name, shallow: true, only: only_routes, defaults: defaults, **options do
|
114
134
|
instance_eval(&block) if block_given?
|
115
135
|
|
116
136
|
if nested
|
@@ -149,16 +169,35 @@ module ActionDispatch
|
|
149
169
|
def arvm_resource(resource_name, options = {}, &block)
|
150
170
|
except = options.delete(:except) { [] }
|
151
171
|
add_shallow_routes = options.delete(:add_shallow_routes) { true }
|
172
|
+
defaults = options.delete(:defaults) { {} }
|
152
173
|
|
153
174
|
only_routes = []
|
154
|
-
|
155
|
-
|
156
|
-
|
175
|
+
nested = shallow_nesting_depth > 0
|
176
|
+
|
177
|
+
association_name = options.delete(:association_name) { resource_name.to_s }
|
178
|
+
owner_viewmodel = options.delete(:owner_viewmodel) do
|
179
|
+
if nested
|
180
|
+
parent_resource.name.to_s.singularize.camelize
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
defaults = {
|
185
|
+
association_name: association_name,
|
186
|
+
owner_viewmodel: owner_viewmodel,
|
187
|
+
}.merge(defaults)
|
188
|
+
|
189
|
+
if nested && !except.include?(:create_associated_bulk)
|
190
|
+
collection do
|
191
|
+
post resource_name, controller: resource_name, action: :create_associated_bulk, as: nil, defaults: defaults
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
resource resource_name, shallow: true, only: only_routes, defaults: defaults, **options do
|
157
196
|
instance_eval(&block) if block_given?
|
158
197
|
|
159
198
|
name_route = { as: '' } # Only one route may take the name
|
160
199
|
|
161
|
-
if
|
200
|
+
if nested
|
162
201
|
post('', action: :create_associated, **name_route.extract!(:as)) unless except.include?(:create)
|
163
202
|
get('', action: :show_associated, **name_route.extract!(:as)) unless except.include?(:show)
|
164
203
|
delete('', action: :destroy_associated, **name_route.extract!(:as)) unless except.include?(:destroy)
|
@@ -170,7 +209,7 @@ module ActionDispatch
|
|
170
209
|
end
|
171
210
|
|
172
211
|
# singularly nested resources provide collection accessors at the top level
|
173
|
-
if
|
212
|
+
if nested && add_shallow_routes
|
174
213
|
resources resource_name.to_s.pluralize, shallow: true, only: [:show, :destroy] - except do
|
175
214
|
shallow_scope do
|
176
215
|
collection do
|