neo4jrb/neo4j

View on GitHub
lib/active_graph/relationship/persistence/query_factory.rb

Summary

Maintainability
A
25 mins
Test Coverage
module ActiveGraph::Relationship::Persistence
  # This class builds and executes a Cypher query, using information from the graph objects to determine
  #   whether they need to be created simultaneously.
  #   It keeps the rel instance from being responsible for inspecting the nodes or talking with Shared::QueryFactory.
  class QueryFactory
    NODE_SYMBOLS = [:from_node, :to_node]
    attr_reader :from_node, :to_node, :rel, :unwrapped_rel

    def initialize(from_node, to_node, rel)
      @from_node = from_node
      @to_node = to_node
      @rel = rel
    end

    # TODO: This feels like it should also wrap the rel, but that is handled in ActiveGraph::Relationship::Persistence at the moment.
    # Builds and executes the query using the objects giving during init.
    # It holds the process:
    # * Execute node callbacks if needed
    # * Create and execute the query
    # * Mix the query response into the unpersisted objects given during init
    def build!
      node_before_callbacks! do
        res = query_factory(rel, rel_id, iterative_query).query.unwrapped.return(*unpersisted_return_ids).first
        node_symbols.each { |n| wrap!(send(n), res, n) }
        @unwrapped_rel = res[rel_id]
      end
    end

    private

    def rel_id
      @rel_id ||= rel.rel_identifier
    end

    # Node callbacks only need to be executed if the node is not persisted. We let the `conditional_callback` method do the work,
    #   we only have to give it the type of callback we expect to be run and the condition which, if true, will prevent it from executing.
    def node_before_callbacks!
      validate_unpersisted_nodes!
      from_node.conditional_callback(:create, from_node.persisted?) do
        to_node.conditional_callback(:create, to_node.persisted?) do
          yield
        end
      end
    end

    def validate_unpersisted_nodes!
      node_symbols.each do |s|
        obj = send(s)
        next if obj.persisted?
        fail RelCreateFailedError, "Cannot create rel with unpersisted, invalid #{s}" unless obj.valid?
      end
    end

    # Each node must be either created or matched before the relationship can be created. This class does not know or care about
    #   how that happens, it just knows that it needs a usable ActiveGraph::Core::Query object to do that.
    # This method is "iterative" because it creates one factory for each node but the second builds upon the first.
    def iterative_query
      node_symbols.inject(false) do |iterative_query, sym|
        obj = send(sym)
        query_factory(obj, sym, iterative_query)
      end
    end

    # Isolates the dependency to the shared class. This has an awareness of ActiveGraph::Core::Query and will match or create
    #   based on the current state of the object passed in.
    def query_factory(obj, sym, factory = false)
      ActiveGraph::Shared::QueryFactory.create(obj, sym).tap do |factory_instance|
        factory_instance.base_query = factory.blank? ? false : factory.query
      end
    end

    # @return [Array<Symbol>] The Cypher identifiers that will be returned from the query.
    # We only need to return objects from our query that were created during it, otherwise we impact performance.
    def unpersisted_return_ids
      [rel_id].tap do |result|
        node_symbols.each { |k| result << k unless send(k).persisted? }
      end
    end

    # @param [ActiveGraph::Node] node A node, persisted or unpersisted
    # @param [Struct] res The result of calling `return` on a ActiveGraph::Core::Query object. It responds to the same keys
    #   as our graph objects. If the object is unpersisted and was created during the query, the unwrapped node is mixed
    #   in, making the object reflect as "persisted".
    # @param [Symbol] key :from_node or :to_node, the object to request from the response.
    def wrap!(node, res, key)
      return if node.persisted? || !res.keys.include?(key)
      unwrapped = res[key]
      node.init_on_load(unwrapped, unwrapped.properties)
    end

    def node_symbols
      self.class::NODE_SYMBOLS
    end
  end
end