Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🧹 Make CreateRelationshipJob work for Valkyrie #908

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions app/factories/bulkrax/object_factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,18 @@ def update
def find
found = find_by_id if attributes[:id].present?
return found if found.present?
rescue Valkyrie::Persistence::ObjectNotFoundError
return search_by_identifier if attributes[work_identifier].present?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change fixed the reason why a link was not being displayed on a successful import. Before it would leave a message that the work didn't successfully import when that wasn't true.

image

false
ensure
search_by_identifier if attributes[work_identifier].present?
end

def find_by_id
# TODO: Push logic into Bulkrax.persistence_adapter; maybe
# Rails / Ruby upgrade, we moved from :exists? to :exist? However we want to continue (for a
# bit) to support older versions.
method_name = klass.respond_to?(:exist?) ? :exist? : :exists?
klass.find(attributes[:id]) if klass.send(method_name, attributes[:id])
rescue Valkyrie::Persistence::ObjectNotFoundError
false
end

def find_or_create
Expand All @@ -110,11 +111,13 @@ def find_or_create
end

def search_by_identifier
# TODO: Push logic into Bulkrax.persistence_adapter; maybe
query = { work_identifier_search_field =>
source_identifier_value }
# Query can return partial matches (something6 matches both something6 and something68)
# so we need to weed out any that are not the correct full match. But other items might be
# in the multivalued field, so we have to go through them one at a time.
#
match = klass.where(query).detect { |m| m.send(work_identifier).include?(source_identifier_value) }
return match if match
end
Expand Down
34 changes: 31 additions & 3 deletions app/jobs/bulkrax/create_relationships_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,15 @@ def perform(parent_identifier:, importer_run_id:) # rubocop:disable Metrics/AbcS
# save record if members were added
if @parent_record_members_added
parent_record.save!
# TODO: Push logic into Bulkrax.persistence_adapter
# Ensure that the new relationship gets indexed onto the children
@child_members_added.each(&:update_index)
if parent_record.is_a?(Valkyrie::Resource)
@child_members_added.each do |child|
Hyrax.index_adapter.save(resource: child)
end
else
@child_members_added.each(&:update_index)
end
end
end
else
Expand Down Expand Up @@ -165,13 +172,34 @@ def add_to_collection(child_record, parent_record)
end

def add_to_work(child_record, parent_record)
return true if parent_record.ordered_members.to_a.include?(child_record)
parent_record.is_a?(Valkyrie::Resource) ? add_to_valkyrie_work(child_record, parent_record) : add_to_af_work(child_record, parent_record)

parent_record.ordered_members << child_record
@parent_record_members_added = true
@child_members_added << child_record
end

def add_to_valkyrie_work(child_record, parent_record)
return true if parent_record.member_ids.include?(child_record.id)

parent_record.member_ids << child_record.id

# TODO: Hyrax is in the process of extracting an "Action" object that we could call. It does
# provide validation that we may want to consider.
#
# NOTE: We may need to look at the step args we're passing, see
# `Hyrax::WorksControllerBehavior#update_valkyrie_work`
# Hyrax's `./app/controllers/concerns/hyrax/works_controller_behavior.rb`
#
change_set = Hyrax::ChangeSet.for(parent_record)
Hyrax::Transactions::Container['change_set.update_work'].call(change_set)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm curious about this choice. won't this ensure that the entire update work process (the same one that happens via the controller action) takes place just to change the work membership?

Hyrax::Publisher provides an object.membership.updated event, and i wonder whether it wouldn't be better to save the work with the updated membership and publish that? https://github.com/samvera/hyrax/blob/fdcabe651850b1885d7bd3e1b36777bf5f336ee8/lib/hyrax/publisher.rb#L183-L192

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@no-reply, for our uses, we're trying to hit this transaction, so you're suggesting we put a publisher here and then that means this can become a listener?

Copy link
Contributor Author

@kirkkwang kirkkwang Feb 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here was the original idea of the listener

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kirkkwang yes. i think what i'm suggesting (and i'm just popping in as an outsider here), is that you want this behavior to trigger whenever an object's membership changes, not only when a given transaction runs.

making it a listener means you'll trigger these changes when outside callers (in Hyrax or in the downstream application) change membership, as long as they dutifully publish the expected message.

i think a helpful line to draw around transactions and listeners is:

  • use a transaction step when you want to EXPLICITLY implement some behavior as part of a given operation, but you might want to (explicitly) implement different behavior in a different context.
    • e.g. i might want to set the current_user as the "depositor" every time a create action happens in the hyrax works controller, but handle this matter differently if i create an object through an API endpoint.
    • it would be normal for the Hyrax engine, Bulkrax's own code, or the downstream application to call different transactions that may or may not include steps the others might use.
  • use a listener when you want to ensure some behavior always runs in response to some event, regardless of that event's context.
    • e.g. whenever metadata updates for an object, i want to reindex that object.

another way of thinking about it is:

  • if in a naive MVC architecture i would have just written the code in the controller, or as a model method, like do_this; do_that; do_other, then you want that to be a transaction step.
  • if in a naive MVC architecture i would have written a callback, you want a listener. (it might be helpful to think of a listener as a multiplexed callback, decoupled from the model layer).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @no-reply that's super helpful!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI I made a ticket here scientist-softserv/iiif_print#330

Copy link
Contributor Author

@kirkkwang kirkkwang Feb 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@no-reply should this publish 'object.membership.updated'

end

def add_to_af_work(child_record, parent_record)
return true if parent_record.ordered_members.to_a.include?(child_record)

parent_record.ordered_members << child_record
end

def reschedule(parent_identifier:, importer_run_id:)
CreateRelationshipsJob.set(wait: 10.minutes).perform_later(
parent_identifier: parent_identifier,
Expand Down
2 changes: 1 addition & 1 deletion lib/bulkrax/persistence_layer/active_fedora_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class ActiveFedoraAdapter < AbstractAdapter
def self.find(id)
ActiveFedora::Base.find(id)
rescue ActiveFedora::ObjectNotFoundError => e
raise PersistenceLayer::RecordNotFound, e.message
raise PersistenceLayer::ObjectNotFoundError, e.message
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!

end

def self.query(q, **kwargs)
Expand Down
Loading