module ViewModel::ActiveRecord::NestedControllerBase

Controller mixin defining machinery for accessing viewmodels nested under a parent. Used by Singular- and CollectionNestedControllers

Protected Instance Methods

association_data() click to toggle source
# File lib/view_model/active_record/nested_controller_base.rb, line 175
def association_data
  @association_data ||= owner_viewmodel._association_data(association_name)
end
association_name() click to toggle source
# File lib/view_model/active_record/nested_controller_base.rb, line 202
def association_name
  params.fetch(:association_name) { raise ArgumentError.new('No association name from routes') }
end
destroy_association(collection, serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, lock_owner: nil) click to toggle source
# File lib/view_model/active_record/nested_controller_base.rb, line 162
def destroy_association(collection, serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, lock_owner: nil)
  require_external_referenced_association!

  if lock_owner
    owner_viewmodel.find(owner_viewmodel_id, eager_include: false, lock: lock_owner)
  end

  empty_update = collection ? [] : nil
  owner_viewmodel.deserialize_from_view(owner_update_hash(empty_update),
                                        deserialize_context: deserialize_context)
  render_viewmodel(empty_update, serialize_context: serialize_context)
end
owner_update_hash(update) click to toggle source
# File lib/view_model/active_record/nested_controller_base.rb, line 179
def owner_update_hash(update)
  {
    ViewModel::ID_ATTRIBUTE   => owner_viewmodel_id,
    ViewModel::TYPE_ATTRIBUTE => owner_viewmodel.view_name,
    association_name.to_s     => update,
  }
end
owner_viewmodel() click to toggle source
# File lib/view_model/active_record/nested_controller_base.rb, line 197
def owner_viewmodel
  name = params.fetch(:owner_viewmodel) { raise ArgumentError.new("No owner viewmodel present") }
  owner_viewmodel_class_for_name(name.to_s.camelize)
end
owner_viewmodel_class_for_name(name) click to toggle source
# File lib/view_model/active_record/nested_controller_base.rb, line 193
def owner_viewmodel_class_for_name(name)
  ViewModel::Registry.for_view_name(name)
end
owner_viewmodel_id(required: true) click to toggle source
# File lib/view_model/active_record/nested_controller_base.rb, line 187
def owner_viewmodel_id(required: true)
  id_param_name = owner_viewmodel.view_name.underscore + '_id'
  default = required ? {} : { default: nil }
  parse_param(id_param_name, **default)
end
require_external_referenced_association!() click to toggle source
# File lib/view_model/active_record/nested_controller_base.rb, line 206
def require_external_referenced_association!
  unless association_data.referenced? && association_data.external?
    raise ArgumentError.new("Expected referenced external association: '#{association_name}'")
  end
end
show_association(scope: nil, serialize_context: new_serialize_context, lock_owner: nil) { |associated_views| ... } click to toggle source
# File lib/view_model/active_record/nested_controller_base.rb, line 46
def show_association(scope: nil, serialize_context: new_serialize_context, lock_owner: nil)
  require_external_referenced_association!

  associated_views = nil
  pre_rendered = owner_viewmodel.transaction do
    owner_view = owner_viewmodel.find(owner_viewmodel_id, eager_include: false, lock: lock_owner)
    ViewModel::Callbacks.wrap_serialize(owner_view, context: serialize_context) do
      # Association manipulation methods construct child contexts internally
      associated_views = owner_view.load_associated(association_name, scope: scope, serialize_context: serialize_context)

      associated_views = yield(associated_views) if block_given?

      child_context = owner_view.context_for_child(association_name, context: serialize_context)
      prerender_viewmodel(associated_views, serialize_context: child_context)
    end
  end
  render_json_string(pre_rendered)
  associated_views
end
write_association(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, lock_owner: nil) { |association_view| ... } click to toggle source

This method always takes direct update hashes, and returns viewmodels directly.

There's no multi membership, so when viewing the children of a single parent each child can only appear once. This means it's safe to use update hashes directly.

# File lib/view_model/active_record/nested_controller_base.rb, line 72
def write_association(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, lock_owner: nil)
  require_external_referenced_association!

  association_view = nil
  pre_rendered = owner_viewmodel.transaction do
    update_hash, refs = parse_viewmodel_updates

    update_hash =
      ViewModel::ActiveRecord.add_reference_indirection(
        update_hash,
        association_data: association_data,
        references:       refs,
        key:              'write-association',
      )

    owner_view = owner_viewmodel.find(owner_viewmodel_id, eager_include: false, lock: lock_owner)

    association_view = owner_view.replace_associated(association_name, update_hash,
                                                     references: refs,
                                                     deserialize_context: deserialize_context)

    ViewModel::Callbacks.wrap_serialize(owner_view, context: serialize_context) do
      child_context = owner_view.context_for_child(association_name, context: serialize_context)
      ViewModel.preload_for_serialization(association_view)
      association_view = yield(association_view) if block_given?
      prerender_viewmodel(association_view, serialize_context: child_context)
    end
  end
  render_json_string(pre_rendered)
  association_view
end
write_association_bulk(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, lock_owner: nil) { |updated_by_parent_viewmodel| ... } click to toggle source

This method takes direct update hashes for owned associations, and reference hashes for shared associations. The return value matches the input structure.

If an association is referenced and owned, each child may only appear once so each is guaranteed to have a unique update hash. This means it's only safe to use update hashes directly in this case.

# File lib/view_model/active_record/nested_controller_base.rb, line 112
def write_association_bulk(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, lock_owner: nil)
  require_external_referenced_association!

  updated_by_parent_viewmodel = nil

  pre_rendered = owner_viewmodel.transaction do
    updates_by_parent_id, references = parse_bulk_update

    if association_data.owned?
      updates_by_parent_id.transform_values!.with_index do |update_hash, index|
        ViewModel::ActiveRecord.add_reference_indirection(
          update_hash,
          association_data: association_data,
          references:       references,
          key:              "write-association-bulk-#{index}",
        )
      end
    end

    updated_by_parent_viewmodel =
      owner_viewmodel.replace_associated_bulk(
        association_name,
        updates_by_parent_id,
        references:          references,
        deserialize_context: deserialize_context,
      )

    views = updated_by_parent_viewmodel.flat_map { |_parent_viewmodel, updated_views| Array.wrap(updated_views) }

    ViewModel.preload_for_serialization(views)

    updated_by_parent_viewmodel = yield(updated_by_parent_viewmodel) if block_given?

    return_updates = updated_by_parent_viewmodel.map do |owner_view, updated_views|
      ParentProxyModel.new(owner_view, association_data, updated_views)
    end

    return_structure = {
      ViewModel::TYPE_ATTRIBUTE         => ViewModel::BULK_UPDATE_TYPE,
      ViewModel::BULK_UPDATES_ATTRIBUTE => return_updates,
    }

    prerender_viewmodel(return_structure, serialize_context: serialize_context)
  end

  render_json_string(pre_rendered)
  updated_by_parent_viewmodel
end