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

Arguments for ActiveRel.creates_unique, new Association unique options #1038

Merged
merged 7 commits into from
Nov 12, 2015
4 changes: 4 additions & 0 deletions lib/neo4j.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
require 'neo4j/timestamps'

require 'neo4j/shared/callbacks'
require 'neo4j/shared/filtered_hash'
require 'neo4j/shared/declared_property/index'
require 'neo4j/shared/declared_property'
require 'neo4j/shared/declared_properties'
Expand All @@ -42,6 +43,7 @@
require 'neo4j/shared/typecaster'
require 'neo4j/shared/initialize'
require 'neo4j/shared/query_factory'
require 'neo4j/shared/cypher'
require 'neo4j/shared'

require 'neo4j/active_rel/callbacks'
Expand Down Expand Up @@ -80,6 +82,8 @@
require 'neo4j/active_node/unpersisted'
require 'neo4j/active_node/has_n'
require 'neo4j/active_node/has_n/association_cypher_methods'
require 'neo4j/active_node/has_n/association/rel_wrapper'
require 'neo4j/active_node/has_n/association/rel_factory'
require 'neo4j/active_node/has_n/association'
require 'neo4j/active_node/query/query_proxy'
require 'neo4j/active_node/query'
Expand Down
8 changes: 8 additions & 0 deletions lib/neo4j/active_node/has_n/association.rb
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,18 @@ def unique?
@origin ? origin_association.unique? : !!@unique
end

def creates_unique_option
@unique || :none
end

def create_method
unique? ? :create_unique : :create
end

def _create_relationship(start_object, node_or_nodes, properties)
RelFactory.create(start_object, node_or_nodes, properties, self)
end

def relationship_class?
!!relationship_class
end
Expand Down
61 changes: 61 additions & 0 deletions lib/neo4j/active_node/has_n/association/rel_factory.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
module Neo4j::ActiveNode::HasN
class Association
class RelFactory
[:start_object, :other_node_or_nodes, :properties, :association].tap do |accessors|
attr_reader(*accessors)
private(*accessors)
end

def self.create(start_object, other_node_or_nodes, properties, association)
factory = new(start_object, other_node_or_nodes, properties, association)
factory._create_relationship
end

def _create_relationship
creator = association.relationship_class ? :rel_class : :factory
send(:"_create_relationship_with_#{creator}")
end

private

def initialize(start_object, other_node_or_nodes, properties, association)
@start_object = start_object
@other_node_or_nodes = other_node_or_nodes
@properties = properties
@association = association
end

def _create_relationship_with_rel_class
Array(other_node_or_nodes).each do |other_node|
node_props = _nodes_for_create(other_node, :from_node, :to_node)
association.relationship_class.create(properties.merge(node_props))
end
end

def _create_relationship_with_factory
Array(other_node_or_nodes).each do |other_node|
wrapper = _rel_wrapper(properties)
base = _match_query(other_node, wrapper)
factory = Neo4j::Shared::RelQueryFactory.new(wrapper, wrapper.rel_identifier)
factory.base_query = base
factory.query.exec
end
end

def _match_query(other_node, wrapper)
nodes = _nodes_for_create(other_node, wrapper.from_node_identifier, wrapper.to_node_identifier)
Neo4j::Session.current.query.match_nodes(nodes)
end

def _nodes_for_create(other_node, from_node_id, to_node_id)
nodes = [@start_object, other_node]
nodes.reverse! if association.direction == :in
{from_node_id => nodes[0], to_node_id => nodes[1]}
end

def _rel_wrapper(properties)
Neo4j::ActiveNode::HasN::Association::RelWrapper.new(association, properties)
end
end
end
end
23 changes: 23 additions & 0 deletions lib/neo4j/active_node/has_n/association/rel_wrapper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class Neo4j::ActiveNode::HasN::Association
# Provides the interface needed to interact with the ActiveRel query factory.
class RelWrapper
include Neo4j::Shared::Cypher::RelIdentifiers
include Neo4j::Shared::Cypher::CreateMethod

attr_reader :type, :association
attr_accessor :properties
private :association
alias_method :props_for_create, :properties

def initialize(association, properties = {})
@association = association
@properties = properties
@type = association.relationship_type(true)
creates_unique(association.creates_unique_option) if association.unique?
end

def persisted?
false
end
end
end
16 changes: 1 addition & 15 deletions lib/neo4j/active_node/query/query_proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -206,21 +206,7 @@ def _nodeify!(*args)
end

def _create_relationship(other_node_or_nodes, properties)
if association.relationship_class
_create_relationship_with_rel_class(other_node_or_nodes, properties)
else
_session.query(context: @options[:context])
.match(:start, :end).match_nodes(start: @start_object, end: other_node_or_nodes)
.send(association.create_method, "start#{_association_arrow(properties, true)}end").exec
end
end

def _create_relationship_with_rel_class(other_node_or_nodes, properties)
Array(other_node_or_nodes).each do |other_node|
node_props = (association.direction == :in) ? {from_node: other_node, to_node: @start_object} : {from_node: @start_object, to_node: other_node}

association.relationship_class.create(properties.merge(node_props))
end
association._create_relationship(@start_object, other_node_or_nodes, properties)
end

def read_attribute_for_serialization(*args)
Expand Down
11 changes: 9 additions & 2 deletions lib/neo4j/active_rel/persistence.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
module Neo4j::ActiveRel
module Persistence
extend ActiveSupport::Concern
include Neo4j::Shared::Cypher::RelIdentifiers
include Neo4j::Shared::Persistence

attr_writer :from_node_identifier, :to_node_identifier

class RelInvalidError < RuntimeError; end
class ModelClassInvalidError < RuntimeError; end
class RelCreateFailedError < RuntimeError; end
Expand All @@ -17,6 +16,14 @@ def to_node_identifier
@to_node_identifier || :to_node
end

def from_node_identifier=(id)
@from_node_identifier = id.to_sym
end

def to_node_identifier=(id)
@to_node_identifier = id.to_sym
end

def cypher_identifier
@cypher_identifier || :rel
end
Expand Down
2 changes: 1 addition & 1 deletion lib/neo4j/active_rel/persistence/query_factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def build!
private

def rel_id
@rel_id ||= rel.cypher_identifier
@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,
Expand Down
26 changes: 6 additions & 20 deletions lib/neo4j/active_rel/property.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,13 @@ def initialize(attributes = nil)
send_props(@relationship_props) unless @relationship_props.nil?
end

def creates_unique_option
self.class.creates_unique_option
end

module ClassMethods
include Neo4j::Shared::Cypher::CreateMethod

# Extracts keys from attributes hash which are relationships of the model
# TODO: Validate separately that relationships are getting the right values? Perhaps also store the values and persist relationships on save?
def extract_association_attributes!(attributes)
Expand Down Expand Up @@ -62,26 +68,6 @@ def id_property_name
def load_entity(id)
Neo4j::Node.load(id)
end

def creates_unique
@creates_unique = true
end

def creates_unique_rel
warning = <<-WARNING
creates_unique_rel() is deprecated and will be removed from future releases,
use creates_unique() instead.
WARNING

ActiveSupport::Deprecation.warn(warning, caller)

creates_unique
end

def creates_unique?
!!@creates_unique
end
alias_method :unique?, :creates_unique?
end

private
Expand Down
37 changes: 37 additions & 0 deletions lib/neo4j/shared/cypher.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
module Neo4j::Shared
module Cypher
module CreateMethod
def create_method
creates_unique? ? :create_unique : :create
end

def creates_unique(option = :none)
option = :none if option == true
@creates_unique = option
end

def creates_unique_option
@creates_unique || :none
end

def creates_unique?
!!@creates_unique
end
alias_method :unique?, :creates_unique?
end

module RelIdentifiers
extend ActiveSupport::Concern

[:from_node, :to_node, :rel].each do |element|
define_method("#{element}_identifier") do
instance_variable_get(:"@#{element}_identifier") || element
end

define_method("#{element}_identifier=") do |id|
instance_variable_set(:"@#{element}_identifier", id.to_sym)
end
end
end
end
end
79 changes: 79 additions & 0 deletions lib/neo4j/shared/filtered_hash.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
module Neo4j::Shared
class FilteredHash
class InvalidHashFilterType < Neo4j::Neo4jrbError; end
VALID_SYMBOL_INSTRUCTIONS = [:all, :none]
VALID_HASH_INSTRUCTIONS = [:on]
VALID_INSTRUCTIONS_TYPES = [Hash, Symbol]

attr_reader :base, :instructions, :instructions_type

def initialize(base, instructions)
@base = base
@instructions = instructions
@instructions_type = instructions.class
validate_instructions!(instructions)
end

def filtered_base
case instructions
when Symbol
filtered_base_by_symbol
when Hash
filtered_base_by_hash
end
end

private

def filtered_base_by_symbol
case instructions
when :all
[base, {}]
when :none
[{}, base]
end
end

def filtered_base_by_hash
behavior_key = instructions.keys.first
filter_keys = keys_array(behavior_key)
[filter(filter_keys, :with), filter(filter_keys, :without)]
end

def key?(filter_keys, key)
filter_keys.include?(key)
end

def filter(filter_keys, key)
filtering = key == :with
base.select { |k, _v| key?(filter_keys, k) == filtering }
end

def keys_array(key)
instructions[key].is_a?(Array) ? instructions[key] : [instructions[key]]
end

def validate_instructions!(instructions)
fail InvalidHashFilterType, "Filtering instructions #{instructions} are invalid" unless VALID_INSTRUCTIONS_TYPES.include?(instructions.class)
clazz = instructions_type.name.downcase
return if send(:"valid_#{clazz}_instructions?", instructions)
fail InvalidHashFilterType, "Invalid instructions #{instructions}, valid options for #{clazz}: #{send(:"valid_#{clazz}_instructions")}"
end

def valid_symbol_instructions?(instructions)
valid_symbol_instructions.include?(instructions)
end

def valid_hash_instructions?(instructions)
valid_hash_instructions.include?(instructions.keys.first)
end

def valid_symbol_instructions
VALID_SYMBOL_INSTRUCTIONS
end

def valid_hash_instructions
VALID_HASH_INSTRUCTIONS
end
end
end
11 changes: 9 additions & 2 deletions lib/neo4j/shared/query_factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,20 @@ def match_string

def create_query
return match_query if graph_object.persisted?
base_query.send(graph_object.create_method, query_string).params(identifier_params.to_sym => graph_object.props_for_create)
create_props, set_props = filtered_props
base_query.send(graph_object.create_method, query_string).break
.set(identifier => set_props)
.params(:"#{identifier}_create_props" => create_props)
end

private

def filtered_props
Neo4j::Shared::FilteredHash.new(graph_object.props_for_create, graph_object.creates_unique_option).filtered_base
end

def query_string
"#{graph_object.from_node_identifier}-[#{identifier}:#{graph_object.type} {#{identifier_params}}]->#{graph_object.to_node_identifier}"
"#{graph_object.from_node_identifier}-[#{identifier}:#{graph_object.type} {#{identifier}_create_props}]->#{graph_object.to_node_identifier}"
end
end
end
Loading