Skip to content

Commit

Permalink
Merge pull request #1038 from neo4jrb/creates_unique_with_props
Browse files Browse the repository at this point in the history
Arguments for ActiveRel.creates_unique, new Association `unique` options
  • Loading branch information
subvertallchris committed Nov 12, 2015
2 parents 64d4eb1 + 0b8411b commit 3c3f637
Show file tree
Hide file tree
Showing 17 changed files with 516 additions and 43 deletions.
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

0 comments on commit 3c3f637

Please sign in to comment.