From ba1953f350fb7c7b7b51ac693bc1c7dee8ed7b52 Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Fri, 11 Jul 2014 21:57:48 -0700 Subject: [PATCH 01/54] Experimental changes to ActiveNode QueryProxy to allow for association chaining and calling of class methods on QueryProxy objects --- lib/neo4j/active_node/has_n.rb | 25 +++++ lib/neo4j/active_node/query.rb | 18 +++- lib/neo4j/active_node/query/query_proxy.rb | 77 ++++++++++++++- spec/e2e/query_spec.rb | 104 +++++++++++++++++++++ 4 files changed, 218 insertions(+), 6 deletions(-) create mode 100644 spec/e2e/query_spec.rb diff --git a/lib/neo4j/active_node/has_n.rb b/lib/neo4j/active_node/has_n.rb index 2a1caa60e..97f5570cb 100644 --- a/lib/neo4j/active_node/has_n.rb +++ b/lib/neo4j/active_node/has_n.rb @@ -107,6 +107,31 @@ def #{rel_type} _decl_rels[rel_type.to_sym] = DeclRel.new(rel_type, false, clazz) end + def has_many(name, options = {}) + to, from, through = options.values_at(:to, :from, :through) + raise ArgumentError, "Must specify either :to or :from" if not (to || from) + raise ArgumentError, "Cannot specify both :to and :from" if to && from + + target_class = to || from + direction = to ? :outbound : :inbound + + # TODO: auto-set through when missing + + @has_many_relationships ||= [] + @has_many_relationships << name + + module_eval(%Q{ + def #{name} + Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class.name}, start_object: self, relationship: #{through.inspect}, direction: #{direction.inspect}) + end}, __FILE__, __LINE__) + + instance_eval(%Q{ + def #{name} + Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class.name}, query_proxy: self.query_proxy, relationship: #{through.inspect}, direction: #{direction.inspect}) + end}, __FILE__, __LINE__) + + end + # Specifies a relationship between two node classes. # Generates assignment and accessor methods for the given relationship diff --git a/lib/neo4j/active_node/query.rb b/lib/neo4j/active_node/query.rb index 16f886231..eefd8b2b9 100644 --- a/lib/neo4j/active_node/query.rb +++ b/lib/neo4j/active_node/query.rb @@ -25,6 +25,14 @@ def query_as(var) end module ClassMethods + include Enumerable + + attr_writer :query_proxy + + def each + self.query_as(:n).pluck(:n).each {|o| yield o } + end + # Returns a Query object with all nodes for the model matched as the specified variable name # # @example Return the registration number of all cars owned by a person over the age of 30 @@ -34,20 +42,24 @@ module ClassMethods # @param var [Symbol, String] The variable name to specify in the query # @return [Neo4j::Core::Query] def query_as(var) - label = self.respond_to?(:mapped_label_name) ? self.mapped_label_name : self - neo4j_session.query.match(var => label) + query_proxy.query_as(var) end Neo4j::ActiveNode::Query::QueryProxy::METHODS.each do |method| module_eval(%Q{ def #{method}(*args) - Neo4j::ActiveNode::Query::QueryProxy.new(self).#{method}(*args) + self.query_proxy.#{method}(*args) end}, __FILE__, __LINE__) end + def query_proxy + @query_proxy || Neo4j::ActiveNode::Query::QueryProxy.new(self) + end + def qq(as = :n1) QuickQuery.new(self.name.constantize, as) end + end end end diff --git a/lib/neo4j/active_node/query/query_proxy.rb b/lib/neo4j/active_node/query/query_proxy.rb index 96d08e718..bcc6b3c36 100644 --- a/lib/neo4j/active_node/query/query_proxy.rb +++ b/lib/neo4j/active_node/query/query_proxy.rb @@ -5,13 +5,14 @@ module Query class QueryProxy include Enumerable - def initialize(model) + def initialize(model, association_options = nil) @model = model + @association_options = association_options @chain = [] end def each - query_as(:n).pluck(:n).each do |obj| + query_as(:result).pluck(:result).each do |obj| yield obj end end @@ -28,8 +29,36 @@ def #{method}(*args) alias_method :offset, :skip alias_method :order_by, :order + # MATCH (teacher40:`Teacher`), (start:`Lesson`), teacher40-[:teaches]->(start:`Lesson`), (end:`Lesson`) WHERE ID(teacher40) = 40 AND ID(end) = 42 CREATE start-[:teaches]->end + + def association_chain_var + if start_object = @association_options[:start_object] + :"#{start_object.class.name.downcase}#{start_object.neo_id}" + elsif @association_options[:query_proxy] + @chain_var_num = (@chain_var_num || 0) + 1 + :"node#{@chain_var_num}" + else + raise "Crazy error" # TODO: Better error + end + end + + def association_query_start(var) + if start_object = @association_options[:start_object] + start_object.query_as(var) + elsif query_proxy = @association_options[:query_proxy] + query_proxy.query_as(var) + else + raise "Crazy error" # TODO: Better error + end + end + def query_as(var) - query = @model.query_as(var).return(var) + query = if @association_options + chain_var = association_chain_var + (association_query_start(chain_var) & query_model_as(var)).match("#{chain_var}#{association_arrow}(#{var}:`#{@model.name}`)") + else + query_model_as(var) + end @chain.inject(query) do |query, (method, arg)| if arg.respond_to?(:call) @@ -40,10 +69,39 @@ def query_as(var) end end + def query_model_as(var) + label = @model.respond_to?(:mapped_label_name) ? @model.mapped_label_name : @model + neo4j_session.query.match(var => label) + end + def to_cypher query_as(:n).to_cypher end + def <<(other_node) + if @association_options + raise ArgumentError, "Node must be of the association's class" if other_node.class != @model + + association_query_start(:start) + .match(end: other_node.class) + .where(end: {neo_id: other_node.neo_id}) + .create("start#{association_arrow}end").exec + else + raise "Can only create associations on associations" + end + end + + def method_missing(method_name, *args) + if @model.respond_to?(method_name) + @model.query_proxy = self + result = @model.send(method_name, *args) + @model.query_proxy = nil + result + else + super + end + end + protected def add_links(links) @@ -52,6 +110,19 @@ def add_links(links) private + def association_arrow + if @association_options + case direction = @association_options[:direction].to_sym + when :outbound + "-[:#{@association_options[:relationship]}]->" + when :inbound + "<-[:#{@association_options[:relationship]}]-" + else + raise ArgumentError, "Invalid relationship direction: #{direction}" + end + end + end + def build_deeper_query_proxy(method, args) self.dup.tap do |new_query| args.each do |arg| diff --git a/spec/e2e/query_spec.rb b/spec/e2e/query_spec.rb new file mode 100644 index 000000000..575e21fb4 --- /dev/null +++ b/spec/e2e/query_spec.rb @@ -0,0 +1,104 @@ +require 'spec_helper' +class Student; end +class Teacher; end + +class Lesson + include Neo4j::ActiveNode + property :subject + property :level + + has_many :teachers, from: Teacher, through: :teaches + has_many :students, from: Student, through: :is_enrolled_for + + def self.max_level + self.query_as(:lesson).pluck('max(lesson.level)').first + end + + def self.level(num) + self.where(level: num) + end +end + +class Student + include Neo4j::ActiveNode + property :name + property :age, type: Integer + + has_many :lessons, to: Lesson, through: :is_enrolled_for +end + +class Teacher + include Neo4j::ActiveNode + property :name + + has_many :lessons_taught, to: Lesson, through: :teaches +end + +describe 'Query API' do + before(:each) { delete_db } + describe 'queries directly on a model class' do + let!(:samuels) { Teacher.create(name: 'Harold Samuels') } + let!(:othmar) { Teacher.create(name: 'Ms. Othmar') } + + let!(:ss101) { Lesson.create(subject: 'Social Studies', level: 101) } + let!(:ss102) { Lesson.create(subject: 'Social Studies', level: 102) } + let!(:math101) { Lesson.create(subject: 'Math', level: 101) } + let!(:math201) { Lesson.create(subject: 'Math', level: 201) } + let!(:geo103) { Lesson.create(subject: 'Geography', level: 103) } + + let!(:sandra) { Student.create(name: 'Sandra', age: 16) } + let!(:danny) { Student.create(name: 'Danny', age: 15) } + let!(:bobby) { Student.create(name: 'Bobby', age: 16) } + before(:each) do + samuels.lessons_taught << ss101 + samuels.lessons_taught << ss102 + samuels.lessons_taught << geo103 + + othmar.lessons_taught << math101 + othmar.lessons_taught << math201 + + + sandra.lessons << math201 + sandra.lessons << ss102 + + danny.lessons << math101 + danny.lessons << ss102 + + bobby.lessons << ss102 + end + + it 'returns all' do + result = Teacher.to_a + + result.size.should == 2 + result.should include(samuels) + result.should include(othmar) + end + + it 'can filter' do + Teacher.where(name: /.*Othmar.*/).to_a.should == [othmar] + end + + it 'can filter on associations' do + samuels.lessons_taught.where(level: 101).to_a.should == [ss101] + + end + + it 'can call class methods on associations' do + samuels.lessons_taught.level(101).to_a.should == [ss101] + + samuels.lessons_taught.max_level.should == 103 + samuels.lessons_taught.where(subject: 'Social Studies').max_level.should == 102 + end + + it 'can allow association chaining' do + result = othmar.lessons_taught.students.to_a + result.should include(sandra) + result.should include(danny) + result.size.should == 2 + + othmar.lessons_taught.students.where(age: 16).to_a.should == [sandra] + end + + end +end From 4b8b75eb14daa37680093dc807bced141776555d Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Sun, 13 Jul 2014 12:58:33 -0700 Subject: [PATCH 02/54] Implementing mid-association-chain variable naming --- lib/neo4j/active_node/has_n.rb | 8 ++++---- lib/neo4j/active_node/query/query_proxy.rb | 7 +++++-- spec/e2e/query_spec.rb | 7 +++++++ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/neo4j/active_node/has_n.rb b/lib/neo4j/active_node/has_n.rb index 97f5570cb..e449cef85 100644 --- a/lib/neo4j/active_node/has_n.rb +++ b/lib/neo4j/active_node/has_n.rb @@ -121,13 +121,13 @@ def has_many(name, options = {}) @has_many_relationships << name module_eval(%Q{ - def #{name} - Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class.name}, start_object: self, relationship: #{through.inspect}, direction: #{direction.inspect}) + def #{name}(var = nil) + Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class.name}, var, start_object: self, relationship: #{through.inspect}, direction: #{direction.inspect}) end}, __FILE__, __LINE__) instance_eval(%Q{ - def #{name} - Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class.name}, query_proxy: self.query_proxy, relationship: #{through.inspect}, direction: #{direction.inspect}) + def #{name}(var = nil) + Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class.name}, var, query_proxy: self.query_proxy, relationship: #{through.inspect}, direction: #{direction.inspect}) end}, __FILE__, __LINE__) end diff --git a/lib/neo4j/active_node/query/query_proxy.rb b/lib/neo4j/active_node/query/query_proxy.rb index bcc6b3c36..c6c8ae4e3 100644 --- a/lib/neo4j/active_node/query/query_proxy.rb +++ b/lib/neo4j/active_node/query/query_proxy.rb @@ -5,8 +5,9 @@ module Query class QueryProxy include Enumerable - def initialize(model, association_options = nil) + def initialize(model, var = nil, association_options = nil) @model = model + @var = var @association_options = association_options @chain = [] end @@ -17,7 +18,7 @@ def each end end - METHODS = %w[where order skip limit] + METHODS = %w[where order skip limit pluck] METHODS.each do |method| module_eval(%Q{ @@ -32,6 +33,8 @@ def #{method}(*args) # MATCH (teacher40:`Teacher`), (start:`Lesson`), teacher40-[:teaches]->(start:`Lesson`), (end:`Lesson`) WHERE ID(teacher40) = 40 AND ID(end) = 42 CREATE start-[:teaches]->end def association_chain_var + return @var if @var + if start_object = @association_options[:start_object] :"#{start_object.class.name.downcase}#{start_object.neo_id}" elsif @association_options[:query_proxy] diff --git a/spec/e2e/query_spec.rb b/spec/e2e/query_spec.rb index 575e21fb4..97e9fb90c 100644 --- a/spec/e2e/query_spec.rb +++ b/spec/e2e/query_spec.rb @@ -100,5 +100,12 @@ class Teacher othmar.lessons_taught.students.where(age: 16).to_a.should == [sandra] end + it 'can allow for filtering mid-association-chain' do + othmar.lessons_taught.where(level: 201).students.to_a.should == [sandra] + end + + it 'can allow for returning nodes mis-association-chain' do + othmar.lessons_taught(:lesson).students.where(age: 16).pluck(:lesson) + end end end From 5202dae32142194a02f71b8771d9f094e7a39365 Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Sun, 13 Jul 2014 13:10:47 -0700 Subject: [PATCH 03/54] Revert "Implementing mid-association-chain variable naming" This reverts commit 4b8b75eb14daa37680093dc807bced141776555d. --- lib/neo4j/active_node/has_n.rb | 8 ++++---- lib/neo4j/active_node/query/query_proxy.rb | 7 ++----- spec/e2e/query_spec.rb | 7 ------- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/lib/neo4j/active_node/has_n.rb b/lib/neo4j/active_node/has_n.rb index e449cef85..97f5570cb 100644 --- a/lib/neo4j/active_node/has_n.rb +++ b/lib/neo4j/active_node/has_n.rb @@ -121,13 +121,13 @@ def has_many(name, options = {}) @has_many_relationships << name module_eval(%Q{ - def #{name}(var = nil) - Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class.name}, var, start_object: self, relationship: #{through.inspect}, direction: #{direction.inspect}) + def #{name} + Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class.name}, start_object: self, relationship: #{through.inspect}, direction: #{direction.inspect}) end}, __FILE__, __LINE__) instance_eval(%Q{ - def #{name}(var = nil) - Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class.name}, var, query_proxy: self.query_proxy, relationship: #{through.inspect}, direction: #{direction.inspect}) + def #{name} + Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class.name}, query_proxy: self.query_proxy, relationship: #{through.inspect}, direction: #{direction.inspect}) end}, __FILE__, __LINE__) end diff --git a/lib/neo4j/active_node/query/query_proxy.rb b/lib/neo4j/active_node/query/query_proxy.rb index c6c8ae4e3..bcc6b3c36 100644 --- a/lib/neo4j/active_node/query/query_proxy.rb +++ b/lib/neo4j/active_node/query/query_proxy.rb @@ -5,9 +5,8 @@ module Query class QueryProxy include Enumerable - def initialize(model, var = nil, association_options = nil) + def initialize(model, association_options = nil) @model = model - @var = var @association_options = association_options @chain = [] end @@ -18,7 +17,7 @@ def each end end - METHODS = %w[where order skip limit pluck] + METHODS = %w[where order skip limit] METHODS.each do |method| module_eval(%Q{ @@ -33,8 +32,6 @@ def #{method}(*args) # MATCH (teacher40:`Teacher`), (start:`Lesson`), teacher40-[:teaches]->(start:`Lesson`), (end:`Lesson`) WHERE ID(teacher40) = 40 AND ID(end) = 42 CREATE start-[:teaches]->end def association_chain_var - return @var if @var - if start_object = @association_options[:start_object] :"#{start_object.class.name.downcase}#{start_object.neo_id}" elsif @association_options[:query_proxy] diff --git a/spec/e2e/query_spec.rb b/spec/e2e/query_spec.rb index 97e9fb90c..575e21fb4 100644 --- a/spec/e2e/query_spec.rb +++ b/spec/e2e/query_spec.rb @@ -100,12 +100,5 @@ class Teacher othmar.lessons_taught.students.where(age: 16).to_a.should == [sandra] end - it 'can allow for filtering mid-association-chain' do - othmar.lessons_taught.where(level: 201).students.to_a.should == [sandra] - end - - it 'can allow for returning nodes mis-association-chain' do - othmar.lessons_taught(:lesson).students.where(age: 16).pluck(:lesson) - end end end From 1e1b1bf466f618123697d85f297b05d7b5505774 Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Sun, 13 Jul 2014 13:11:32 -0700 Subject: [PATCH 04/54] Putting (working) specs back --- spec/e2e/query_spec.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spec/e2e/query_spec.rb b/spec/e2e/query_spec.rb index 575e21fb4..30927f0af 100644 --- a/spec/e2e/query_spec.rb +++ b/spec/e2e/query_spec.rb @@ -100,5 +100,13 @@ class Teacher othmar.lessons_taught.students.where(age: 16).to_a.should == [sandra] end + it 'can allow for filtering mid-association-chain' do + othmar.lessons_taught.where(level: 201).students.to_a.should == [sandra] + end + + it 'can allow for returning nodes mis-association-chain' do + othmar.lessons_taught(:lesson).students.where(age: 16).pluck(:lesson).should == [math201] + end end end + From 4e636dfd137321f45bf0705f5018cebbf89db34a Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Sun, 13 Jul 2014 13:12:42 -0700 Subject: [PATCH 05/54] Implement QueryProxy#pluck --- lib/neo4j/active_node/query/query_proxy.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/neo4j/active_node/query/query_proxy.rb b/lib/neo4j/active_node/query/query_proxy.rb index bcc6b3c36..328c5ab35 100644 --- a/lib/neo4j/active_node/query/query_proxy.rb +++ b/lib/neo4j/active_node/query/query_proxy.rb @@ -29,7 +29,9 @@ def #{method}(*args) alias_method :offset, :skip alias_method :order_by, :order - # MATCH (teacher40:`Teacher`), (start:`Lesson`), teacher40-[:teaches]->(start:`Lesson`), (end:`Lesson`) WHERE ID(teacher40) = 40 AND ID(end) = 42 CREATE start-[:teaches]->end + def pluck(var) + self.query_as(:n).pluck(var) + end def association_chain_var if start_object = @association_options[:start_object] From d47a320a2eb8824f29ba595479cee14204e5cf74 Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Sun, 13 Jul 2014 13:25:09 -0700 Subject: [PATCH 06/54] Should be able to pluck to 2d array --- spec/e2e/query_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/e2e/query_spec.rb b/spec/e2e/query_spec.rb index 30927f0af..7fd86c875 100644 --- a/spec/e2e/query_spec.rb +++ b/spec/e2e/query_spec.rb @@ -106,6 +106,8 @@ class Teacher it 'can allow for returning nodes mis-association-chain' do othmar.lessons_taught(:lesson).students.where(age: 16).pluck(:lesson).should == [math201] + + othmar.lessons_taught(:lesson).students(:student).where(age: 16).pluck(:lesson, :student).should == [[math201], [sandra]] end end end From 7a28f9a99009bb9b4286f8977ff9c5705a0c26c1 Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Sun, 13 Jul 2014 13:38:51 -0700 Subject: [PATCH 07/54] Specs for supporting through_any --- spec/e2e/query_spec.rb | 53 ++++++++++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/spec/e2e/query_spec.rb b/spec/e2e/query_spec.rb index 7fd86c875..d3986e49b 100644 --- a/spec/e2e/query_spec.rb +++ b/spec/e2e/query_spec.rb @@ -7,7 +7,7 @@ class Lesson property :subject property :level - has_many :teachers, from: Teacher, through: :teaches + has_many :teachers, from: Teacher, through: :teaching has_many :students, from: Student, through: :is_enrolled_for def self.max_level @@ -31,7 +31,10 @@ class Teacher include Neo4j::ActiveNode property :name - has_many :lessons_taught, to: Lesson, through: :teaches + has_many :lessons_teaching, to: Lesson, through: :teaching + has_many :lessons_taught, to: Lesson, through: :taught + + has_many :lessions, to: Lesson, through_any: true end describe 'Query API' do @@ -50,12 +53,13 @@ class Teacher let!(:danny) { Student.create(name: 'Danny', age: 15) } let!(:bobby) { Student.create(name: 'Bobby', age: 16) } before(:each) do - samuels.lessons_taught << ss101 - samuels.lessons_taught << ss102 - samuels.lessons_taught << geo103 + samuels.lessons_teaching << ss101 + samuels.lessons_teaching << ss102 + samuels.lessons_teaching << geo103 + samuels.lessons_taught << math101 - othmar.lessons_taught << math101 - othmar.lessons_taught << math201 + othmar.lessons_teaching << math101 + othmar.lessons_teaching << math201 sandra.lessons << math201 @@ -79,35 +83,50 @@ class Teacher Teacher.where(name: /.*Othmar.*/).to_a.should == [othmar] end + it 'returns only objects specified by association' do + result = samuels.lessons_teaching.to_a + result.size.should == 3 + result.should include(ss101) + result.should include(ss102) + result.should include(geo103) + + result = samuels.lessons.to_a + result.size.should == 4 + result.should include(ss101) + result.should include(ss102) + result.should include(geo103) + result.should include(math101) + end + it 'can filter on associations' do - samuels.lessons_taught.where(level: 101).to_a.should == [ss101] + samuels.lessons_teaching.where(level: 101).to_a.should == [ss101] end it 'can call class methods on associations' do - samuels.lessons_taught.level(101).to_a.should == [ss101] + samuels.lessons_teaching.level(101).to_a.should == [ss101] - samuels.lessons_taught.max_level.should == 103 - samuels.lessons_taught.where(subject: 'Social Studies').max_level.should == 102 + samuels.lessons_teaching.max_level.should == 103 + samuels.lessons_teaching.where(subject: 'Social Studies').max_level.should == 102 end it 'can allow association chaining' do - result = othmar.lessons_taught.students.to_a + result = othmar.lessons_teaching.students.to_a + result.size.should == 2 result.should include(sandra) result.should include(danny) - result.size.should == 2 - othmar.lessons_taught.students.where(age: 16).to_a.should == [sandra] + othmar.lessons_teaching.students.where(age: 16).to_a.should == [sandra] end it 'can allow for filtering mid-association-chain' do - othmar.lessons_taught.where(level: 201).students.to_a.should == [sandra] + othmar.lessons_teaching.where(level: 201).students.to_a.should == [sandra] end it 'can allow for returning nodes mis-association-chain' do - othmar.lessons_taught(:lesson).students.where(age: 16).pluck(:lesson).should == [math201] + othmar.lessons_teaching(:lesson).students.where(age: 16).pluck(:lesson).should == [math201] - othmar.lessons_taught(:lesson).students(:student).where(age: 16).pluck(:lesson, :student).should == [[math201], [sandra]] + othmar.lessons_teaching(:lesson).students(:student).where(age: 16).pluck(:lesson, :student).should == [[math201], [sandra]] end end end From 7b3617531434ced86698a62724eeedebdbc4e625 Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Sun, 13 Jul 2014 13:54:59 -0700 Subject: [PATCH 08/54] Add some specs, clean some specs --- lib/neo4j/active_node/identity.rb | 3 +++ spec/e2e/query_spec.rb | 23 ++++++----------------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/lib/neo4j/active_node/identity.rb b/lib/neo4j/active_node/identity.rb index 131870790..bbf190a36 100644 --- a/lib/neo4j/active_node/identity.rb +++ b/lib/neo4j/active_node/identity.rb @@ -23,6 +23,9 @@ def id id.is_a?(Integer) ? id : nil end + def hash + id.hash + end end diff --git a/spec/e2e/query_spec.rb b/spec/e2e/query_spec.rb index d3986e49b..7204f7895 100644 --- a/spec/e2e/query_spec.rb +++ b/spec/e2e/query_spec.rb @@ -1,4 +1,5 @@ require 'spec_helper' +require 'set' class Student; end class Teacher; end @@ -34,7 +35,7 @@ class Teacher has_many :lessons_teaching, to: Lesson, through: :teaching has_many :lessons_taught, to: Lesson, through: :taught - has_many :lessions, to: Lesson, through_any: true + has_many :lessons, to: Lesson, through_any: true end describe 'Query API' do @@ -84,18 +85,9 @@ class Teacher end it 'returns only objects specified by association' do - result = samuels.lessons_teaching.to_a - result.size.should == 3 - result.should include(ss101) - result.should include(ss102) - result.should include(geo103) - - result = samuels.lessons.to_a - result.size.should == 4 - result.should include(ss101) - result.should include(ss102) - result.should include(geo103) - result.should include(math101) + samuels.lessons_teaching.to_set.should == [ss101, ss102, geo103].to_set + + samuels.lessons.to_set.should == [ss101, ss102, geo103, math101].to_set end it 'can filter on associations' do @@ -111,10 +103,7 @@ class Teacher end it 'can allow association chaining' do - result = othmar.lessons_teaching.students.to_a - result.size.should == 2 - result.should include(sandra) - result.should include(danny) + result = othmar.lessons_teaching.students.to_set.should == [sandra, danny].to_set othmar.lessons_teaching.students.where(age: 16).to_a.should == [sandra] end From a23c6388759f64ef419778057289ce07f00b6715 Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Sun, 13 Jul 2014 13:59:02 -0700 Subject: [PATCH 09/54] Implementing through_any key for has_many --- lib/neo4j/active_node/has_n.rb | 7 ++++--- lib/neo4j/active_node/query/query_proxy.rb | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/neo4j/active_node/has_n.rb b/lib/neo4j/active_node/has_n.rb index 97f5570cb..b2a30404a 100644 --- a/lib/neo4j/active_node/has_n.rb +++ b/lib/neo4j/active_node/has_n.rb @@ -108,12 +108,13 @@ def #{rel_type} end def has_many(name, options = {}) - to, from, through = options.values_at(:to, :from, :through) + to, from, through, through_any = options.values_at(:to, :from, :through, :through_any) raise ArgumentError, "Must specify either :to or :from" if not (to || from) raise ArgumentError, "Cannot specify both :to and :from" if to && from target_class = to || from direction = to ? :outbound : :inbound + through_any ||= false # TODO: auto-set through when missing @@ -122,12 +123,12 @@ def has_many(name, options = {}) module_eval(%Q{ def #{name} - Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class.name}, start_object: self, relationship: #{through.inspect}, direction: #{direction.inspect}) + Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class.name}, start_object: self, through_any: #{through_any}, relationship: #{through.inspect}, direction: #{direction.inspect}) end}, __FILE__, __LINE__) instance_eval(%Q{ def #{name} - Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class.name}, query_proxy: self.query_proxy, relationship: #{through.inspect}, direction: #{direction.inspect}) + Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class.name}, query_proxy: self.query_proxy, through_any: #{through_any}, relationship: #{through.inspect}, direction: #{direction.inspect}) end}, __FILE__, __LINE__) end diff --git a/lib/neo4j/active_node/query/query_proxy.rb b/lib/neo4j/active_node/query/query_proxy.rb index 328c5ab35..fe8752442 100644 --- a/lib/neo4j/active_node/query/query_proxy.rb +++ b/lib/neo4j/active_node/query/query_proxy.rb @@ -114,11 +114,12 @@ def add_links(links) def association_arrow if @association_options + relationship = @association_options[:through_any] ? '' : "[:#{@association_options[:relationship]}]" case direction = @association_options[:direction].to_sym when :outbound - "-[:#{@association_options[:relationship]}]->" + "-#{relationship}->" when :inbound - "<-[:#{@association_options[:relationship]}]-" + "<-#{relationship}-" else raise ArgumentError, "Invalid relationship direction: #{direction}" end From e5365fa54d86f5f151d53ce7ab9584ccc08ac8bc Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Sun, 13 Jul 2014 18:55:22 -0700 Subject: [PATCH 10/54] Bit of cleaner syntax to use "through: false" rather than "through_any: true". Also implement default of Class#association for relationship names --- lib/neo4j/active_node/has_n.rb | 9 ++++----- lib/neo4j/active_node/query/query_proxy.rb | 2 +- spec/e2e/query_spec.rb | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/neo4j/active_node/has_n.rb b/lib/neo4j/active_node/has_n.rb index b2a30404a..ddad7096c 100644 --- a/lib/neo4j/active_node/has_n.rb +++ b/lib/neo4j/active_node/has_n.rb @@ -108,27 +108,26 @@ def #{rel_type} end def has_many(name, options = {}) - to, from, through, through_any = options.values_at(:to, :from, :through, :through_any) + to, from, through = options.values_at(:to, :from, :through) raise ArgumentError, "Must specify either :to or :from" if not (to || from) raise ArgumentError, "Cannot specify both :to and :from" if to && from target_class = to || from direction = to ? :outbound : :inbound - through_any ||= false - # TODO: auto-set through when missing + through = "#{target_class.name}##{name}" if through.nil? @has_many_relationships ||= [] @has_many_relationships << name module_eval(%Q{ def #{name} - Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class.name}, start_object: self, through_any: #{through_any}, relationship: #{through.inspect}, direction: #{direction.inspect}) + Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class.name}, start_object: self, relationship: #{through.inspect}, direction: #{direction.inspect}) end}, __FILE__, __LINE__) instance_eval(%Q{ def #{name} - Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class.name}, query_proxy: self.query_proxy, through_any: #{through_any}, relationship: #{through.inspect}, direction: #{direction.inspect}) + Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class.name}, query_proxy: self.query_proxy, relationship: #{through.inspect}, direction: #{direction.inspect}) end}, __FILE__, __LINE__) end diff --git a/lib/neo4j/active_node/query/query_proxy.rb b/lib/neo4j/active_node/query/query_proxy.rb index fe8752442..afebe2370 100644 --- a/lib/neo4j/active_node/query/query_proxy.rb +++ b/lib/neo4j/active_node/query/query_proxy.rb @@ -114,7 +114,7 @@ def add_links(links) def association_arrow if @association_options - relationship = @association_options[:through_any] ? '' : "[:#{@association_options[:relationship]}]" + relationship = (@association_options[:relationship] == false) ? '' : "[:#{@association_options[:relationship]}]" case direction = @association_options[:direction].to_sym when :outbound "-#{relationship}->" diff --git a/spec/e2e/query_spec.rb b/spec/e2e/query_spec.rb index 7204f7895..570fdfeda 100644 --- a/spec/e2e/query_spec.rb +++ b/spec/e2e/query_spec.rb @@ -35,7 +35,7 @@ class Teacher has_many :lessons_teaching, to: Lesson, through: :teaching has_many :lessons_taught, to: Lesson, through: :taught - has_many :lessons, to: Lesson, through_any: true + has_many :lessons, to: Lesson, through: false end describe 'Query API' do From de4573419cc2f01241dc6707028e7896359eab34 Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Sun, 13 Jul 2014 20:15:31 -0700 Subject: [PATCH 11/54] Create Association class to store association options --- lib/neo4j.rb | 1 + lib/neo4j/active_node/has_n.rb | 13 +++++--- lib/neo4j/active_node/has_n/association.rb | 33 ++++++++++++++++++++ lib/neo4j/active_node/query/query_proxy.rb | 35 ++++++++-------------- 4 files changed, 56 insertions(+), 26 deletions(-) create mode 100644 lib/neo4j/active_node/has_n/association.rb diff --git a/lib/neo4j.rb b/lib/neo4j.rb index aa233ac1f..f41369193 100644 --- a/lib/neo4j.rb +++ b/lib/neo4j.rb @@ -27,6 +27,7 @@ require 'neo4j/active_node/rels' require 'neo4j/active_node/has_n' require 'neo4j/active_node/has_n/decl_rel' +require 'neo4j/active_node/has_n/association' require 'neo4j/active_node/has_n/nodes' require 'neo4j/active_node/query/query_proxy' require 'neo4j/active_node/query' diff --git a/lib/neo4j/active_node/has_n.rb b/lib/neo4j/active_node/has_n.rb index ddad7096c..020af0655 100644 --- a/lib/neo4j/active_node/has_n.rb +++ b/lib/neo4j/active_node/has_n.rb @@ -108,30 +108,35 @@ def #{rel_type} end def has_many(name, options = {}) - to, from, through = options.values_at(:to, :from, :through) + name = name.to_sym + to, from, relationship = options.values_at(:to, :from, :through) raise ArgumentError, "Must specify either :to or :from" if not (to || from) raise ArgumentError, "Cannot specify both :to and :from" if to && from target_class = to || from direction = to ? :outbound : :inbound - through = "#{target_class.name}##{name}" if through.nil? + relationship = "#{target_class.name}##{name}" if relationship.nil? @has_many_relationships ||= [] @has_many_relationships << name + @associations ||= {} + @associations[name] = Neo4j::ActiveNode::HasN::Association.new(:has_many, name, relationship, direction) + module_eval(%Q{ def #{name} - Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class.name}, start_object: self, relationship: #{through.inspect}, direction: #{direction.inspect}) + Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class.name}, self.class.associations[#{name.inspect}], start_object: self) end}, __FILE__, __LINE__) instance_eval(%Q{ def #{name} - Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class.name}, query_proxy: self.query_proxy, relationship: #{through.inspect}, direction: #{direction.inspect}) + Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class.name}, @associations[#{name.inspect}], query_proxy: self.query_proxy) end}, __FILE__, __LINE__) end + attr_reader :associations # Specifies a relationship between two node classes. # Generates assignment and accessor methods for the given relationship diff --git a/lib/neo4j/active_node/has_n/association.rb b/lib/neo4j/active_node/has_n/association.rb new file mode 100644 index 000000000..488e18a27 --- /dev/null +++ b/lib/neo4j/active_node/has_n/association.rb @@ -0,0 +1,33 @@ +module Neo4j + module ActiveNode + module HasN + class Association + attr_reader :type, :name, :relationship, :direction + + def initialize(type, name, relationship, direction, options = {}) + raise ArgumentError if not [:has_many, :has_one].include?(type) + raise ArgumentError if not [:inbound, :outbound].include?(direction) + + @type = type + @name = name + @relationship = relationship + @direction = direction + end + + def arrow_cypher + relationship_cypher = (@relationship == false) ? '' : "[:#{@relationship}]" + case @direction.to_sym + when :outbound + "-#{relationship_cypher}->" + when :inbound + "<-#{relationship_cypher}-" + else + raise ArgumentError, "Invalid relationship direction: #{@direction.inspect}" + end + end + + end + end + end +end + diff --git a/lib/neo4j/active_node/query/query_proxy.rb b/lib/neo4j/active_node/query/query_proxy.rb index afebe2370..2e099e1ea 100644 --- a/lib/neo4j/active_node/query/query_proxy.rb +++ b/lib/neo4j/active_node/query/query_proxy.rb @@ -5,9 +5,10 @@ module Query class QueryProxy include Enumerable - def initialize(model, association_options = nil) + def initialize(model, association = nil, options = nil) @model = model - @association_options = association_options + @association = association + @options = options @chain = [] end @@ -34,9 +35,9 @@ def pluck(var) end def association_chain_var - if start_object = @association_options[:start_object] + if start_object = @options[:start_object] :"#{start_object.class.name.downcase}#{start_object.neo_id}" - elsif @association_options[:query_proxy] + elsif @options[:query_proxy] @chain_var_num = (@chain_var_num || 0) + 1 :"node#{@chain_var_num}" else @@ -45,9 +46,9 @@ def association_chain_var end def association_query_start(var) - if start_object = @association_options[:start_object] + if start_object = @options[:start_object] start_object.query_as(var) - elsif query_proxy = @association_options[:query_proxy] + elsif query_proxy = @options[:query_proxy] query_proxy.query_as(var) else raise "Crazy error" # TODO: Better error @@ -55,7 +56,7 @@ def association_query_start(var) end def query_as(var) - query = if @association_options + query = if @association chain_var = association_chain_var (association_query_start(chain_var) & query_model_as(var)).match("#{chain_var}#{association_arrow}(#{var}:`#{@model.name}`)") else @@ -81,7 +82,7 @@ def to_cypher end def <<(other_node) - if @association_options + if @association raise ArgumentError, "Node must be of the association's class" if other_node.class != @model association_query_start(:start) @@ -93,6 +94,10 @@ def <<(other_node) end end + def association_arrow + @association && @association.arrow_cypher + end + def method_missing(method_name, *args) if @model.respond_to?(method_name) @model.query_proxy = self @@ -112,20 +117,6 @@ def add_links(links) private - def association_arrow - if @association_options - relationship = (@association_options[:relationship] == false) ? '' : "[:#{@association_options[:relationship]}]" - case direction = @association_options[:direction].to_sym - when :outbound - "-#{relationship}->" - when :inbound - "<-#{relationship}-" - else - raise ArgumentError, "Invalid relationship direction: #{direction}" - end - end - end - def build_deeper_query_proxy(method, args) self.dup.tap do |new_query| args.each do |arg| From c5b8ed271c289f615bde6e5e9c041ff69106de1a Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Mon, 14 Jul 2014 06:20:53 -0700 Subject: [PATCH 12/54] Escape relationship names --- lib/neo4j/active_node/has_n/association.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/neo4j/active_node/has_n/association.rb b/lib/neo4j/active_node/has_n/association.rb index 488e18a27..7b94cf8be 100644 --- a/lib/neo4j/active_node/has_n/association.rb +++ b/lib/neo4j/active_node/has_n/association.rb @@ -15,7 +15,7 @@ def initialize(type, name, relationship, direction, options = {}) end def arrow_cypher - relationship_cypher = (@relationship == false) ? '' : "[:#{@relationship}]" + relationship_cypher = (@relationship == false) ? '' : "[:`#{@relationship}`]" case @direction.to_sym when :outbound "-#{relationship_cypher}->" From 2f5e4791eb0c0f95295cb51325c3f41dd8b60be6 Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Mon, 14 Jul 2014 06:22:04 -0700 Subject: [PATCH 13/54] Cleaner way to check chain levels --- lib/neo4j/active_node/query/query_proxy.rb | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/neo4j/active_node/query/query_proxy.rb b/lib/neo4j/active_node/query/query_proxy.rb index 2e099e1ea..75f70042f 100644 --- a/lib/neo4j/active_node/query/query_proxy.rb +++ b/lib/neo4j/active_node/query/query_proxy.rb @@ -38,8 +38,7 @@ def association_chain_var if start_object = @options[:start_object] :"#{start_object.class.name.downcase}#{start_object.neo_id}" elsif @options[:query_proxy] - @chain_var_num = (@chain_var_num || 0) + 1 - :"node#{@chain_var_num}" + :"node#{_chain_level}" else raise "Crazy error" # TODO: Better error end @@ -110,17 +109,28 @@ def method_missing(method_name, *args) end protected + # Methods are underscored to prevent conflict with user class methods - def add_links(links) + def _add_links(links) @chain += links end + def _chain_level + if @options[:start_object] + 1 + elsif query_proxy = @options[:query_proxy] + query_proxy._chain_level + 1 + else + raise "Crazy error" # TODO: Better error + end + end + private def build_deeper_query_proxy(method, args) self.dup.tap do |new_query| args.each do |arg| - new_query.add_links(links_for_arg(method, arg)) + new_query._add_links(links_for_arg(method, arg)) end end end From a540fc88801b42da50a104bf341527ddbfacf90b Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Mon, 14 Jul 2014 06:22:23 -0700 Subject: [PATCH 14/54] Test for three level association chaining --- spec/e2e/query_spec.rb | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/spec/e2e/query_spec.rb b/spec/e2e/query_spec.rb index 570fdfeda..a5f9059f4 100644 --- a/spec/e2e/query_spec.rb +++ b/spec/e2e/query_spec.rb @@ -3,6 +3,12 @@ class Student; end class Teacher; end +class Interest + include Neo4j::ActiveNode + + property :name +end + class Lesson include Neo4j::ActiveNode property :subject @@ -26,6 +32,8 @@ class Student property :age, type: Integer has_many :lessons, to: Lesson, through: :is_enrolled_for + + has_many :interests, to: Interest end class Teacher @@ -36,6 +44,8 @@ class Teacher has_many :lessons_taught, to: Lesson, through: :taught has_many :lessons, to: Lesson, through: false + + has_many :interests, to: Interest end describe 'Query API' do @@ -53,6 +63,11 @@ class Teacher let!(:sandra) { Student.create(name: 'Sandra', age: 16) } let!(:danny) { Student.create(name: 'Danny', age: 15) } let!(:bobby) { Student.create(name: 'Bobby', age: 16) } + + let!(:reading) { Interest.create(name: 'Reading') } + let!(:math) { Interest.create(name: 'Math') } + let!(:monster_trucks) { Interest.create(name: 'Monster Trucks') } + before(:each) do samuels.lessons_teaching << ss101 samuels.lessons_teaching << ss102 @@ -70,6 +85,10 @@ class Teacher danny.lessons << ss102 bobby.lessons << ss102 + + danny.interests << reading + bobby.interests << math + othmar.interests << monster_trucks end it 'returns all' do @@ -103,7 +122,9 @@ class Teacher end it 'can allow association chaining' do - result = othmar.lessons_teaching.students.to_set.should == [sandra, danny].to_set + othmar.lessons_teaching.students.to_set.should == [sandra, danny].to_set + + othmar.lessons_teaching.students.interests.to_set.should == [reading].to_set othmar.lessons_teaching.students.where(age: 16).to_a.should == [sandra] end From 10270aabfb0079be5a7726b917c0ac018df5ab89 Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Mon, 14 Jul 2014 06:31:11 -0700 Subject: [PATCH 15/54] Yatta! Can now query mid-association-chain --- lib/neo4j/active_node/has_n.rb | 8 ++++---- lib/neo4j/active_node/query/query_proxy.rb | 23 +++++++++++++++------- spec/e2e/query_spec.rb | 2 +- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/lib/neo4j/active_node/has_n.rb b/lib/neo4j/active_node/has_n.rb index 020af0655..c117295f5 100644 --- a/lib/neo4j/active_node/has_n.rb +++ b/lib/neo4j/active_node/has_n.rb @@ -125,13 +125,13 @@ def has_many(name, options = {}) @associations[name] = Neo4j::ActiveNode::HasN::Association.new(:has_many, name, relationship, direction) module_eval(%Q{ - def #{name} - Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class.name}, self.class.associations[#{name.inspect}], start_object: self) + def #{name}(var = nil) + Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class.name}, self.class.associations[#{name.inspect}], var: var, start_object: self) end}, __FILE__, __LINE__) instance_eval(%Q{ - def #{name} - Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class.name}, @associations[#{name.inspect}], query_proxy: self.query_proxy) + def #{name}(var = nil) + Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class.name}, @associations[#{name.inspect}], var: var, query_proxy: self.query_proxy) end}, __FILE__, __LINE__) end diff --git a/lib/neo4j/active_node/query/query_proxy.rb b/lib/neo4j/active_node/query/query_proxy.rb index 75f70042f..7636856ad 100644 --- a/lib/neo4j/active_node/query/query_proxy.rb +++ b/lib/neo4j/active_node/query/query_proxy.rb @@ -5,7 +5,7 @@ module Query class QueryProxy include Enumerable - def initialize(model, association = nil, options = nil) + def initialize(model, association = nil, options = {}) @model = model @association = association @options = options @@ -13,7 +13,7 @@ def initialize(model, association = nil, options = nil) end def each - query_as(:result).pluck(:result).each do |obj| + query.pluck(:result).each do |obj| yield obj end end @@ -30,15 +30,15 @@ def #{method}(*args) alias_method :offset, :skip alias_method :order_by, :order - def pluck(var) - self.query_as(:n).pluck(var) + def pluck(*args) + self.query.pluck(*args) end def association_chain_var if start_object = @options[:start_object] :"#{start_object.class.name.downcase}#{start_object.neo_id}" - elsif @options[:query_proxy] - :"node#{_chain_level}" + elsif query_proxy = @options[:query_proxy] + query_proxy.var || :"node#{_chain_level}" else raise "Crazy error" # TODO: Better error end @@ -54,9 +54,18 @@ def association_query_start(var) end end + def query + query_as(self.var || :result) + end + + def var + @options[:var] + end + def query_as(var) query = if @association chain_var = association_chain_var + var = self.var if self.var (association_query_start(chain_var) & query_model_as(var)).match("#{chain_var}#{association_arrow}(#{var}:`#{@model.name}`)") else query_model_as(var) @@ -77,7 +86,7 @@ def query_model_as(var) end def to_cypher - query_as(:n).to_cypher + query.to_cypher end def <<(other_node) diff --git a/spec/e2e/query_spec.rb b/spec/e2e/query_spec.rb index a5f9059f4..6375a8650 100644 --- a/spec/e2e/query_spec.rb +++ b/spec/e2e/query_spec.rb @@ -136,7 +136,7 @@ class Teacher it 'can allow for returning nodes mis-association-chain' do othmar.lessons_teaching(:lesson).students.where(age: 16).pluck(:lesson).should == [math201] - othmar.lessons_teaching(:lesson).students(:student).where(age: 16).pluck(:lesson, :student).should == [[math201], [sandra]] + othmar.lessons_teaching(:lesson).students(:student).where(age: 16).pluck(:lesson, :student).should == [[math201, sandra]] end end end From 7ca7cccc3ed46fad6c4589992466754deafb96c9 Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Mon, 14 Jul 2014 06:38:40 -0700 Subject: [PATCH 16/54] Protecting/underscoring some internal methods. Adding some basic comments --- lib/neo4j/active_node/query/query_proxy.rb | 84 ++++++++++++---------- 1 file changed, 45 insertions(+), 39 deletions(-) diff --git a/lib/neo4j/active_node/query/query_proxy.rb b/lib/neo4j/active_node/query/query_proxy.rb index 7636856ad..c5205f5f7 100644 --- a/lib/neo4j/active_node/query/query_proxy.rb +++ b/lib/neo4j/active_node/query/query_proxy.rb @@ -30,45 +30,25 @@ def #{method}(*args) alias_method :offset, :skip alias_method :order_by, :order + # For getting variables which have been defined as part of the association chain def pluck(*args) self.query.pluck(*args) end - def association_chain_var - if start_object = @options[:start_object] - :"#{start_object.class.name.downcase}#{start_object.neo_id}" - elsif query_proxy = @options[:query_proxy] - query_proxy.var || :"node#{_chain_level}" - else - raise "Crazy error" # TODO: Better error - end - end - - def association_query_start(var) - if start_object = @options[:start_object] - start_object.query_as(var) - elsif query_proxy = @options[:query_proxy] - query_proxy.query_as(var) - else - raise "Crazy error" # TODO: Better error - end - end - + # Like calling #query_as, but for when you don't care about the variable name def query - query_as(self.var || :result) + query_as(self._var || :result) end - def var - @options[:var] - end + # Build a Neo4j::Core::Query object for the QueryProxy def query_as(var) query = if @association - chain_var = association_chain_var - var = self.var if self.var - (association_query_start(chain_var) & query_model_as(var)).match("#{chain_var}#{association_arrow}(#{var}:`#{@model.name}`)") + chain_var = _association_chain_var + var = self._var if self._var + (_association_query_start(chain_var) & _query_model_as(var)).match("#{chain_var}#{_association_arrow}(#{var}:`#{@model.name}`)") else - query_model_as(var) + _query_model_as(var) end @chain.inject(query) do |query, (method, arg)| @@ -80,32 +60,25 @@ def query_as(var) end end - def query_model_as(var) - label = @model.respond_to?(:mapped_label_name) ? @model.mapped_label_name : @model - neo4j_session.query.match(var => label) - end - + # Cypher string for the QueryProxy's query def to_cypher query.to_cypher end + # To add a relationship for the node for the association on this QueryProxy def <<(other_node) if @association raise ArgumentError, "Node must be of the association's class" if other_node.class != @model - association_query_start(:start) + _association_query_start(:start) .match(end: other_node.class) .where(end: {neo_id: other_node.neo_id}) - .create("start#{association_arrow}end").exec + .create("start#{_association_arrow}end").exec else raise "Can only create associations on associations" end end - def association_arrow - @association && @association.arrow_cypher - end - def method_missing(method_name, *args) if @model.respond_to?(method_name) @model.query_proxy = self @@ -124,6 +97,15 @@ def _add_links(links) @chain += links end + def _query_model_as(var) + label = @model.respond_to?(:mapped_label_name) ? @model.mapped_label_name : @model + neo4j_session.query.match(var => label) + end + + def _association_arrow + @association && @association.arrow_cypher + end + def _chain_level if @options[:start_object] 1 @@ -134,6 +116,30 @@ def _chain_level end end + def _var + @options[:var] + end + + def _association_chain_var + if start_object = @options[:start_object] + :"#{start_object.class.name.downcase}#{start_object.neo_id}" + elsif query_proxy = @options[:query_proxy] + query_proxy._var || :"node#{_chain_level}" + else + raise "Crazy error" # TODO: Better error + end + end + + def _association_query_start(var) + if start_object = @options[:start_object] + start_object.query_as(var) + elsif query_proxy = @options[:query_proxy] + query_proxy.query_as(var) + else + raise "Crazy error" # TODO: Better error + end + end + private def build_deeper_query_proxy(method, args) From 82d2caa6c1fc580e7361a5bcf107e410157626d4 Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Mon, 14 Jul 2014 09:52:55 -0700 Subject: [PATCH 17/54] Refactors toward being able to specify properties on creating relationships --- lib/neo4j/active_node/has_n.rb | 25 ++++++----- lib/neo4j/active_node/has_n/association.rb | 49 +++++++++++++++++++--- lib/neo4j/active_node/query/query_proxy.rb | 18 +++++--- spec/e2e/query_spec.rb | 30 +++++++++---- 4 files changed, 89 insertions(+), 33 deletions(-) diff --git a/lib/neo4j/active_node/has_n.rb b/lib/neo4j/active_node/has_n.rb index c117295f5..6b43ca322 100644 --- a/lib/neo4j/active_node/has_n.rb +++ b/lib/neo4j/active_node/has_n.rb @@ -6,6 +6,10 @@ def _decl_rels_for(rel_type) self.class._decl_rels[rel_type] end + def associate(node, association_name, properties) + + end + module ClassMethods @@ -109,29 +113,24 @@ def #{rel_type} def has_many(name, options = {}) name = name.to_sym - to, from, relationship = options.values_at(:to, :from, :through) - raise ArgumentError, "Must specify either :to or :from" if not (to || from) - raise ArgumentError, "Cannot specify both :to and :from" if to && from - - target_class = to || from - direction = to ? :outbound : :inbound - - relationship = "#{target_class.name}##{name}" if relationship.nil? @has_many_relationships ||= [] @has_many_relationships << name + association = Neo4j::ActiveNode::HasN::Association.new(:has_many, name, options) @associations ||= {} - @associations[name] = Neo4j::ActiveNode::HasN::Association.new(:has_many, name, relationship, direction) + @associations[name] = association + + target_class_name = association.target_class ? association.target_class.name : 'nil' module_eval(%Q{ - def #{name}(var = nil) - Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class.name}, self.class.associations[#{name.inspect}], var: var, start_object: self) + def #{name}(*args) + Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class_name}, self.class.associations[#{name.inspect}], start_object: self) end}, __FILE__, __LINE__) instance_eval(%Q{ - def #{name}(var = nil) - Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class.name}, @associations[#{name.inspect}], var: var, query_proxy: self.query_proxy) + def #{name}(*args) + Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class_name}, @associations[#{name.inspect}], query_proxy: self.query_proxy) end}, __FILE__, __LINE__) end diff --git a/lib/neo4j/active_node/has_n/association.rb b/lib/neo4j/active_node/has_n/association.rb index 7b94cf8be..7e110f821 100644 --- a/lib/neo4j/active_node/has_n/association.rb +++ b/lib/neo4j/active_node/has_n/association.rb @@ -2,16 +2,16 @@ module Neo4j module ActiveNode module HasN class Association - attr_reader :type, :name, :relationship, :direction + attr_reader :type, :name, :target_class, :relationship, :direction - def initialize(type, name, relationship, direction, options = {}) - raise ArgumentError if not [:has_many, :has_one].include?(type) - raise ArgumentError if not [:inbound, :outbound].include?(direction) + def initialize(type, name, options = {}) + raise ArgumentError, "Invalid association type: #{type.inspect}" if not [:has_many, :has_one].include?(type) @type = type @name = name - @relationship = relationship - @direction = direction + @direction = direction_from_options(options) + @target_class = target_class_from_options(options) + @relationship = relationship_from_options(options) end def arrow_cypher @@ -21,11 +21,48 @@ def arrow_cypher "-#{relationship_cypher}->" when :inbound "<-#{relationship_cypher}-" + when :bidirectional + "-#{relationship_cypher}-" else raise ArgumentError, "Invalid relationship direction: #{@direction.inspect}" end end + private + + # {to: Person} + # {from: Person} + # {with: Person} + # {direction: :inbound} + def direction_from_options(options) + to, from, with, direction = options.values_at(:to, :from, :with, :direction) + + raise ArgumentError, "Can only specify one of :to, :from, and :with" if [to, from, with].compact.size > 1 + + if to + :outbound + elsif from + :inbound + elsif with + :bidirectional + elsif direction + raise ArgumentError, "Invalid direction: #{direction.inspect}" if not [:outbound, :inbound, :bidirectional].include?(direction) + direction + else + :bidirectional + end + end + + def target_class_from_options(options) + options[:to] || options[:from] || options[:with] + end + + def relationship_from_options(options) + relationship = options[:through] + # Need to support false as matching any relationship + relationship = "#{@target_class ? @target_class.name : 'ANY'}##{@name}" if relationship.nil? + relationship + end end end end diff --git a/lib/neo4j/active_node/query/query_proxy.rb b/lib/neo4j/active_node/query/query_proxy.rb index c5205f5f7..fbf13add6 100644 --- a/lib/neo4j/active_node/query/query_proxy.rb +++ b/lib/neo4j/active_node/query/query_proxy.rb @@ -40,13 +40,17 @@ def query query_as(self._var || :result) end + def as(var) + @options[:var] = var + end # Build a Neo4j::Core::Query object for the QueryProxy def query_as(var) query = if @association chain_var = _association_chain_var var = self._var if self._var - (_association_query_start(chain_var) & _query_model_as(var)).match("#{chain_var}#{_association_arrow}(#{var}:`#{@model.name}`)") + label_string = @model && ":`#{@model.name}`" + (_association_query_start(chain_var) & _query_model_as(var)).match("#{chain_var}#{_association_arrow}(#{var}#{label_string})") else _query_model_as(var) end @@ -68,7 +72,7 @@ def to_cypher # To add a relationship for the node for the association on this QueryProxy def <<(other_node) if @association - raise ArgumentError, "Node must be of the association's class" if other_node.class != @model + raise ArgumentError, "Node must be of the association's class when model is specified" if @model && other_node.class != @model _association_query_start(:start) .match(end: other_node.class) @@ -80,7 +84,7 @@ def <<(other_node) end def method_missing(method_name, *args) - if @model.respond_to?(method_name) + if @model && @model.respond_to?(method_name) @model.query_proxy = self result = @model.send(method_name, *args) @model.query_proxy = nil @@ -98,8 +102,12 @@ def _add_links(links) end def _query_model_as(var) - label = @model.respond_to?(:mapped_label_name) ? @model.mapped_label_name : @model - neo4j_session.query.match(var => label) + if @model + label = @model.respond_to?(:mapped_label_name) ? @model.mapped_label_name : @model + neo4j_session.query.match(var => label) + else + neo4j_session.query.match(var) + end end def _association_arrow diff --git a/spec/e2e/query_spec.rb b/spec/e2e/query_spec.rb index 6375a8650..b51cce105 100644 --- a/spec/e2e/query_spec.rb +++ b/spec/e2e/query_spec.rb @@ -7,6 +7,8 @@ class Interest include Neo4j::ActiveNode property :name + + has_many :interested end class Lesson @@ -88,7 +90,9 @@ class Teacher danny.interests << reading bobby.interests << math - othmar.interests << monster_trucks + +# samuels.associate(monster_trucks, :interests, intensity: 1) +# othmar.associate(monster_trucks, :interests, intensity: 11) end it 'returns all' do @@ -99,7 +103,7 @@ class Teacher result.should include(othmar) end - it 'can filter' do + it 'allows filtering' do Teacher.where(name: /.*Othmar.*/).to_a.should == [othmar] end @@ -109,19 +113,19 @@ class Teacher samuels.lessons.to_set.should == [ss101, ss102, geo103, math101].to_set end - it 'can filter on associations' do + it 'allows filtering on associations' do samuels.lessons_teaching.where(level: 101).to_a.should == [ss101] end - it 'can call class methods on associations' do + it 'allows class methods on associations' do samuels.lessons_teaching.level(101).to_a.should == [ss101] samuels.lessons_teaching.max_level.should == 103 samuels.lessons_teaching.where(subject: 'Social Studies').max_level.should == 102 end - it 'can allow association chaining' do + it 'allows association chaining' do othmar.lessons_teaching.students.to_set.should == [sandra, danny].to_set othmar.lessons_teaching.students.interests.to_set.should == [reading].to_set @@ -129,14 +133,22 @@ class Teacher othmar.lessons_teaching.students.where(age: 16).to_a.should == [sandra] end - it 'can allow for filtering mid-association-chain' do + it 'allows for filtering mid-association-chain' do othmar.lessons_teaching.where(level: 201).students.to_a.should == [sandra] end - it 'can allow for returning nodes mis-association-chain' do - othmar.lessons_teaching(:lesson).students.where(age: 16).pluck(:lesson).should == [math201] + it 'allows for returning nodes mis-association-chain' do + othmar.lessons_teaching.as(:lesson).students.where(age: 16).pluck(:lesson).should == [math201] + + othmar.lessons_teaching.as(:lesson).students.as(:student).where(age: 16).pluck(:lesson, :student).should == [[math201, sandra]] + end + + it 'allows association with properties' do + monster_trucks.interested.to_set.should == [samuels, othmar] + + monster_trucks.interested(intensity: 11).to_set.should == [othmar] - othmar.lessons_teaching(:lesson).students(:student).where(age: 16).pluck(:lesson, :student).should == [[math201, sandra]] + monster_trucks.interested(r: 'r.intensity < 5').to_set.should == [samuels] end end end From 858dd9155019e1d14a8e60f042becb4d08f18dc6 Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Mon, 14 Jul 2014 19:51:30 -0700 Subject: [PATCH 18/54] Actually return a new QueryProxy object for #as calls --- lib/neo4j/active_node/query/query_proxy.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/neo4j/active_node/query/query_proxy.rb b/lib/neo4j/active_node/query/query_proxy.rb index fbf13add6..bc94d6035 100644 --- a/lib/neo4j/active_node/query/query_proxy.rb +++ b/lib/neo4j/active_node/query/query_proxy.rb @@ -37,18 +37,20 @@ def pluck(*args) # Like calling #query_as, but for when you don't care about the variable name def query - query_as(self._var || :result) + query_as(@var || :result) end def as(var) - @options[:var] = var + self.dup.tap do |new_query| + new_query.var = var + end end # Build a Neo4j::Core::Query object for the QueryProxy def query_as(var) query = if @association chain_var = _association_chain_var - var = self._var if self._var + var = @var if @var label_string = @model && ":`#{@model.name}`" (_association_query_start(chain_var) & _query_model_as(var)).match("#{chain_var}#{_association_arrow}(#{var}#{label_string})") else @@ -97,6 +99,8 @@ def method_missing(method_name, *args) protected # Methods are underscored to prevent conflict with user class methods + attr_accessor :var + def _add_links(links) @chain += links end @@ -124,15 +128,11 @@ def _chain_level end end - def _var - @options[:var] - end - def _association_chain_var if start_object = @options[:start_object] :"#{start_object.class.name.downcase}#{start_object.neo_id}" elsif query_proxy = @options[:query_proxy] - query_proxy._var || :"node#{_chain_level}" + query_proxy.var || :"node#{_chain_level}" else raise "Crazy error" # TODO: Better error end From 4116db7059c45a6b479a9fe5ecff29203708b38b Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Mon, 14 Jul 2014 19:52:03 -0700 Subject: [PATCH 19/54] Load the Query module first so that the #first method gets overwritten properly --- lib/neo4j/active_node.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/neo4j/active_node.rb b/lib/neo4j/active_node.rb index 3205a3dcf..8a4d8c90a 100644 --- a/lib/neo4j/active_node.rb +++ b/lib/neo4j/active_node.rb @@ -33,12 +33,12 @@ module ActiveNode include Neo4j::ActiveNode::Identity include Neo4j::ActiveNode::Persistence include Neo4j::ActiveNode::Property + include Neo4j::ActiveNode::Query include Neo4j::ActiveNode::Labels include Neo4j::ActiveNode::Validations include Neo4j::ActiveNode::Callbacks include Neo4j::ActiveNode::Rels include Neo4j::ActiveNode::HasN - include Neo4j::ActiveNode::Query def wrapper self From c8827fdebe08b7a82912bcd0d179b904b2341506 Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Tue, 15 Jul 2014 21:05:06 -0700 Subject: [PATCH 20/54] Changing specs to match discussion in issue #380 and fixing implementation to match --- lib/neo4j/active_node/has_n.rb | 11 +- lib/neo4j/active_node/has_n/association.rb | 37 ++++-- lib/neo4j/active_node/query.rb | 8 +- lib/neo4j/active_node/query/query_proxy.rb | 128 +++++++++++++-------- spec/e2e/query_spec.rb | 25 ++-- 5 files changed, 131 insertions(+), 78 deletions(-) diff --git a/lib/neo4j/active_node/has_n.rb b/lib/neo4j/active_node/has_n.rb index 6b43ca322..6757a94d3 100644 --- a/lib/neo4j/active_node/has_n.rb +++ b/lib/neo4j/active_node/has_n.rb @@ -6,9 +6,6 @@ def _decl_rels_for(rel_type) self.class._decl_rels[rel_type] end - def associate(node, association_name, properties) - - end module ClassMethods @@ -124,13 +121,13 @@ def has_many(name, options = {}) target_class_name = association.target_class ? association.target_class.name : 'nil' module_eval(%Q{ - def #{name}(*args) - Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class_name}, self.class.associations[#{name.inspect}], start_object: self) + def #{name}(node = nil, rel = nil) + Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class_name}, self.class.associations[#{name.inspect}], session: self.class.neo4j_session, start_object: self, node: node, rel: rel) end}, __FILE__, __LINE__) instance_eval(%Q{ - def #{name}(*args) - Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class_name}, @associations[#{name.inspect}], query_proxy: self.query_proxy) + def #{name}(node = nil, rel = nil) + Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class_name}, @associations[#{name.inspect}], session: self.neo4j_session, query_proxy: self.query_proxy, node: node, rel: rel) end}, __FILE__, __LINE__) end diff --git a/lib/neo4j/active_node/has_n/association.rb b/lib/neo4j/active_node/has_n/association.rb index 7e110f821..b66456272 100644 --- a/lib/neo4j/active_node/has_n/association.rb +++ b/lib/neo4j/active_node/has_n/association.rb @@ -14,8 +14,16 @@ def initialize(type, name, options = {}) @relationship = relationship_from_options(options) end - def arrow_cypher - relationship_cypher = (@relationship == false) ? '' : "[:`#{@relationship}`]" + def arrow_cypher(var = nil, properties = {}, create = false) + relationship_name = self.relationship_name(create) + relationship_name_cypher = ":`#{relationship_name}`" if relationship_name + + properties_string = properties.map do |key, value| + "#{key}: #{value.inspect}" + end.join(', ') + properties_string = " {#{properties_string}}" unless properties_string.empty? + + relationship_cypher = "[#{var}#{relationship_name_cypher}#{properties_string}]" case @direction.to_sym when :outbound "-#{relationship_cypher}->" @@ -28,12 +36,24 @@ def arrow_cypher end end + def relationship_name(create = false) + case @relationship + when nil # Need to support false as any relationship + if create + "#{@target_class ? @target_class.name : 'ANY'}##{@name}" + end + else + @relationship + end + end + private - # {to: Person} - # {from: Person} - # {with: Person} - # {direction: :inbound} + # Should support: + # {to: Model} + # {from: Model} + # {with: Model} + # {direction: [:inbound|:outbound|:bidirectional]} def direction_from_options(options) to, from, with, direction = options.values_at(:to, :from, :with, :direction) @@ -58,10 +78,7 @@ def target_class_from_options(options) end def relationship_from_options(options) - relationship = options[:through] - # Need to support false as matching any relationship - relationship = "#{@target_class ? @target_class.name : 'ANY'}##{@name}" if relationship.nil? - relationship + options[:through] end end end diff --git a/lib/neo4j/active_node/query.rb b/lib/neo4j/active_node/query.rb index eefd8b2b9..59f5b5bfc 100644 --- a/lib/neo4j/active_node/query.rb +++ b/lib/neo4j/active_node/query.rb @@ -52,14 +52,18 @@ def #{method}(*args) end}, __FILE__, __LINE__) end - def query_proxy - @query_proxy || Neo4j::ActiveNode::Query::QueryProxy.new(self) + def query_proxy(options = {}) + @query_proxy || Neo4j::ActiveNode::Query::QueryProxy.new(self, nil, options) end def qq(as = :n1) QuickQuery.new(self.name.constantize, as) end + def as(node_var) + query_proxy(node: node_var) + end + end end end diff --git a/lib/neo4j/active_node/query/query_proxy.rb b/lib/neo4j/active_node/query/query_proxy.rb index bc94d6035..0ee9ffeb9 100644 --- a/lib/neo4j/active_node/query/query_proxy.rb +++ b/lib/neo4j/active_node/query/query_proxy.rb @@ -9,11 +9,15 @@ def initialize(model, association = nil, options = {}) @model = model @association = association @options = options + @node_var = options[:node] + @rel_var = options[:rel] + @session = options[:session] @chain = [] + @params = {} end def each - query.pluck(:result).each do |obj| + query.pluck(@node_var || :result).each do |obj| yield obj end end @@ -35,29 +39,30 @@ def pluck(*args) self.query.pluck(*args) end - # Like calling #query_as, but for when you don't care about the variable name - def query - query_as(@var || :result) - end - - def as(var) + def params(params) self.dup.tap do |new_query| - new_query.var = var + new_query._add_params(params) end end + # Like calling #query_as, but for when you don't care about the variable name + def query + query_as(@node_var || :result) + end + # Build a Neo4j::Core::Query object for the QueryProxy def query_as(var) + var = @node_var if @node_var + query = if @association chain_var = _association_chain_var - var = @var if @var label_string = @model && ":`#{@model.name}`" (_association_query_start(chain_var) & _query_model_as(var)).match("#{chain_var}#{_association_arrow}(#{var}#{label_string})") else _query_model_as(var) end - @chain.inject(query) do |query, (method, arg)| + @chain.inject(query.params(@params)) do |query, (method, arg)| if arg.respond_to?(:call) query.send(method, arg.call(var)) else @@ -79,7 +84,20 @@ def <<(other_node) _association_query_start(:start) .match(end: other_node.class) .where(end: {neo_id: other_node.neo_id}) - .create("start#{_association_arrow}end").exec + .create("start#{_association_arrow({}, true)}end").exec + else + raise "Can only create associations on associations" + end + end + + def associate(other_node, properties) + if @association + raise ArgumentError, "Node must be of the association's class when model is specified" if @model && other_node.class != @model + + _association_query_start(:start) + .match(end: other_node.class) + .where(end: {neo_id: other_node.neo_id}) + .create("start#{_association_arrow(properties, true)}end").exec else raise "Can only create associations on associations" end @@ -99,7 +117,11 @@ def method_missing(method_name, *args) protected # Methods are underscored to prevent conflict with user class methods - attr_accessor :var + attr_reader :node_var + + def _add_params(params) + @params = @params.merge(params) + end def _add_links(links) @chain += links @@ -108,14 +130,18 @@ def _add_links(links) def _query_model_as(var) if @model label = @model.respond_to?(:mapped_label_name) ? @model.mapped_label_name : @model - neo4j_session.query.match(var => label) + _session.query.match(var => label) else - neo4j_session.query.match(var) + _session.query.match(var) end end - def _association_arrow - @association && @association.arrow_cypher + def _session + @session || (@model && @model.neo4j_session) + end + + def _association_arrow(properties = {}, create = false) + @association && @association.arrow_cypher(@rel_var, properties, create) end def _chain_level @@ -132,7 +158,7 @@ def _association_chain_var if start_object = @options[:start_object] :"#{start_object.class.name.downcase}#{start_object.neo_id}" elsif query_proxy = @options[:query_proxy] - query_proxy.var || :"node#{_chain_level}" + query_proxy.node_var || :"node#{_chain_level}" else raise "Crazy error" # TODO: Better error end @@ -152,49 +178,51 @@ def _association_query_start(var) def build_deeper_query_proxy(method, args) self.dup.tap do |new_query| - args.each do |arg| - new_query._add_links(links_for_arg(method, arg)) + args.each do |arg| + new_query._add_links(links_for_arg(method, arg)) + end end end - end - def links_for_arg(method, arg) - method_to_call = "links_for_#{method}_arg" + def links_for_arg(method, arg) + method_to_call = "links_for_#{method}_arg" - default = [[method, arg]] + default = [[method, arg]] - self.send(method_to_call, arg) || default - rescue NoMethodError - default - end + self.send(method_to_call, arg) || default + rescue NoMethodError + default + end - def links_for_where_arg(arg) - node_num = 1 - result = [] - if arg.is_a?(Hash) - arg.map do |key, value| - if @model.has_one_relationship?(key) - neo_id = value.try(:neo_id) || value - raise ArgumentError, "Invalid value for '#{key}' condition" if not neo_id.is_a?(Integer) - - n_string = "n#{node_num}" - dir = @model.relationship_dir(key) - - arrow = dir == :outgoing ? '-->' : '<--' - result << [:match, ->(v) { "#{v}#{arrow}(#{n_string})" }] - result << [:where, ->(v) { {"ID(#{n_string})" => neo_id.to_i} }] - node_num += 1 - else - result << [:where, ->(v) { {v => {key => value}}}] + def links_for_where_arg(arg) + node_num = 1 + result = [] + if arg.is_a?(Hash) + arg.map do |key, value| + if @model.has_one_relationship?(key) + neo_id = value.try(:neo_id) || value + raise ArgumentError, "Invalid value for '#{key}' condition" if not neo_id.is_a?(Integer) + + n_string = "n#{node_num}" + dir = @model.relationship_dir(key) + + arrow = dir == :outgoing ? '-->' : '<--' + result << [:match, ->(v) { "#{v}#{arrow}(#{n_string})" }] + result << [:where, ->(v) { {"ID(#{n_string})" => neo_id.to_i} }] + node_num += 1 + else + result << [:where, ->(v) { {v => {key => value}}}] + end end + elsif arg.is_a?(String) + result << [:where, arg] end + result end - result - end - def links_for_order_arg(arg) - [[:order, ->(v) { {v => arg} }]] - end + def links_for_order_arg(arg) + [[:order, ->(v) { {v => arg} }]] + end end diff --git a/spec/e2e/query_spec.rb b/spec/e2e/query_spec.rb index b51cce105..a6ebebac7 100644 --- a/spec/e2e/query_spec.rb +++ b/spec/e2e/query_spec.rb @@ -91,8 +91,8 @@ class Teacher danny.interests << reading bobby.interests << math -# samuels.associate(monster_trucks, :interests, intensity: 1) -# othmar.associate(monster_trucks, :interests, intensity: 11) + samuels.interests.associate(monster_trucks, intensity: 1) + othmar.interests.associate(monster_trucks, intensity: 11) end it 'returns all' do @@ -107,15 +107,24 @@ class Teacher Teacher.where(name: /.*Othmar.*/).to_a.should == [othmar] end + it 'allows definining of a variable for class as start of QueryProxy chain' do + Teacher.as(:t).lessons.where(level: 101).pluck(:t).to_set.should == [samuels, othmar].to_set + end + it 'returns only objects specified by association' do samuels.lessons_teaching.to_set.should == [ss101, ss102, geo103].to_set samuels.lessons.to_set.should == [ss101, ss102, geo103, math101].to_set end + it 'allows params' do + Teacher.as(:t).where("t.name = {name}").params(name: 'Harold Samuels').to_a.should == [samuels] + + # Example here for params on association + end + it 'allows filtering on associations' do samuels.lessons_teaching.where(level: 101).to_a.should == [ss101] - end it 'allows class methods on associations' do @@ -138,17 +147,15 @@ class Teacher end it 'allows for returning nodes mis-association-chain' do - othmar.lessons_teaching.as(:lesson).students.where(age: 16).pluck(:lesson).should == [math201] + othmar.lessons_teaching(:lesson).students.where(age: 16).pluck(:lesson).should == [math201] - othmar.lessons_teaching.as(:lesson).students.as(:student).where(age: 16).pluck(:lesson, :student).should == [[math201, sandra]] + othmar.lessons_teaching(:lesson).students(:student).where(age: 16).pluck(:lesson, :student).should == [[math201, sandra]] end it 'allows association with properties' do - monster_trucks.interested.to_set.should == [samuels, othmar] - - monster_trucks.interested(intensity: 11).to_set.should == [othmar] + monster_trucks.interested.to_set.should == [samuels, othmar].to_set - monster_trucks.interested(r: 'r.intensity < 5').to_set.should == [samuels] + monster_trucks.interested(:person, :r).where('r.intensity < 5').pluck(:person).to_set.should == [samuels].to_set end end end From 4e98e0958d5b1eab589b684793f7800e9d88e62d Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Tue, 15 Jul 2014 21:15:50 -0700 Subject: [PATCH 21/54] Double checking params support --- spec/e2e/query_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/e2e/query_spec.rb b/spec/e2e/query_spec.rb index a6ebebac7..6becdb12b 100644 --- a/spec/e2e/query_spec.rb +++ b/spec/e2e/query_spec.rb @@ -120,7 +120,8 @@ class Teacher it 'allows params' do Teacher.as(:t).where("t.name = {name}").params(name: 'Harold Samuels').to_a.should == [samuels] - # Example here for params on association + samuels.lessons_teaching(:lesson).where("lesson.level = {level}").params(level: 103).to_a.should == [geo103] + samuels.lessons_teaching.where(level: "{level}").params(level: 103).to_a.should == [geo103] end it 'allows filtering on associations' do From aa83f7a65d111496bb9aa60374b06a9dbad2f68a Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Wed, 16 Jul 2014 09:06:53 -0700 Subject: [PATCH 22/54] Require neo4j-core alpha.18 --- neo4j.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neo4j.gemspec b/neo4j.gemspec index b42ee1841..f8436485f 100644 --- a/neo4j.gemspec +++ b/neo4j.gemspec @@ -33,7 +33,7 @@ It comes included with the Apache Lucene document database. s.add_dependency("activemodel", "~> 4") s.add_dependency("railties", "~> 4") s.add_dependency('active_attr', "~> 0.8") - s.add_dependency("neo4j-core", "= 3.0.0.alpha.17") + s.add_dependency("neo4j-core", "= 3.0.0.alpha.18") if RUBY_PLATFORM =~ /java/ s.add_dependency("neo4j-community", '~> 2.0') From 8dacca1dd56482adf308e151d62d79450912e4a8 Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Fri, 18 Jul 2014 21:14:23 -0700 Subject: [PATCH 23/54] Changing has_many calls in query_spec to match new syntax and getting specs back to passing --- Gemfile | 2 ++ lib/neo4j/active_node/has_n.rb | 7 ++-- lib/neo4j/active_node/has_n/association.rb | 37 ++++++++++++---------- lib/neo4j/active_node/query/query_proxy.rb | 2 +- spec/e2e/query_spec.rb | 16 +++++----- 5 files changed, 35 insertions(+), 29 deletions(-) diff --git a/Gemfile b/Gemfile index 8f7736a40..856d7f822 100644 --- a/Gemfile +++ b/Gemfile @@ -8,6 +8,8 @@ gemspec gem 'coveralls', require: false +gem 'activesupport' + group 'development' do gem 'pry' gem 'os' # for neo4j-server rake task diff --git a/lib/neo4j/active_node/has_n.rb b/lib/neo4j/active_node/has_n.rb index 6757a94d3..bc9fe12e6 100644 --- a/lib/neo4j/active_node/has_n.rb +++ b/lib/neo4j/active_node/has_n.rb @@ -10,6 +10,10 @@ def _decl_rels_for(rel_type) module ClassMethods + def has_association?(name) + !!@associations[name] + end + def has_relationship?(rel_type) !!_decl_rels[rel_type] end @@ -111,9 +115,6 @@ def #{rel_type} def has_many(name, options = {}) name = name.to_sym - @has_many_relationships ||= [] - @has_many_relationships << name - association = Neo4j::ActiveNode::HasN::Association.new(:has_many, name, options) @associations ||= {} @associations[name] = association diff --git a/lib/neo4j/active_node/has_n/association.rb b/lib/neo4j/active_node/has_n/association.rb index b66456272..3f9c5e97b 100644 --- a/lib/neo4j/active_node/has_n/association.rb +++ b/lib/neo4j/active_node/has_n/association.rb @@ -1,3 +1,5 @@ +require 'active_support/inflector/inflections' + module Neo4j module ActiveNode module HasN @@ -10,8 +12,12 @@ def initialize(type, name, options = {}) @type = type @name = name @direction = direction_from_options(options) - @target_class = target_class_from_options(options) - @relationship = relationship_from_options(options) + @target_class = begin + options[:model] || name.to_s.classify.constantize + rescue NameError + end + + @relationship = options[:via] || options[:from] || options[:with] end def arrow_cypher(var = nil, properties = {}, create = false) @@ -24,7 +30,11 @@ def arrow_cypher(var = nil, properties = {}, create = false) properties_string = " {#{properties_string}}" unless properties_string.empty? relationship_cypher = "[#{var}#{relationship_name_cypher}#{properties_string}]" - case @direction.to_sym + + direction = @direction + direction = :outbound if create && @direction == :bidirectional + + case direction.to_sym when :outbound "-#{relationship_cypher}->" when :inbound @@ -32,15 +42,15 @@ def arrow_cypher(var = nil, properties = {}, create = false) when :bidirectional "-#{relationship_cypher}-" else - raise ArgumentError, "Invalid relationship direction: #{@direction.inspect}" + raise ArgumentError, "Invalid relationship direction: #{direction.inspect}" end end def relationship_name(create = false) case @relationship - when nil # Need to support false as any relationship + when nil if create - "#{@target_class ? @target_class.name : 'ANY'}##{@name}" + "##{@name}" end else @relationship @@ -50,16 +60,16 @@ def relationship_name(create = false) private # Should support: - # {to: Model} + # {via: Model} # {from: Model} # {with: Model} # {direction: [:inbound|:outbound|:bidirectional]} def direction_from_options(options) - to, from, with, direction = options.values_at(:to, :from, :with, :direction) + via, from, with = options.values_at(:via, :from, :with) - raise ArgumentError, "Can only specify one of :to, :from, and :with" if [to, from, with].compact.size > 1 + raise ArgumentError, "Can only specify one of :via, :from, and :with" if [via, from, with].compact.size > 1 - if to + if via :outbound elsif from :inbound @@ -73,13 +83,6 @@ def direction_from_options(options) end end - def target_class_from_options(options) - options[:to] || options[:from] || options[:with] - end - - def relationship_from_options(options) - options[:through] - end end end end diff --git a/lib/neo4j/active_node/query/query_proxy.rb b/lib/neo4j/active_node/query/query_proxy.rb index 0ee9ffeb9..e3a83ea55 100644 --- a/lib/neo4j/active_node/query/query_proxy.rb +++ b/lib/neo4j/active_node/query/query_proxy.rb @@ -199,7 +199,7 @@ def links_for_where_arg(arg) result = [] if arg.is_a?(Hash) arg.map do |key, value| - if @model.has_one_relationship?(key) + if @model && @model.has_association?(key) neo_id = value.try(:neo_id) || value raise ArgumentError, "Invalid value for '#{key}' condition" if not neo_id.is_a?(Integer) diff --git a/spec/e2e/query_spec.rb b/spec/e2e/query_spec.rb index 6becdb12b..d65f8c48f 100644 --- a/spec/e2e/query_spec.rb +++ b/spec/e2e/query_spec.rb @@ -16,8 +16,8 @@ class Lesson property :subject property :level - has_many :teachers, from: Teacher, through: :teaching - has_many :students, from: Student, through: :is_enrolled_for + has_many :teachers, from: :teaching + has_many :students, from: :is_enrolled_for def self.max_level self.query_as(:lesson).pluck('max(lesson.level)').first @@ -33,21 +33,21 @@ class Student property :name property :age, type: Integer - has_many :lessons, to: Lesson, through: :is_enrolled_for + has_many :lessons, via: :is_enrolled_for - has_many :interests, to: Interest + has_many :interests, direction: :outbound end class Teacher include Neo4j::ActiveNode property :name - has_many :lessons_teaching, to: Lesson, through: :teaching - has_many :lessons_taught, to: Lesson, through: :taught + has_many :lessons_teaching, via: :teaching, model: Lesson + has_many :lessons_taught, via: :taught, model: Lesson - has_many :lessons, to: Lesson, through: false + has_many :lessons - has_many :interests, to: Interest + has_many :interests, direction: :outbound end describe 'Query API' do From 7b407e09d976318a00795c73301ee765b969dbfa Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Fri, 18 Jul 2014 21:45:27 -0700 Subject: [PATCH 24/54] Match on relationship if model specified for has_many and that model doesn't match what would have come from the association name --- lib/neo4j/active_node/has_n/association.rb | 5 +++-- spec/e2e/query_spec.rb | 11 +++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/neo4j/active_node/has_n/association.rb b/lib/neo4j/active_node/has_n/association.rb index 3f9c5e97b..e3f424c67 100644 --- a/lib/neo4j/active_node/has_n/association.rb +++ b/lib/neo4j/active_node/has_n/association.rb @@ -12,8 +12,9 @@ def initialize(type, name, options = {}) @type = type @name = name @direction = direction_from_options(options) + @target_class_name_from_name = name.to_s.classify @target_class = begin - options[:model] || name.to_s.classify.constantize + options[:model] || @target_class_name_from_name.constantize rescue NameError end @@ -49,7 +50,7 @@ def arrow_cypher(var = nil, properties = {}, create = false) def relationship_name(create = false) case @relationship when nil - if create + if create || (@target_class && @target_class.name != @target_class_name_from_name) "##{@name}" end else diff --git a/spec/e2e/query_spec.rb b/spec/e2e/query_spec.rb index d65f8c48f..f7af691b7 100644 --- a/spec/e2e/query_spec.rb +++ b/spec/e2e/query_spec.rb @@ -36,6 +36,9 @@ class Student has_many :lessons, via: :is_enrolled_for has_many :interests, direction: :outbound + + has_many :favorite_teachers, model: Teacher + has_many :hated_teachers, model: Teacher end class Teacher @@ -117,6 +120,14 @@ class Teacher samuels.lessons.to_set.should == [ss101, ss102, geo103, math101].to_set end + it 'differentiates associations on the same model for the same class' do + bobby.favorite_teachers << samuels + bobby.hated_teachers << othmar + + bobby.favorite_teachers.to_set.should == [samuels].to_set + bobby.hated_teachers.to_set.should == [othmar].to_set + end + it 'allows params' do Teacher.as(:t).where("t.name = {name}").params(name: 'Harold Samuels').to_a.should == [samuels] From a999dedddff4639c01f3f248655722abff80c562 Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Fri, 18 Jul 2014 22:14:55 -0700 Subject: [PATCH 25/54] A little bit of code cleanup and commenting --- lib/neo4j/active_node/has_n/association.rb | 32 +++++++++++++--------- lib/neo4j/active_node/query/query_proxy.rb | 9 +++--- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/lib/neo4j/active_node/has_n/association.rb b/lib/neo4j/active_node/has_n/association.rb index e3f424c67..42c0a38d4 100644 --- a/lib/neo4j/active_node/has_n/association.rb +++ b/lib/neo4j/active_node/has_n/association.rb @@ -21,6 +21,7 @@ def initialize(type, name, options = {}) @relationship = options[:via] || options[:from] || options[:with] end + # Return cypher partial query string for the relationship part of a MATCH (arrow / relationship definition) def arrow_cypher(var = nil, properties = {}, create = false) relationship_name = self.relationship_name(create) relationship_name_cypher = ":`#{relationship_name}`" if relationship_name @@ -48,23 +49,28 @@ def arrow_cypher(var = nil, properties = {}, create = false) end def relationship_name(create = false) - case @relationship - when nil - if create || (@target_class && @target_class.name != @target_class_name_from_name) - "##{@name}" - end - else - @relationship - end + @relationship || (create || exceptional_target_class?) && "##{@name}" end private - # Should support: - # {via: Model} - # {from: Model} - # {with: Model} - # {direction: [:inbound|:outbound|:bidirectional]} + # Determine if model class as derived from the association name would be different than the one specified via the model key + # @example + # has_many :friends # Would return false + # has_many :friends, model: Friend # Would return false + # has_many :friends, model: Person # Would return true + def exceptional_target_class? + @target_class && @target_class.name != @target_class_name_from_name + end + + # Determine which direction is desired for the assication from the association options + # Can be specified by using the via/from/with keys, or by using the direction key + # + # @example + # has_many :a, via: Model + # has_many :a, from: Model + # has_many :a, with: Model + # has_many :a, direction: [:inbound|:outbound|:bidirectional] def direction_from_options(options) via, from, with = options.values_at(:via, :from, :with) diff --git a/lib/neo4j/active_node/query/query_proxy.rb b/lib/neo4j/active_node/query/query_proxy.rb index e3a83ea55..b811ad8a7 100644 --- a/lib/neo4j/active_node/query/query_proxy.rb +++ b/lib/neo4j/active_node/query/query_proxy.rb @@ -62,12 +62,9 @@ def query_as(var) _query_model_as(var) end + # Build a query chain via the chain, return the result @chain.inject(query.params(@params)) do |query, (method, arg)| - if arg.respond_to?(:call) - query.send(method, arg.call(var)) - else - query.send(method, arg) - end + query.send(method, arg.respond_to?(:call) ? arg.call(var) : arg) end end @@ -103,6 +100,8 @@ def associate(other_node, properties) end end + # QueryProxy objects act as a representation of a model at the class level so we pass through calls + # This allows us to define class functions for reusable query chaining or for end-of-query aggregation/summarizing def method_missing(method_name, *args) if @model && @model.respond_to?(method_name) @model.query_proxy = self From debd31f25660405e00a3bbb7582f9cfd8dd473f0 Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Thu, 24 Jul 2014 10:56:05 -0700 Subject: [PATCH 26/54] Change to `has_many `, relationship type specified by `type`, require `model: false` to make an association to any model --- lib/neo4j/active_node/has_n.rb | 4 +- lib/neo4j/active_node/has_n/association.rb | 55 +++++++--------------- spec/e2e/query_spec.rb | 22 ++++----- 3 files changed, 30 insertions(+), 51 deletions(-) diff --git a/lib/neo4j/active_node/has_n.rb b/lib/neo4j/active_node/has_n.rb index bc9fe12e6..4fa95300a 100644 --- a/lib/neo4j/active_node/has_n.rb +++ b/lib/neo4j/active_node/has_n.rb @@ -112,10 +112,10 @@ def #{rel_type} _decl_rels[rel_type.to_sym] = DeclRel.new(rel_type, false, clazz) end - def has_many(name, options = {}) + def has_many(direction, name, options = {}) name = name.to_sym - association = Neo4j::ActiveNode::HasN::Association.new(:has_many, name, options) + association = Neo4j::ActiveNode::HasN::Association.new(:has_many, direction, name, options) @associations ||= {} @associations[name] = association diff --git a/lib/neo4j/active_node/has_n/association.rb b/lib/neo4j/active_node/has_n/association.rb index 42c0a38d4..54b1fbce7 100644 --- a/lib/neo4j/active_node/has_n/association.rb +++ b/lib/neo4j/active_node/has_n/association.rb @@ -6,25 +6,31 @@ module HasN class Association attr_reader :type, :name, :target_class, :relationship, :direction - def initialize(type, name, options = {}) + def initialize(type, direction, name, options = {}) raise ArgumentError, "Invalid association type: #{type.inspect}" if not [:has_many, :has_one].include?(type) + raise ArgumentError, "Invalid direction: #{direction.inspect}" if not [:outbound, :inbound, :bidirectional].include?(direction) @type = type @name = name - @direction = direction_from_options(options) + @direction = direction @target_class_name_from_name = name.to_s.classify @target_class = begin - options[:model] || @target_class_name_from_name.constantize + if options[:model_class].nil? + @target_class_name_from_name.constantize + elsif options[:model_class] + options[:model_class] + end rescue NameError + raise ArgumentError, "Could not find #{@target_class_name_from_name} class and no model_class specified" end - @relationship = options[:via] || options[:from] || options[:with] + @relationship_type = options[:type] end # Return cypher partial query string for the relationship part of a MATCH (arrow / relationship definition) def arrow_cypher(var = nil, properties = {}, create = false) - relationship_name = self.relationship_name(create) - relationship_name_cypher = ":`#{relationship_name}`" if relationship_name + relationship_type = self.relationship_type(create) + relationship_name_cypher = ":`#{relationship_type}`" if relationship_type properties_string = properties.map do |key, value| "#{key}: #{value.inspect}" @@ -48,48 +54,21 @@ def arrow_cypher(var = nil, properties = {}, create = false) end end - def relationship_name(create = false) - @relationship || (create || exceptional_target_class?) && "##{@name}" + def relationship_type(create = false) + @relationship_type || (create || exceptional_target_class?) && "##{@name}" end private - # Determine if model class as derived from the association name would be different than the one specified via the model key + # Determine if model class as derived from the association name would be different than the one specified via the model_class key # @example # has_many :friends # Would return false - # has_many :friends, model: Friend # Would return false - # has_many :friends, model: Person # Would return true + # has_many :friends, model_class: Friend # Would return false + # has_many :friends, model_class: Person # Would return true def exceptional_target_class? @target_class && @target_class.name != @target_class_name_from_name end - # Determine which direction is desired for the assication from the association options - # Can be specified by using the via/from/with keys, or by using the direction key - # - # @example - # has_many :a, via: Model - # has_many :a, from: Model - # has_many :a, with: Model - # has_many :a, direction: [:inbound|:outbound|:bidirectional] - def direction_from_options(options) - via, from, with = options.values_at(:via, :from, :with) - - raise ArgumentError, "Can only specify one of :via, :from, and :with" if [via, from, with].compact.size > 1 - - if via - :outbound - elsif from - :inbound - elsif with - :bidirectional - elsif direction - raise ArgumentError, "Invalid direction: #{direction.inspect}" if not [:outbound, :inbound, :bidirectional].include?(direction) - direction - else - :bidirectional - end - end - end end end diff --git a/spec/e2e/query_spec.rb b/spec/e2e/query_spec.rb index f7af691b7..07f08dc03 100644 --- a/spec/e2e/query_spec.rb +++ b/spec/e2e/query_spec.rb @@ -8,7 +8,7 @@ class Interest property :name - has_many :interested + has_many :bidirectional, :interested, model_class: false end class Lesson @@ -16,8 +16,8 @@ class Lesson property :subject property :level - has_many :teachers, from: :teaching - has_many :students, from: :is_enrolled_for + has_many :inbound, :teachers, type: :teaching + has_many :inbound, :students, type: :is_enrolled_for def self.max_level self.query_as(:lesson).pluck('max(lesson.level)').first @@ -33,24 +33,24 @@ class Student property :name property :age, type: Integer - has_many :lessons, via: :is_enrolled_for + has_many :outbound, :lessons, type: :is_enrolled_for - has_many :interests, direction: :outbound + has_many :outbound, :interests - has_many :favorite_teachers, model: Teacher - has_many :hated_teachers, model: Teacher + has_many :bidirectional, :favorite_teachers, model_class: Teacher + has_many :bidirectional, :hated_teachers, model_class: Teacher end class Teacher include Neo4j::ActiveNode property :name - has_many :lessons_teaching, via: :teaching, model: Lesson - has_many :lessons_taught, via: :taught, model: Lesson + has_many :bidirectional, :lessons - has_many :lessons + has_many :outbound, :lessons_teaching, model_class: Lesson + has_many :outbound, :lessons_taught, model_class: Lesson - has_many :interests, direction: :outbound + has_many :outbound, :interests end describe 'Query API' do From 04d1407be5aa1189a52abbb44a964fa6129e7afd Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Thu, 24 Jul 2014 10:58:16 -0700 Subject: [PATCH 27/54] Move `to_sym` calls to initializer --- lib/neo4j/active_node/has_n/association.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/neo4j/active_node/has_n/association.rb b/lib/neo4j/active_node/has_n/association.rb index 54b1fbce7..623da5be6 100644 --- a/lib/neo4j/active_node/has_n/association.rb +++ b/lib/neo4j/active_node/has_n/association.rb @@ -7,12 +7,12 @@ class Association attr_reader :type, :name, :target_class, :relationship, :direction def initialize(type, direction, name, options = {}) - raise ArgumentError, "Invalid association type: #{type.inspect}" if not [:has_many, :has_one].include?(type) - raise ArgumentError, "Invalid direction: #{direction.inspect}" if not [:outbound, :inbound, :bidirectional].include?(direction) + raise ArgumentError, "Invalid association type: #{type.inspect}" if not [:has_many, :has_one].include?(type.to_sym) + raise ArgumentError, "Invalid direction: #{direction.inspect}" if not [:outbound, :inbound, :bidirectional].include?(direction.to_sym) - @type = type + @type = type.to_sym @name = name - @direction = direction + @direction = direction.to_sym @target_class_name_from_name = name.to_s.classify @target_class = begin if options[:model_class].nil? @@ -42,7 +42,7 @@ def arrow_cypher(var = nil, properties = {}, create = false) direction = @direction direction = :outbound if create && @direction == :bidirectional - case direction.to_sym + case direction when :outbound "-#{relationship_cypher}->" when :inbound From 26d0b2f3f1cce050fa8ae67b6cd455550224f9ef Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Sun, 27 Jul 2014 16:24:59 -0700 Subject: [PATCH 28/54] inbound/outbound/bidirectional to in/out/both. Supporting `origin` and putting in some validation for `origin`. Still need specs around validation (and probably more). Added unit specs for Association class --- lib/neo4j/active_node/has_n.rb | 3 +- lib/neo4j/active_node/has_n/association.rb | 51 +++++- spec/e2e/query_spec.rb | 175 ++++++++++++--------- spec/unit/association.rb | 90 +++++++++++ 4 files changed, 237 insertions(+), 82 deletions(-) create mode 100644 spec/unit/association.rb diff --git a/lib/neo4j/active_node/has_n.rb b/lib/neo4j/active_node/has_n.rb index 4fa95300a..f3206838d 100644 --- a/lib/neo4j/active_node/has_n.rb +++ b/lib/neo4j/active_node/has_n.rb @@ -9,7 +9,6 @@ def _decl_rels_for(rel_type) module ClassMethods - def has_association?(name) !!@associations[name] end @@ -116,6 +115,8 @@ def has_many(direction, name, options = {}) name = name.to_sym association = Neo4j::ActiveNode::HasN::Association.new(:has_many, direction, name, options) + name = name.to_sym + @associations ||= {} @associations[name] = association diff --git a/lib/neo4j/active_node/has_n/association.rb b/lib/neo4j/active_node/has_n/association.rb index 623da5be6..72b5db10e 100644 --- a/lib/neo4j/active_node/has_n/association.rb +++ b/lib/neo4j/active_node/has_n/association.rb @@ -8,7 +8,7 @@ class Association def initialize(type, direction, name, options = {}) raise ArgumentError, "Invalid association type: #{type.inspect}" if not [:has_many, :has_one].include?(type.to_sym) - raise ArgumentError, "Invalid direction: #{direction.inspect}" if not [:outbound, :inbound, :bidirectional].include?(direction.to_sym) + raise ArgumentError, "Invalid direction: #{direction.inspect}" if not [:out, :in, :both].include?(direction.to_sym) @type = type.to_sym @name = name @@ -21,10 +21,17 @@ def initialize(type, direction, name, options = {}) options[:model_class] end rescue NameError - raise ArgumentError, "Could not find #{@target_class_name_from_name} class and no model_class specified" + raise ArgumentError, "Could not find `#{@target_class_name_from_name}` class and no :model_class specified" end - @relationship_type = options[:type] + if options[:type] && options[:origin] + raise ArgumentError, "Cannot specify both :type and :origin (#{self.base_declaration})" + else + @relationship_type = options[:type] && options[:type].to_sym + @origin = options[:origin] && options[:origin].to_sym + + validate_origin! + end end # Return cypher partial query string for the relationship part of a MATCH (arrow / relationship definition) @@ -40,14 +47,14 @@ def arrow_cypher(var = nil, properties = {}, create = false) relationship_cypher = "[#{var}#{relationship_name_cypher}#{properties_string}]" direction = @direction - direction = :outbound if create && @direction == :bidirectional + direction = :out if create && @direction == :both case direction - when :outbound + when :out "-#{relationship_cypher}->" - when :inbound + when :in "<-#{relationship_cypher}-" - when :bidirectional + when :both "-#{relationship_cypher}-" else raise ArgumentError, "Invalid relationship direction: #{direction.inspect}" @@ -55,7 +62,20 @@ def arrow_cypher(var = nil, properties = {}, create = false) end def relationship_type(create = false) - @relationship_type || (create || exceptional_target_class?) && "##{@name}" + if @relationship_type + @relationship_type + elsif @origin + "##{@origin}" + else + (create || exceptional_target_class?) && "##{@name}" + end + end + + # Return basic details about association as declared in the model + # @example + # has_many :in, :bands + def base_declaration + "#{type} #{direction.inspect}, #{name.inspect}" end private @@ -69,6 +89,21 @@ def exceptional_target_class? @target_class && @target_class.name != @target_class_name_from_name end + def validate_origin! + if @origin + if @target_class + if association = @target_class.associations[@origin] + if @direction == association.direction + raise ArgumentError, "Origin `#{@origin.inspect}` (specified in #{self.base_declaration}) has same direction `#{@direction}`)" + end + else + raise ArgumentError, "Origin `#{@origin.inspect}` association not found for #{@target_class} (specified in #{self.base_declaration})" + end + else + raise ArgumentError, "Cannot use :origin without a model_class (implied or explicit)" + end + end + end end end end diff --git a/spec/e2e/query_spec.rb b/spec/e2e/query_spec.rb index 07f08dc03..d0fdd21a4 100644 --- a/spec/e2e/query_spec.rb +++ b/spec/e2e/query_spec.rb @@ -8,7 +8,7 @@ class Interest property :name - has_many :bidirectional, :interested, model_class: false + has_many :both, :interested, model_class: false end class Lesson @@ -16,8 +16,8 @@ class Lesson property :subject property :level - has_many :inbound, :teachers, type: :teaching - has_many :inbound, :students, type: :is_enrolled_for + has_many :in, :teachers, type: :teaching + has_many :in, :students, type: :is_enrolled_for def self.max_level self.query_as(:lesson).pluck('max(lesson.level)').first @@ -33,24 +33,24 @@ class Student property :name property :age, type: Integer - has_many :outbound, :lessons, type: :is_enrolled_for + has_many :out, :lessons, type: :is_enrolled_for - has_many :outbound, :interests + has_many :out, :interests - has_many :bidirectional, :favorite_teachers, model_class: Teacher - has_many :bidirectional, :hated_teachers, model_class: Teacher + has_many :both, :favorite_teachers, model_class: Teacher + has_many :both, :hated_teachers, model_class: Teacher end class Teacher include Neo4j::ActiveNode property :name - has_many :bidirectional, :lessons + has_many :both, :lessons - has_many :outbound, :lessons_teaching, model_class: Lesson - has_many :outbound, :lessons_taught, model_class: Lesson + has_many :out, :lessons_teaching, model_class: Lesson + has_many :out, :lessons_taught, model_class: Lesson - has_many :outbound, :interests + has_many :out, :interests end describe 'Query API' do @@ -68,36 +68,12 @@ class Teacher let!(:sandra) { Student.create(name: 'Sandra', age: 16) } let!(:danny) { Student.create(name: 'Danny', age: 15) } let!(:bobby) { Student.create(name: 'Bobby', age: 16) } + let!(:brian) { Student.create(name: 'Bobby', age: 25) } let!(:reading) { Interest.create(name: 'Reading') } let!(:math) { Interest.create(name: 'Math') } let!(:monster_trucks) { Interest.create(name: 'Monster Trucks') } - before(:each) do - samuels.lessons_teaching << ss101 - samuels.lessons_teaching << ss102 - samuels.lessons_teaching << geo103 - samuels.lessons_taught << math101 - - othmar.lessons_teaching << math101 - othmar.lessons_teaching << math201 - - - sandra.lessons << math201 - sandra.lessons << ss102 - - danny.lessons << math101 - danny.lessons << ss102 - - bobby.lessons << ss102 - - danny.interests << reading - bobby.interests << math - - samuels.interests.associate(monster_trucks, intensity: 1) - othmar.interests.associate(monster_trucks, intensity: 11) - end - it 'returns all' do result = Teacher.to_a @@ -110,64 +86,117 @@ class Teacher Teacher.where(name: /.*Othmar.*/).to_a.should == [othmar] end - it 'allows definining of a variable for class as start of QueryProxy chain' do - Teacher.as(:t).lessons.where(level: 101).pluck(:t).to_set.should == [samuels, othmar].to_set - end + context 'samuels teaching soc 101 and 102 lessons' do + before(:each) do + samuels.lessons_teaching << ss101 + samuels.lessons_teaching << ss102 + end - it 'returns only objects specified by association' do - samuels.lessons_teaching.to_set.should == [ss101, ss102, geo103].to_set + it 'allows definining of a variable for class as start of QueryProxy chain' do + Teacher.as(:t).lessons.where(level: 101).pluck(:t).should == [samuels] + end - samuels.lessons.to_set.should == [ss101, ss102, geo103, math101].to_set - end + context 'samuels taught math 101 lesson' do + before(:each) { samuels.lessons_taught << math101 } - it 'differentiates associations on the same model for the same class' do - bobby.favorite_teachers << samuels - bobby.hated_teachers << othmar + it 'returns only objects specified by association' do + samuels.lessons_teaching.to_set.should == [ss101, ss102].to_set - bobby.favorite_teachers.to_set.should == [samuels].to_set - bobby.hated_teachers.to_set.should == [othmar].to_set + samuels.lessons.to_set.should == [ss101, ss102, math101].to_set + end + end end - it 'allows params' do - Teacher.as(:t).where("t.name = {name}").params(name: 'Harold Samuels').to_a.should == [samuels] + context 'bobby has teacher preferences' do + before(:each) do + bobby.favorite_teachers << samuels + bobby.hated_teachers << othmar + end - samuels.lessons_teaching(:lesson).where("lesson.level = {level}").params(level: 103).to_a.should == [geo103] - samuels.lessons_teaching.where(level: "{level}").params(level: 103).to_a.should == [geo103] + it 'differentiates associations on the same model for the same class' do + bobby.favorite_teachers.to_set.should == [samuels].to_set + bobby.hated_teachers.to_set.should == [othmar].to_set + end end - it 'allows filtering on associations' do - samuels.lessons_teaching.where(level: 101).to_a.should == [ss101] - end + context 'samuels is teaching soc 101 and geo 103' do + before(:each) do + samuels.lessons_teaching << ss101 + samuels.lessons_teaching << geo103 + end - it 'allows class methods on associations' do - samuels.lessons_teaching.level(101).to_a.should == [ss101] + it 'allows params' do + Teacher.as(:t).where("t.name = {name}").params(name: 'Harold Samuels').to_a.should == [samuels] - samuels.lessons_teaching.max_level.should == 103 - samuels.lessons_teaching.where(subject: 'Social Studies').max_level.should == 102 - end + samuels.lessons_teaching(:lesson).where("lesson.level = {level}").params(level: 103).to_a.should == [geo103] + samuels.lessons_teaching.where(level: "{level}").params(level: 103).to_a.should == [geo103] + end - it 'allows association chaining' do - othmar.lessons_teaching.students.to_set.should == [sandra, danny].to_set + it 'allows filtering on associations' do + samuels.lessons_teaching.where(level: 101).to_a.should == [ss101] + end - othmar.lessons_teaching.students.interests.to_set.should == [reading].to_set + it 'allows class methods on associations' do + samuels.lessons_teaching.level(101).to_a.should == [ss101] - othmar.lessons_teaching.students.where(age: 16).to_a.should == [sandra] + samuels.lessons_teaching.max_level.should == 103 + samuels.lessons_teaching.where(subject: 'Social Studies').max_level.should == 101 + end end - it 'allows for filtering mid-association-chain' do - othmar.lessons_teaching.where(level: 201).students.to_a.should == [sandra] - end + describe 'association chaining' do + context 'othmar is teaching math 101' do + before(:each) { othmar.lessons_teaching << math101 } + + context 'bobby is taking math 101, sandra is taking soc 101' do + before(:each) { bobby.lessons << math101 } + before(:each) { sandra.lessons << ss101 } + + it { othmar.lessons_teaching.students.to_a.should == [bobby] } + + context 'bobby likes to read, sandra likes math' do + before(:each) { bobby.interests << reading } + before(:each) { sandra.interests << math } + + # Simple association chaining on three levels + it { othmar.lessons_teaching.students.interests.to_a.should == [reading] } + end + end - it 'allows for returning nodes mis-association-chain' do - othmar.lessons_teaching(:lesson).students.where(age: 16).pluck(:lesson).should == [math201] + context 'danny is also taking math 101' do + before(:each) { danny.lessons << math101 } - othmar.lessons_teaching(:lesson).students(:student).where(age: 16).pluck(:lesson, :student).should == [[math201, sandra]] + # Filtering on last association + it { othmar.lessons_teaching.students.where(age: 15).to_a.should == [danny] } + + # Mid-association variable assignment when filtering later + it { othmar.lessons_teaching(:lesson).students.where(age: 15).pluck(:lesson).should == [math101] } + + # Two variable assignments + it { othmar.lessons_teaching(:lesson).students(:student).where(age: 15).pluck(:lesson, :student).should == [[math101, danny]] } + end + end + + context 'othmar is also teaching math 201, brian is taking it' do + before(:each) { othmar.lessons_teaching << math201 } + before(:each) { brian.lessons << math201 } + + # Mid-association filtering + it { othmar.lessons_teaching.where(level: 201).students.to_a.should == [brian] } + end end - it 'allows association with properties' do - monster_trucks.interested.to_set.should == [samuels, othmar].to_set + context 'othmar likes moster trucks more than samuels' do + before(:each) do + samuels.interests.associate(monster_trucks, intensity: 1) + othmar.interests.associate(monster_trucks, intensity: 11) + end + + # Should get both + it { monster_trucks.interested.to_set.should == [samuels, othmar].to_set } - monster_trucks.interested(:person, :r).where('r.intensity < 5').pluck(:person).to_set.should == [samuels].to_set + # Variable assignment and filtering on a relationship + it { monster_trucks.interested(:person, :r).where('r.intensity < 5').pluck(:person).should == [samuels] } end end end diff --git a/spec/unit/association.rb b/spec/unit/association.rb new file mode 100644 index 000000000..ee6ccdcea --- /dev/null +++ b/spec/unit/association.rb @@ -0,0 +1,90 @@ +require 'spec_helper' + +class Default +end + +describe Neo4j::ActiveNode::HasN::Association do + let(:options) { {} } + let(:name) { :default } + let(:direction) { :out } + + let(:association) { Neo4j::ActiveNode::HasN::Association.new(type, direction, name, options) } + subject { association } + + context 'type = :invalid' do + let(:type) { :invalid } + + it { expect { subject }.to raise_error(ArgumentError) } + end + + context 'has_many' do + let(:type) { :has_many } + + context 'direction = :invalid' do + let(:direction) { :invalid } + + it { expect { subject }.to raise_error(ArgumentError) } + end + + describe '#arrow_cypher' do + let(:var) { nil } + let(:properties) { {} } + let(:create) { false } + + subject { association.arrow_cypher(var, properties, create) } + + it { should == '-[]->' } + + context 'inbound' do + let(:direction) { :in } + + it { should == '<-[]-' } + end + + context 'bidirectional' do + let(:direction) { :both } + + it { should == '-[]-' } + end + + context 'creation' do + let(:create) { true } + + it { should == '-[:`#default`]->' } + + context 'properties given' do + let(:properties) { {foo: 1, bar: 'test'} } + + it { should == '-[:`#default` {foo: 1, bar: "test"}]->' } + end + end + + context 'varable given' do + let(:var) { :fooy } + + it { should == '-[fooy]->' } + + context 'properties given' do + let(:properties) { {foo: 1, bar: 'test'} } + + it { should == '-[fooy {foo: 1, bar: "test"}]->' } + end + + context 'creation' do + let(:create) { true } + + it { should == '-[fooy:`#default`]->' } + + context 'properties given' do + let(:properties) { {foo: 1, bar: 'test'} } + + it { should == '-[fooy:`#default` {foo: 1, bar: "test"}]->' } + end + + end + end + + end + + end +end From ae3b4c9cbd769068eb6ea4ab68c0bd889fa994a4 Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Sun, 27 Jul 2014 20:38:48 -0700 Subject: [PATCH 29/54] Specs to cover association `origin` validation --- spec/e2e/query_spec.rb | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/spec/e2e/query_spec.rb b/spec/e2e/query_spec.rb index d0fdd21a4..2295fe3e3 100644 --- a/spec/e2e/query_spec.rb +++ b/spec/e2e/query_spec.rb @@ -55,6 +55,34 @@ class Teacher describe 'Query API' do before(:each) { delete_db } + + describe 'association validation' do + before(:each) do + %w{Foo Bar}.each do |const| + stub_const const, Class.new { include Neo4j::ActiveNode } + end + end + + context 'Foo has an association to Bar' do + before(:each) do + Foo.has_many :in, :bars, model_class: Bar + end + + it { expect { Bar.has_many :out, :foos, origin: :bars }.not_to raise_error } + it { expect { Bar.has_many :both, :foos, origin: :bars }.not_to raise_error } + + # No such model Foosr + it { expect { Bar.has_many :out, :foosrs, origin: :bars }.to raise_error(ArgumentError) } + + # Specifed origin not found + it { expect { Bar.has_many :out, :foos, origin: :barsy }.to raise_error(ArgumentError) } + + # Should raise error when direction is the same + it { expect { Bar.has_many :in, :foos, origin: :bars }.to raise_error(ArgumentError) } + + end + end + describe 'queries directly on a model class' do let!(:samuels) { Teacher.create(name: 'Harold Samuels') } let!(:othmar) { Teacher.create(name: 'Ms. Othmar') } @@ -74,6 +102,7 @@ class Teacher let!(:math) { Interest.create(name: 'Math') } let!(:monster_trucks) { Interest.create(name: 'Monster Trucks') } + it 'returns all' do result = Teacher.to_a From be22945659303377bc02a39e550b563788ed8649 Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Mon, 28 Jul 2014 23:42:06 -0700 Subject: [PATCH 30/54] Fell into the hole of moving all has_n / has_one to new has_many / has_one. Not done, but getting there. --- lib/neo4j/active_node/has_n.rb | 77 +++++++++++----------- lib/neo4j/active_node/has_n/association.rb | 45 +++++++------ lib/neo4j/active_node/persistence.rb | 8 +-- lib/neo4j/active_node/property.rb | 11 ++-- lib/neo4j/active_node/query/query_proxy.rb | 19 ++---- spec/e2e/has_one_spec.rb | 10 +-- spec/e2e/query_spec.rb | 36 +++++++--- spec/integration/label_spec.rb | 2 +- spec/integration/node_persistence_spec.rb | 2 +- spec/integration/orm_adapter/neo4j_spec.rb | 4 +- 10 files changed, 117 insertions(+), 97 deletions(-) diff --git a/lib/neo4j/active_node/has_n.rb b/lib/neo4j/active_node/has_n.rb index f3206838d..9ae7783fe 100644 --- a/lib/neo4j/active_node/has_n.rb +++ b/lib/neo4j/active_node/has_n.rb @@ -10,7 +10,11 @@ def _decl_rels_for(rel_type) module ClassMethods def has_association?(name) - !!@associations[name] + !!associations[name] + end + + def associations + @associations || {} end def has_relationship?(rel_type) @@ -120,11 +124,20 @@ def has_many(direction, name, options = {}) @associations ||= {} @associations[name] = association - target_class_name = association.target_class ? association.target_class.name : 'nil' + target_class_name = association.target_class_name || 'nil' + # TODO: Make assignment more efficient? (don't delete nodes when they are being assigned) module_eval(%Q{ def #{name}(node = nil, rel = nil) Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class_name}, self.class.associations[#{name.inspect}], session: self.class.neo4j_session, start_object: self, node: node, rel: rel) + end + + def #{name}=(other_nodes) + #{name}.query_as(:n).delete(:n).exec + + other_nodes.each do |node| + #{name} << node + end end}, __FILE__, __LINE__) instance_eval(%Q{ @@ -134,51 +147,35 @@ def #{name}(node = nil, rel = nil) end - attr_reader :associations + def has_one(direction, name, options = {}) + name = name.to_sym - # Specifies a relationship between two node classes. - # Generates assignment and accessor methods for the given relationship - # Old relationship is deleted when a new relationship is assigned. - # Both incoming and outgoing relationships can be declared, see {Neo4j::Wrapper::HasN::DeclRel} - # - # @example - # - # class FileNode - # include Neo4j::ActiveNode - # has_one(:folder) - # end - # - # file = FileNode.create - # file.folder = Neo4j::Node.create - # file.folder # => the node above - # file.folder_rel # => the relationship object between those nodes - # - # @return [Neo4j::ActiveNode::HasN::DeclRel] a DSL object where the has_one relationship can be futher specified - def has_one(rel_type) - clazz = self - module_eval(%Q{def #{rel_type}=(value) - dsl = _decl_rels_for(:#{rel_type}) - rel = dsl.single_relationship(self) - rel && rel.del - dsl.create_relationship_to(self, value) if value - end}, __FILE__, __LINE__) + association = Neo4j::ActiveNode::HasN::Association.new(:has_one, direction, name, options) + name = name.to_sym - module_eval(%Q{def #{rel_type} - dsl = _decl_rels_for('#{rel_type}'.to_sym) - dsl.single_node(self) - end}, __FILE__, __LINE__) + @associations ||= {} + @associations[name] = association + + target_class_name = association.target_class_name || 'nil' + + module_eval(%Q{ + def #{name}=(other_node) + #{name}_query_proxy << other_node + end - module_eval(%Q{def #{rel_type}_rel - dsl = _decl_rels_for(:#{rel_type}) - dsl.single_relationship(self) - end}, __FILE__, __LINE__) + def #{name}_query_proxy(node = nil, rel = nil) + Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class_name}, self.class.associations[#{name.inspect}], session: self.class.neo4j_session, start_object: self, node: node, rel: rel) + end + + def #{name}(node = nil, rel = nil) + #{name}_query_proxy(node, rel).first + end}, __FILE__, __LINE__) instance_eval(%Q{ - def #{rel_type} - _decl_rels[:#{rel_type}].rel_type + def #{name}(node = nil, rel = nil) + Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class_name}, @associations[#{name.inspect}], session: self.neo4j_session, query_proxy: self.query_proxy, node: node, rel: rel).first end}, __FILE__, __LINE__) - _decl_rels[rel_type.to_sym] = DeclRel.new(rel_type, true, clazz) end diff --git a/lib/neo4j/active_node/has_n/association.rb b/lib/neo4j/active_node/has_n/association.rb index 72b5db10e..9102120ac 100644 --- a/lib/neo4j/active_node/has_n/association.rb +++ b/lib/neo4j/active_node/has_n/association.rb @@ -4,7 +4,7 @@ module Neo4j module ActiveNode module HasN class Association - attr_reader :type, :name, :target_class, :relationship, :direction + attr_reader :type, :name, :relationship, :direction def initialize(type, direction, name, options = {}) raise ArgumentError, "Invalid association type: #{type.inspect}" if not [:has_many, :has_one].include?(type.to_sym) @@ -14,29 +14,25 @@ def initialize(type, direction, name, options = {}) @name = name @direction = direction.to_sym @target_class_name_from_name = name.to_s.classify - @target_class = begin - if options[:model_class].nil? - @target_class_name_from_name.constantize - elsif options[:model_class] - options[:model_class] - end - rescue NameError - raise ArgumentError, "Could not find `#{@target_class_name_from_name}` class and no :model_class specified" - end + @target_class_option = if options[:model_class].nil? + @target_class_name_from_name + elsif options[:model_class] + options[:model_class] + end if options[:type] && options[:origin] raise ArgumentError, "Cannot specify both :type and :origin (#{self.base_declaration})" else @relationship_type = options[:type] && options[:type].to_sym @origin = options[:origin] && options[:origin].to_sym - - validate_origin! end end # Return cypher partial query string for the relationship part of a MATCH (arrow / relationship definition) def arrow_cypher(var = nil, properties = {}, create = false) - relationship_type = self.relationship_type(create) + validate_origin! + + relationship_type = relationship_type(create) relationship_name_cypher = ":`#{relationship_type}`" if relationship_type properties_string = properties.map do |key, value| @@ -61,6 +57,18 @@ def arrow_cypher(var = nil, properties = {}, create = false) end end + def target_class_name + @target_class_option + end + + def target_class + @target_class ||= @target_class_option.constantize if @target_class + rescue NameError + raise ArgumentError, "Could not find `#{@target_class}` class and no :model_class specified" + end + + private + def relationship_type(create = false) if @relationship_type @relationship_type @@ -78,26 +86,25 @@ def base_declaration "#{type} #{direction.inspect}, #{name.inspect}" end - private - + # Determine if model class as derived from the association name would be different than the one specified via the model_class key # @example # has_many :friends # Would return false # has_many :friends, model_class: Friend # Would return false # has_many :friends, model_class: Person # Would return true def exceptional_target_class? - @target_class && @target_class.name != @target_class_name_from_name + target_class && target_class.name != @target_class_name_from_name end def validate_origin! if @origin - if @target_class - if association = @target_class.associations[@origin] + if target_class + if association = target_class.associations[@origin] if @direction == association.direction raise ArgumentError, "Origin `#{@origin.inspect}` (specified in #{self.base_declaration}) has same direction `#{@direction}`)" end else - raise ArgumentError, "Origin `#{@origin.inspect}` association not found for #{@target_class} (specified in #{self.base_declaration})" + raise ArgumentError, "Origin `#{@origin.inspect}` association not found for #{target_class} (specified in #{self.base_declaration})" end else raise ArgumentError, "Cannot use :origin without a model_class (implied or explicit)" diff --git a/lib/neo4j/active_node/persistence.rb b/lib/neo4j/active_node/persistence.rb index 712671c9d..3a6cff5a8 100644 --- a/lib/neo4j/active_node/persistence.rb +++ b/lib/neo4j/active_node/persistence.rb @@ -197,11 +197,11 @@ module ClassMethods # Creates a saves a new node # @param [Hash] props the properties the new node should have def create(props = {}) - relationship_props = extract_relationship_attributes!(props) + association_props = extract_association_attributes!(props) new(props).tap do |obj| obj.save - relationship_props.each do |prop, value| + association_props.each do |prop, value| obj.send("#{prop}=", value) end end @@ -210,12 +210,12 @@ def create(props = {}) # Same as #create, but raises an error if there is a problem during save. def create!(*args) props = args[0] || {} - relationship_props = extract_relationship_attributes!(props) + association_props = extract_association_attributes!(props) new(*args).tap do |o| yield o if block_given? o.save! - relationship_props.each do |prop, value| + association_props.each do |prop, value| o.send("#{prop}=", value) end end diff --git a/lib/neo4j/active_node/property.rb b/lib/neo4j/active_node/property.rb index 63f9c6ded..de4293c37 100644 --- a/lib/neo4j/active_node/property.rb +++ b/lib/neo4j/active_node/property.rb @@ -13,7 +13,8 @@ class UndefinedPropertyError < RuntimeError end def initialize(attributes={}, options={}) - relationship_props = self.class.extract_relationship_attributes!(attributes) + self.class.extract_association_attributes!(attributes) + writer_method_props = extract_writer_methods!(attributes) validate_attributes!(attributes) writer_method_props.each do |key, value| @@ -77,11 +78,11 @@ def attribute!(name, options={}) # 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_relationship_attributes!(attributes) - attributes.keys.inject({}) do |relationship_props, key| - relationship_props[key] = attributes.delete(key) if self.has_relationship?(key) + def extract_association_attributes!(attributes) + attributes.keys.inject({}) do |association_props, key| + association_props[key] = attributes.delete(key) if self.has_association?(key) - relationship_props + association_props end end diff --git a/lib/neo4j/active_node/query/query_proxy.rb b/lib/neo4j/active_node/query/query_proxy.rb index b811ad8a7..5044a9eac 100644 --- a/lib/neo4j/active_node/query/query_proxy.rb +++ b/lib/neo4j/active_node/query/query_proxy.rb @@ -17,11 +17,15 @@ def initialize(model, association = nil, options = {}) end def each - query.pluck(@node_var || :result).each do |obj| + self.pluck(@node_var || :result).each do |obj| yield obj end end + def ==(value) + self.to_a == value + end + METHODS = %w[where order skip limit] METHODS.each do |method| @@ -75,16 +79,7 @@ def to_cypher # To add a relationship for the node for the association on this QueryProxy def <<(other_node) - if @association - raise ArgumentError, "Node must be of the association's class when model is specified" if @model && other_node.class != @model - - _association_query_start(:start) - .match(end: other_node.class) - .where(end: {neo_id: other_node.neo_id}) - .create("start#{_association_arrow({}, true)}end").exec - else - raise "Can only create associations on associations" - end + associate(other_node, {}) end def associate(other_node, properties) @@ -155,7 +150,7 @@ def _chain_level def _association_chain_var if start_object = @options[:start_object] - :"#{start_object.class.name.downcase}#{start_object.neo_id}" + :"#{start_object.class.name.gsub('::', '_').downcase}#{start_object.neo_id}" elsif query_proxy = @options[:query_proxy] query_proxy.node_var || :"node#{_chain_level}" else diff --git a/spec/e2e/has_one_spec.rb b/spec/e2e/has_one_spec.rb index 3654026c4..44a46f121 100644 --- a/spec/e2e/has_one_spec.rb +++ b/spec/e2e/has_one_spec.rb @@ -6,13 +6,13 @@ class HasOneA include Neo4j::ActiveNode property :name - has_n :children + has_many :out, :children, model_class: false end class HasOneB include Neo4j::ActiveNode property :name - has_one(:parent).from(:children) + has_one :in, :parent, type: :children, model_class: false end it 'find the nodes via the has_one accessor' do @@ -76,8 +76,8 @@ class File1 include Neo4j::ActiveNode end - Folder1.has_n(:files).to(File1) - File1.has_one(:parent).from(Folder1.files) + Folder1.has_many :out, :files, model_class: File1 + File1.has_one :in, :parent, model_class: Folder1, origin: :files it 'can access nodes via parent has_one relationship' do f1 = Folder1.create @@ -90,4 +90,4 @@ class File1 end end -end \ No newline at end of file +end diff --git a/spec/e2e/query_spec.rb b/spec/e2e/query_spec.rb index 2295fe3e3..7c8dc15a0 100644 --- a/spec/e2e/query_spec.rb +++ b/spec/e2e/query_spec.rb @@ -68,17 +68,37 @@ class Teacher Foo.has_many :in, :bars, model_class: Bar end - it { expect { Bar.has_many :out, :foos, origin: :bars }.not_to raise_error } - it { expect { Bar.has_many :both, :foos, origin: :bars }.not_to raise_error } + subject { Bar.new } - # No such model Foosr - it { expect { Bar.has_many :out, :foosrs, origin: :bars }.to raise_error(ArgumentError) } + context 'other class is opposite direction' do + before(:each) { Bar.has_many :out, :foos, origin: :bars } - # Specifed origin not found - it { expect { Bar.has_many :out, :foos, origin: :barsy }.to raise_error(ArgumentError) } + it { expect { subject.foos }.not_to raise_error } + end + + context 'other class is both' do + before(:each) { Bar.has_many :both, :foos, origin: :bar } + + it { expect { subject.foos }.not_to raise_error } + end + + context 'Assumed model does not exist' do + before(:each) { Bar.has_many :out, :foosrs, origin: :bars } + + it { expect { subject.foosrs }.to raise_error(NameError) } + end + + context 'Origin does not exist' do + before(:each) { Bar.has_many :out, :foos, origin: :barsy } + + it { expect { subject.foos.to_a }.to raise_error(ArgumentError) } + end + + context 'Direction is the same' do + before(:each) { Bar.has_many :in, :foos, origin: :bars } - # Should raise error when direction is the same - it { expect { Bar.has_many :in, :foos, origin: :bars }.to raise_error(ArgumentError) } + it { expect { subject.foos.to_a }.to raise_error(ArgumentError) } + end end end diff --git a/spec/integration/label_spec.rb b/spec/integration/label_spec.rb index 180d0bae6..b3b89233a 100644 --- a/spec/integration/label_spec.rb +++ b/spec/integration/label_spec.rb @@ -42,7 +42,7 @@ class SomeLabelClass class RelationTestClass include Neo4j::ActiveNode - has_one(:test_class) + has_one :in, :test_class end end diff --git a/spec/integration/node_persistence_spec.rb b/spec/integration/node_persistence_spec.rb index 1d3d8cf79..21d4099c9 100644 --- a/spec/integration/node_persistence_spec.rb +++ b/spec/integration/node_persistence_spec.rb @@ -6,7 +6,7 @@ class MyThing include Neo4j::ActiveNode property :a property :x - has_one :parent + has_one :out, :parent, model_class: false end diff --git a/spec/integration/orm_adapter/neo4j_spec.rb b/spec/integration/orm_adapter/neo4j_spec.rb index 41924a24e..0bd303c14 100644 --- a/spec/integration/orm_adapter/neo4j_spec.rb +++ b/spec/integration/orm_adapter/neo4j_spec.rb @@ -13,7 +13,7 @@ class User index :name - has_n(:notes).to('Neo4j::OrmSpec::Note') + has_many :out, :notes, model_class: 'Neo4j::OrmSpec::Note' end class Note @@ -21,7 +21,7 @@ class Note property :body, :index => :exact - has_one(:owner).from('Neo4j::OrmSpec::User', :notes) + has_one :in, :owner, type: :notes, model_class: 'Neo4j::OrmSpec::User' end describe '[Neo4j orm adapter]', :type => :integration do From ea0e11a51d865bec605cf0baf84c62f409e5d32c Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Mon, 28 Jul 2014 23:42:21 -0700 Subject: [PATCH 31/54] Going to remove QuickQuery soon, but the specs cause extra errors --- spec/e2e/quick_query_spec.rb | 162 ----------------------------------- 1 file changed, 162 deletions(-) diff --git a/spec/e2e/quick_query_spec.rb b/spec/e2e/quick_query_spec.rb index ed8d1bead..e69de29bb 100644 --- a/spec/e2e/quick_query_spec.rb +++ b/spec/e2e/quick_query_spec.rb @@ -1,162 +0,0 @@ -require 'spec_helper' -class Student; end -class Teacher; end - -class Lesson - include Neo4j::ActiveNode - property :name - - has_n(:teachers).from(Teacher, :teaching_lessons) - has_n(:students).from(Student, :lessons) -end - -class Student - include Neo4j::ActiveNode - property :name - property :age, type: Integer - property :occupation - - has_n(:lessons).to(Lesson) -end - -class Teacher - include Neo4j::ActiveNode - property :name - - has_n(:teaching_lessons).to(Teacher) -end - -describe 'QuickQuery Queries' do - describe "#qq on class" do - it 'creates a new instance of QuickQuery' do - expect(Student.qq).to be_a(Neo4j::ActiveNode::QuickQuery) - end - - it 'accepts a symbol to identify the first node' do - expect(Student.qq(:foo).instance_variable_get(:@return_obj)).to eq :foo - end - end - - describe "#qq on instance" do - let(:chris) { Student.create(name: 'jim', age: '30') } - - it 'creates a new instance of QuickQuery' do - expect(chris.qq).to be_a(Neo4j::ActiveNode::QuickQuery) - end - - it 'sets the starting ID to the node ID' do - expect(chris.qq.to_cypher).to include "= #{chris.id}" - end - end - - describe "#where" do - after(:all) { Student.all.each{|s| s.destroy } } - let!(:chris) { Student.create(name: 'chris', age: 30, occupation: '') } - let!(:lauren) { Student.create(name: 'lauren', age: 30, occupation: '') } - let!(:jasmine) { Student.create(name: 'jasmine', age: 5, occupation: 'cat')} - - it 'sets match parameters based on node_on_deck' do - expect(Student.qq.where(name: 'chris').to_cypher).to include "WHERE n1.name = \"chris\"" - end - - it 'allows explicit setting of an identifier' do - expect(Student.qq.where(:foo, name: 'chris').to_cypher).to include "WHERE foo.name = \"chris\"" - end - - it 'always increments the node identifier' do - expect(Student.qq(:foo).lessons.instance_variable_get(:@node_on_deck)).to eq 'n2' - end - - it 'passes strings directly to core query' do - @end = Student.qq.where('age > 29').to_a - expect(@end).to include chris, lauren - expect(@end).to_not include jasmine - end - - it 'recognizes an identifier in a string' do - expect(Student.qq(:s).where('s.age > 29').to_a).to include chris, lauren - end - - it 'adds identifiers to strings when missing' do - expect(Student.qq.where('age > 29').to_a).to include chris, lauren - end - end - - describe "#set" do - after(:all) { Student.all.each{|s| s.destroy } } - - let!(:chris) { Student.create(name: 'chris', age: 30, occupation: '') } - let!(:lauren) { Student.create(name: 'lauren', age: 30, occupation: '') } - let!(:jasmine) { Student.create(name: 'jasmine', age: 5, occupation: 'cat')} - - it "updates the specified parameter" do - Student.qq.set_props(occupation: 'adult').where(age: 30).return - @nc = Student.qq.to_a.first - expect(@nc.occupation).to eq 'adult' - end - - it "leaves other parameters alone" do - Student.qq.set_props(occupation: 'adult').where(age: 30).return - expect(chris.age).to eq 30 - end - - it "updates all matching objects" do - Student.qq.set_props(occupation: 'adult').where(age: 30).return - expect([chris, lauren].all?{|el| el.age == 30}).to be_truthy - end - end - - describe "dynamic rel method creation" do - it 'creates methods based on the calling model' do - expect(Student.qq.respond_to?(:lessons)).to be_truthy - end - - it 'creates new methods with each traversal to a new model' do - expect(Student.qq.lessons.respond_to?(:teachers)).to be_truthy - end - - it 'automatically sets a rel identifier' do - expect(Student.qq.lessons.instance_variable_get(:@rel_on_deck)).to eq 'r1' - end - - it 'increments the rel identifier' do - expect(Student.qq.lessons.teachers.instance_variable_get(:@rel_on_deck)).to eq 'r2' - end - - it 'allows explicit setting of rel identifier' do - expect(Student.qq.lessons(rel_as: :foo).instance_variable_get(:@rel_on_deck)).to eq :foo - end - - it 'always increments the rel identifier' do - #we want to be sure that it increments, even if an earlier one is explicitly set - expect(Student.qq.lessons(:foo).teachers.instance_variable_get(:@rel_on_deck)).to eq 'r2' - end - end - - describe "return" do - after(:all) { Student.all.each{|s| s.destroy } } - - let!(:chris) { Student.create(name: 'chris', age: 30) } - let!(:history) { Lesson.create(name: 'history 101') } - let!(:math) { Lesson.create(name: 'math 101') } - before do - chris.lessons << history - chris.lessons << math - chris.save - 4.times { Student.create(age: 30) } - 6.times { Student.create(age: 31) } - end - - it "returns an enum of the object requested" do - expect(Student.qq.where(age: 31).return.to_a.count).to eq 6 - end - - it "returns the object requested" do - expect(chris.qq.lessons.return(:n2).to_a.first).to be_a(Lesson) - end - - it "uses implicit return" do - expect(Student.qq.where(age: 30).to_a).to_not be nil - end - end -end \ No newline at end of file From 222170efcc5ae62303874b32c5819674eb5d9a9c Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Tue, 29 Jul 2014 23:18:47 -0700 Subject: [PATCH 32/54] Almost have specs to passing --- lib/neo4j/active_node/has_n.rb | 20 +++++++++++++++----- lib/neo4j/active_node/has_n/association.rb | 10 ++++++---- lib/neo4j/active_node/query/query_proxy.rb | 2 +- spec/e2e/has_one_spec.rb | 17 ++++++++++------- spec/e2e/queries_spec.rb | 8 ++++---- spec/e2e/query_spec.rb | 4 ++-- 6 files changed, 38 insertions(+), 23 deletions(-) diff --git a/lib/neo4j/active_node/has_n.rb b/lib/neo4j/active_node/has_n.rb index 9ae7783fe..acfb3d8fa 100644 --- a/lib/neo4j/active_node/has_n.rb +++ b/lib/neo4j/active_node/has_n.rb @@ -133,7 +133,7 @@ def #{name}(node = nil, rel = nil) end def #{name}=(other_nodes) - #{name}.query_as(:n).delete(:n).exec + #{name}(nil, :r).query_as(:n).delete(:r).exec other_nodes.each do |node| #{name} << node @@ -160,20 +160,30 @@ def has_one(direction, name, options = {}) module_eval(%Q{ def #{name}=(other_node) + #{name}_query_proxy(rel: :r).query_as(:n).delete(:r).exec + #{name}_query_proxy << other_node end - def #{name}_query_proxy(node = nil, rel = nil) - Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class_name}, self.class.associations[#{name.inspect}], session: self.class.neo4j_session, start_object: self, node: node, rel: rel) + def #{name}_query_proxy(options = {}) + self.class.#{name}_query_proxy({start_object: self}.merge(options)) + end + + def #{name}_rel + #{name}_query_proxy(rel: :r).pluck(:r).first end def #{name}(node = nil, rel = nil) - #{name}_query_proxy(node, rel).first + #{name}_query_proxy(node: node, rel: rel).first end}, __FILE__, __LINE__) instance_eval(%Q{ + def #{name}_query_proxy(options = {}) + Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class_name}, @associations[#{name.inspect}], {session: self.neo4j_session}.merge(options)) + end + def #{name}(node = nil, rel = nil) - Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class_name}, @associations[#{name.inspect}], session: self.neo4j_session, query_proxy: self.query_proxy, node: node, rel: rel).first + #{name}_query_proxy(query_proxy: self.query_proxy, node: node, rel: rel) end}, __FILE__, __LINE__) end diff --git a/lib/neo4j/active_node/has_n/association.rb b/lib/neo4j/active_node/has_n/association.rb index 9102120ac..ec95c6b45 100644 --- a/lib/neo4j/active_node/has_n/association.rb +++ b/lib/neo4j/active_node/has_n/association.rb @@ -58,11 +58,13 @@ def arrow_cypher(var = nil, properties = {}, create = false) end def target_class_name - @target_class_option + @target_class_option.to_s if @target_class_option end def target_class - @target_class ||= @target_class_option.constantize if @target_class + return @target_class if @target_class + + @target_class = target_class_name.constantize if target_class_name rescue NameError raise ArgumentError, "Could not find `#{@target_class}` class and no :model_class specified" end @@ -101,10 +103,10 @@ def validate_origin! if target_class if association = target_class.associations[@origin] if @direction == association.direction - raise ArgumentError, "Origin `#{@origin.inspect}` (specified in #{self.base_declaration}) has same direction `#{@direction}`)" + raise ArgumentError, "Origin `#{@origin.inspect}` (specified in #{base_declaration}) has same direction `#{@direction}`)" end else - raise ArgumentError, "Origin `#{@origin.inspect}` association not found for #{target_class} (specified in #{self.base_declaration})" + raise ArgumentError, "Origin `#{@origin.inspect}` association not found for #{target_class} (specified in #{base_declaration})" end else raise ArgumentError, "Cannot use :origin without a model_class (implied or explicit)" diff --git a/lib/neo4j/active_node/query/query_proxy.rb b/lib/neo4j/active_node/query/query_proxy.rb index 5044a9eac..eb6ffb97d 100644 --- a/lib/neo4j/active_node/query/query_proxy.rb +++ b/lib/neo4j/active_node/query/query_proxy.rb @@ -144,7 +144,7 @@ def _chain_level elsif query_proxy = @options[:query_proxy] query_proxy._chain_level + 1 else - raise "Crazy error" # TODO: Better error + 1 end end diff --git a/spec/e2e/has_one_spec.rb b/spec/e2e/has_one_spec.rb index 44a46f121..b1b1e7d9c 100644 --- a/spec/e2e/has_one_spec.rb +++ b/spec/e2e/has_one_spec.rb @@ -6,13 +6,13 @@ class HasOneA include Neo4j::ActiveNode property :name - has_many :out, :children, model_class: false + has_many :out, :children, model_class: 'HasOneB' end class HasOneB include Neo4j::ActiveNode property :name - has_one :in, :parent, type: :children, model_class: false + has_one :in, :parent, origin: :children, model_class: 'HasOneA' end it 'find the nodes via the has_one accessor' do @@ -24,7 +24,7 @@ class HasOneB c.parent.should == a b.parent.should == a - a.children.should =~ [b,c] + a.children.to_a.should =~ [b,c] end it 'can create a relationship via the has_one accessor' do @@ -62,8 +62,10 @@ class HasOneB a = HasOneA.create(name: 'a') b = HasOneB.create(name: 'b') b.parent = a - b.nodes(dir: :incoming, type: HasOneB.parent).to_a.should == [a] - a.nodes(dir: :outgoing, type: HasOneB.parent).to_a.should == [b] + b.query_as(:b).match("b<-[:`#children`]-(r)").pluck(:r).should == [a] + a.query_as(:a).match("a-[:`#children`]->(r)").pluck(:r).should == [b] +# b.nodes(dir: :incoming, type: HasOneB.parent).to_a.should == [a] +# a.nodes(dir: :outgoing, type: HasOneB.parent).to_a.should == [b] end end @@ -83,8 +85,9 @@ class File1 f1 = Folder1.create file1 = File1.create file2 = File1.create - f1.files << file1 << file2 - f1.files.should =~ [file1, file2] + f1.files << file1 + f1.files << file2 + f1.files.to_a.should =~ [file1, file2] file1.parent.should == f1 file2.parent.should == f1 end diff --git a/spec/e2e/queries_spec.rb b/spec/e2e/queries_spec.rb index 374a1eaf2..23800ba3b 100644 --- a/spec/e2e/queries_spec.rb +++ b/spec/e2e/queries_spec.rb @@ -26,14 +26,14 @@ def create_clazz include Neo4j::ActiveNode property :name property :score, type: Integer - has_one :knows + yield self end end before(:all) do - @clazz_a = create_clazz - @clazz_b = create_clazz + @clazz_a = create_clazz {|c| c.has_one :out, :knows, model_class: false } + @clazz_b = create_clazz {|c| c.has_many :in, :known_by, model_class: false } @b2 = @clazz_b.create(name: 'b2', score: '2') @b1 = @clazz_b.create(name: 'b1', score: '1') @@ -57,7 +57,7 @@ def create_clazz end it 'can find all nodes having a relationship to another node' do - expect(@clazz_a.where(knows: @b2).to_a).to match_array([@a3, @a2]) + expect(@b2.known_by.to_a).to match_array([@a3, @a2]) end it 'can not find all nodes having a relationship to another node if there are non' do diff --git a/spec/e2e/query_spec.rb b/spec/e2e/query_spec.rb index 7c8dc15a0..5abe30248 100644 --- a/spec/e2e/query_spec.rb +++ b/spec/e2e/query_spec.rb @@ -163,8 +163,8 @@ class Teacher end it 'differentiates associations on the same model for the same class' do - bobby.favorite_teachers.to_set.should == [samuels].to_set - bobby.hated_teachers.to_set.should == [othmar].to_set + bobby.favorite_teachers.to_a.should == [samuels] + bobby.hated_teachers.to_a.should == [othmar] end end From 43b218659bc38a0e34e68b800741bfc32fb933dd Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Thu, 31 Jul 2014 17:11:30 -0700 Subject: [PATCH 33/54] Use new API to query for relationship --- spec/e2e/queries_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/e2e/queries_spec.rb b/spec/e2e/queries_spec.rb index 23800ba3b..2fd12fe51 100644 --- a/spec/e2e/queries_spec.rb +++ b/spec/e2e/queries_spec.rb @@ -61,7 +61,7 @@ def create_clazz end it 'can not find all nodes having a relationship to another node if there are non' do - expect(@clazz_b.where(knows: @a1).to_a).to eq([]) + expect(@clazz_b.query_as(:b).match('b<-[:knows]-(r)').pluck(:r)).to eq([]) end end From 21e2b15f55d5528d42206fa35abd52565c7691bc Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Fri, 1 Aug 2014 08:40:01 -0700 Subject: [PATCH 34/54] Not ideal, but gets the spec passing --- spec/integration/node_persistence_spec.rb | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/spec/integration/node_persistence_spec.rb b/spec/integration/node_persistence_spec.rb index 21d4099c9..741afe7b6 100644 --- a/spec/integration/node_persistence_spec.rb +++ b/spec/integration/node_persistence_spec.rb @@ -48,21 +48,26 @@ class MyThing end it 'can create relationships' do - parent = double("parent node") - node = double('unwrapped_node', props: {a: 999}, rel: nil) - expect(node).to receive(:create_rel).with(:parent, parent, {}) + parent = double("parent node", neo_id: 1) + node = double('unwrapped_node', props: {a: 999}, rel: nil, neo_id: 2) + @session.should_receive(:create_node).with({a: 1}, [:MyThing]).and_return(node) + @session.should_receive(:query).exactly(3).times.and_return(Neo4j::Core::Query.new) + @session.should_receive(:_query).exactly(2).times thing = MyThing.create(a: 1, parent: parent) thing.props.should == {a: 999} end it 'will delete old relationship before creating a new one' do - parent = double("parent node") + parent = double("parent node", neo_id: 1) old_rel = double("old relationship") - expect(old_rel).to receive(:del) - node = double('unwrapped_node', props: {a: 999}, rel: old_rel) - expect(node).to receive(:create_rel).with(:parent, parent, {}) + + node = double('unwrapped_node', props: {a: 999}, rel: old_rel, neo_id: 2) + @session.should_receive(:create_node).with({a: 1}, [:MyThing]).and_return(node) + @session.should_receive(:query).exactly(3).times.and_return(Neo4j::Core::Query.new) + @session.should_receive(:_query).exactly(2).times + thing = MyThing.create(a: 1, parent: parent) thing.props.should == {a: 999} end @@ -169,3 +174,5 @@ class MyThing end + + From f93449f0b46a24c513d16e06314d10906c65e7c9 Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Fri, 1 Aug 2014 20:49:36 -0700 Subject: [PATCH 35/54] Improving specs a bit --- spec/e2e/query_spec.rb | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/spec/e2e/query_spec.rb b/spec/e2e/query_spec.rb index 5abe30248..31ffe9492 100644 --- a/spec/e2e/query_spec.rb +++ b/spec/e2e/query_spec.rb @@ -68,24 +68,30 @@ class Teacher Foo.has_many :in, :bars, model_class: Bar end - subject { Bar.new } + subject { Bar.create } context 'other class is opposite direction' do before(:each) { Bar.has_many :out, :foos, origin: :bars } - it { expect { subject.foos }.not_to raise_error } + it { expect { subject.foos.to_a }.not_to raise_error } end context 'other class is both' do - before(:each) { Bar.has_many :both, :foos, origin: :bar } + before(:each) { Bar.has_many :both, :foos, origin: :bars } - it { expect { subject.foos }.not_to raise_error } + it { expect { subject.foos.to_a }.not_to raise_error } end context 'Assumed model does not exist' do before(:each) { Bar.has_many :out, :foosrs, origin: :bars } - it { expect { subject.foosrs }.to raise_error(NameError) } + it { expect { subject.foosrs.to_a }.to raise_error(NameError) } + end + + context 'Specified model does not exist' do + before(:each) { Bar.has_many :out, :foosrs, model_class: 'Foosrs', origin: :bars } + + it { expect { subject.foosrs.to_a }.to raise_error(NameError) } end context 'Origin does not exist' do From 9468601912846b1d46f06f024d8a450fd3815bb3 Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Fri, 1 Aug 2014 21:01:54 -0700 Subject: [PATCH 36/54] Can't call private method --- lib/neo4j/active_node/has_n/association.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/neo4j/active_node/has_n/association.rb b/lib/neo4j/active_node/has_n/association.rb index ec95c6b45..c42ddcab3 100644 --- a/lib/neo4j/active_node/has_n/association.rb +++ b/lib/neo4j/active_node/has_n/association.rb @@ -21,7 +21,7 @@ def initialize(type, direction, name, options = {}) end if options[:type] && options[:origin] - raise ArgumentError, "Cannot specify both :type and :origin (#{self.base_declaration})" + raise ArgumentError, "Cannot specify both :type and :origin (#{base_declaration})" else @relationship_type = options[:type] && options[:type].to_sym @origin = options[:origin] && options[:origin].to_sym From 73410953ba0190c2bff8784b548a66492d1b1406 Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Fri, 1 Aug 2014 21:02:12 -0700 Subject: [PATCH 37/54] Validation is already being done in initalize --- lib/neo4j/active_node/has_n/association.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/neo4j/active_node/has_n/association.rb b/lib/neo4j/active_node/has_n/association.rb index c42ddcab3..a389dae77 100644 --- a/lib/neo4j/active_node/has_n/association.rb +++ b/lib/neo4j/active_node/has_n/association.rb @@ -52,8 +52,6 @@ def arrow_cypher(var = nil, properties = {}, create = false) "<-#{relationship_cypher}-" when :both "-#{relationship_cypher}-" - else - raise ArgumentError, "Invalid relationship direction: #{direction.inspect}" end end From 4a3e9f8b236021f991112bc50a372a1297acd439 Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Fri, 1 Aug 2014 21:10:26 -0700 Subject: [PATCH 38/54] Improving coverage --- spec/unit/association.rb | 42 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/spec/unit/association.rb b/spec/unit/association.rb index ee6ccdcea..1efa504d7 100644 --- a/spec/unit/association.rb +++ b/spec/unit/association.rb @@ -20,12 +20,22 @@ class Default context 'has_many' do let(:type) { :has_many } + ### Validations + context 'direction = :invalid' do let(:direction) { :invalid } it { expect { subject }.to raise_error(ArgumentError) } end + context 'origin and type specified' do + let(:options) { {type: :bar, origin: :foo} } + + it { expect { subject }.to raise_error(ArgumentError) } + end + + + describe '#arrow_cypher' do let(:var) { nil } let(:properties) { {} } @@ -86,5 +96,37 @@ class Default end + describe "#target_class_name" do + subject { association.target_class_name } + + context "assumed model class" do + let(:name) { :burzs } + + it { should == 'Burz' } + end + + + context "specified model class" do + context "specified as string" do + let(:options) { {model_class: 'Bizzl'} } + + it { should == 'Bizzl' } + end + + context "specified as class" do + before(:each) do + stub_const 'Fizzl', Class.new { include Neo4j::ActiveNode } + end + + let(:options) { {model_class: 'Fizzl'} } + + it { should == 'Fizzl' } + end + end + + end + end + + end From cf6f07d12abdd88b7aa44416da9bb35839332285 Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Sun, 3 Aug 2014 14:28:44 -0700 Subject: [PATCH 39/54] Change spec name so that it's used when running all specs. Add a spec for coverage --- spec/unit/{association.rb => association_spec.rb} | 6 ++++++ 1 file changed, 6 insertions(+) rename spec/unit/{association.rb => association_spec.rb} (94%) diff --git a/spec/unit/association.rb b/spec/unit/association_spec.rb similarity index 94% rename from spec/unit/association.rb rename to spec/unit/association_spec.rb index 1efa504d7..79ac68a6b 100644 --- a/spec/unit/association.rb +++ b/spec/unit/association_spec.rb @@ -80,6 +80,12 @@ class Default it { should == '-[fooy {foo: 1, bar: "test"}]->' } end + context 'relationship type given' do + let(:options) { {type: :new_type} } + + it { should == '-[fooy:`new_type`]->' } + end + context 'creation' do let(:create) { true } From 887f63834a793b88127cb8ae84da46ad8322de1a Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Sun, 3 Aug 2014 21:13:48 -0700 Subject: [PATCH 40/54] Replacing has_n_spec with has_many declarations and fixing specs / code. --- lib/neo4j/active_node/has_n.rb | 78 ++-------------------- lib/neo4j/active_node/query/query_proxy.rb | 8 +++ spec/e2e/has_n_spec.rb | 44 +++++++----- 3 files changed, 38 insertions(+), 92 deletions(-) diff --git a/lib/neo4j/active_node/has_n.rb b/lib/neo4j/active_node/has_n.rb index acfb3d8fa..956b463da 100644 --- a/lib/neo4j/active_node/has_n.rb +++ b/lib/neo4j/active_node/has_n.rb @@ -41,80 +41,6 @@ def inherited(klass) super end - - # Specifies a relationship between two node active node classes. - # Generates assignment and accessor methods for the given relationship. - # Both incoming and outgoing relationships can be declared, see {Neo4j::ActiveNode::HasN::DeclRel} - # - # @example has_n(:files) - # - # class FolderNode - # include Neo4j::ActiveNode - # has_n(:files) - # end - # - # folder = FolderNode.new - # folder.files << Neo4j::Node.new << Neo4j::Node.new - # folder.files.inject {...} - # - # FolderNode.files #=> 'files' the name of the relationship - # - # @example has_n(x).to(...) - # - # # You can declare which class it has relationship to. - # # The generated relationships will be prefixed with the name of that class. - # class FolderNode - # include Neo4j::ActiveNode - # has_n(:files).to(File) - # # Same as has_n(:files).to("File") - # end - # - # FolderNode.files #=> 'File#files' the name of the relationship - # - # @example has_one(x).from(class, has_one_name) - # - # # generate accessor method for traversing and adding relationship on incoming nodes. - # class FileNode - # include Neo4j::ActiveNode - # has_one(:folder).from(FolderNode.files) - # # or same as - # has_one(:folder).from(FolderNode, :files) - # end - # - # - # @return [Neo4j::ActiveNode::HasN::DeclRel] a DSL object where the has_n relationship can be further specified - def has_n(rel_type) - clazz = self - module_eval(%Q{def #{rel_type}=(values) - #{rel_type}_rels.each {|rel| rel.del } - - dsl = _decl_rels_for('#{rel_type}'.to_sym) - values.each do |value| - dsl.create_relationship_to(self, value) - end - end}, __FILE__, __LINE__) - - module_eval(%Q{ - def #{rel_type}() - dsl = _decl_rels_for('#{rel_type}'.to_sym) - Neo4j::ActiveNode::HasN::Nodes.new(self, dsl) - end}, __FILE__, __LINE__) - - module_eval(%Q{ - def #{rel_type}_rels - dsl = _decl_rels_for('#{rel_type}'.to_sym) - dsl.all_relationships(self) - end}, __FILE__, __LINE__) - - - instance_eval(%Q{ - def #{rel_type} - _decl_rels[:#{rel_type}].rel_type - end}, __FILE__, __LINE__) - - _decl_rels[rel_type.to_sym] = DeclRel.new(rel_type, false, clazz) - end - def has_many(direction, name, options = {}) name = name.to_sym @@ -138,6 +64,10 @@ def #{name}=(other_nodes) other_nodes.each do |node| #{name} << node end + end + + def #{name}_rels + #{name}(nil, :r).pluck(:r) end}, __FILE__, __LINE__) instance_eval(%Q{ diff --git a/lib/neo4j/active_node/query/query_proxy.rb b/lib/neo4j/active_node/query/query_proxy.rb index eb6ffb97d..aff5d4a50 100644 --- a/lib/neo4j/active_node/query/query_proxy.rb +++ b/lib/neo4j/active_node/query/query_proxy.rb @@ -80,6 +80,14 @@ def to_cypher # To add a relationship for the node for the association on this QueryProxy def <<(other_node) associate(other_node, {}) + + self + end + + def [](index) + # TODO: Maybe for this and other methods, use array if already loaded, otherwise + # use OFFSET and LIMIT 1? + self.to_a[index] end def associate(other_node, properties) diff --git a/spec/e2e/has_n_spec.rb b/spec/e2e/has_n_spec.rb index be8de0ef7..cd22c76ad 100644 --- a/spec/e2e/has_n_spec.rb +++ b/spec/e2e/has_n_spec.rb @@ -2,10 +2,6 @@ describe 'has_n' do - let(:node) { clazz_a.create } - let(:friend1) { clazz_a.create } - let(:friend2) { clazz_a.create } - let(:clazz_b) do UniqueClass.create do include Neo4j::ActiveNode @@ -13,38 +9,42 @@ end let(:clazz_a) do - knows_type = clazz_b.to_s + #knows_type = clazz_b UniqueClass.create do include Neo4j::ActiveNode - has_n :friends - has_n(:knows).to(knows_type) - has_n(:knows_me).from(:knows) + has_many :both, :friends, model_class: false + has_many :out, :knows, model_class: self + has_many :in, :knows_me, origin: :knows, model_class: self end end + let(:node) { clazz_a.create } + let(:friend1) { clazz_a.create } + let(:friend2) { clazz_a.create } + describe 'rel_type' do it 'creates the correct type' do node.friends << friend1 r = node.rel - expect(r.rel_type).to eq(:friends) + expect(r.rel_type).to eq(:'#friends') end it 'creates the correct type' do node.knows << friend1 r = node.rel - expect(r.rel_type).to eq(:"#{clazz_a.to_s}#knows") + expect(r.rel_type).to eq(:'#knows') end it 'creates correct incoming relationship' do node.knows_me << friend1 - expect(friend1.rel(dir: :outgoing).rel_type).to eq(:knows) - expect(node.rel(dir: :incoming).rel_type).to eq(:knows) + expect(friend1.rel(dir: :outgoing).rel_type).to eq(:'#knows') + expect(node.rel(dir: :incoming).rel_type).to eq(:'#knows') end end it 'access nodes via declared has_n method' do expect(node.friends.to_a).to eq([]) - expect(node.friends.empty?()).to be true + expect(node.friends.any?()).to be false node.friends << friend1 expect(node.friends.to_a).to eq([friend1]) @@ -79,7 +79,7 @@ end it 'is not empty' do - expect(node.friends.empty?()).to be false + expect(node.friends.any?()).to be true end it 'removes relationships when given a different list' do @@ -107,18 +107,26 @@ end it 'has a is_a method' do - expect(node.friends.is_a?(Array)).to be true + expect(node.friends.is_a?(Neo4j::ActiveNode::Query::QueryProxy)).to be true + expect(node.friends.is_a?(Array)).to be false expect(node.friends.is_a?(String)).to be false end end end - describe 'me.friends.create(other, since: 1994)' do + describe 'me.friends.associate(other, since: 1994)' do it 'creates a new relationship with given properties' do - r = node.friends.create(friend1, since: 1994) + r = node.friends.associate(friend1, since: 1994) + + r = node.rel(dir: :outgoing, type: '#friends') r[:since].should eq(1994) - node.rel(dir: :outgoing, type: clazz_a.friends).should == r end end + + describe "me.friends.create(name: 'Joe')" do + # Should be able to create both relationship and node off of an association + # Maybe .create / .push for creating relationship / node (respectively) + # Maybe should be able to create relationships by passing either node object or hash of values + end end From d3363ad3feb0b1ab5824495c4e8be3ec1a368bb6 Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Sun, 3 Aug 2014 21:14:11 -0700 Subject: [PATCH 41/54] TODO for highlighting --- spec/e2e/has_n_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/e2e/has_n_spec.rb b/spec/e2e/has_n_spec.rb index cd22c76ad..da48979ff 100644 --- a/spec/e2e/has_n_spec.rb +++ b/spec/e2e/has_n_spec.rb @@ -125,7 +125,7 @@ end describe "me.friends.create(name: 'Joe')" do - # Should be able to create both relationship and node off of an association + # TODO: Should be able to create both relationship and node off of an association # Maybe .create / .push for creating relationship / node (respectively) # Maybe should be able to create relationships by passing either node object or hash of values end From 383159a8c387db27c6ac162425a2f3a8f87afdf2 Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Tue, 5 Aug 2014 09:13:47 -0700 Subject: [PATCH 42/54] Change QueryProxy#associate to #create and allow for creating of relationships with persisted/unpersisted nodes/arrays of nodes --- lib/neo4j/active_node/query/query_proxy.rb | 22 +++++++--- spec/e2e/has_n_spec.rb | 51 +++++++++++++++++----- spec/e2e/query_spec.rb | 4 +- 3 files changed, 58 insertions(+), 19 deletions(-) diff --git a/lib/neo4j/active_node/query/query_proxy.rb b/lib/neo4j/active_node/query/query_proxy.rb index aff5d4a50..dd1039fb4 100644 --- a/lib/neo4j/active_node/query/query_proxy.rb +++ b/lib/neo4j/active_node/query/query_proxy.rb @@ -79,7 +79,7 @@ def to_cypher # To add a relationship for the node for the association on this QueryProxy def <<(other_node) - associate(other_node, {}) + create(other_node, {}) self end @@ -90,14 +90,22 @@ def [](index) self.to_a[index] end - def associate(other_node, properties) + def create(other_nodes, properties) if @association - raise ArgumentError, "Node must be of the association's class when model is specified" if @model && other_node.class != @model + other_nodes = [other_nodes].flatten - _association_query_start(:start) - .match(end: other_node.class) - .where(end: {neo_id: other_node.neo_id}) - .create("start#{_association_arrow(properties, true)}end").exec + raise ArgumentError, "Node must be of the association's class when model is specified" if @model && other_nodes.any? {|other_node| other_node.class != @model } + + other_nodes.each do |other_node| + Neo4j::Transaction.run do |tx| + other_node.save if not other_node.persisted? + + _association_query_start(:start) + .match(end: other_node.class) + .where(end: {neo_id: other_node.neo_id}) + .create("start#{_association_arrow(properties, true)}end").exec + end + end else raise "Can only create associations on associations" end diff --git a/spec/e2e/has_n_spec.rb b/spec/e2e/has_n_spec.rb index da48979ff..2d37df9d7 100644 --- a/spec/e2e/has_n_spec.rb +++ b/spec/e2e/has_n_spec.rb @@ -12,6 +12,8 @@ #knows_type = clazz_b UniqueClass.create do include Neo4j::ActiveNode + property :name + has_many :both, :friends, model_class: false has_many :out, :knows, model_class: self has_many :in, :knows_me, origin: :knows, model_class: self @@ -114,19 +116,48 @@ end end - describe 'me.friends.associate(other, since: 1994)' do - it 'creates a new relationship with given properties' do - r = node.friends.associate(friend1, since: 1994) + describe 'me.friends#create(other, since: 1994)' do + describe "creating relationships to existing nodes" do + it 'creates a new relationship when given existing nodes and given properties' do + node.friends.create(friend1, since: 1994) + + r = node.rel(dir: :outgoing, type: '#friends') + + r[:since].should eq(1994) + end + + it 'creates new relationships when given an array of nodes and given properties' do + node.friends.create([friend1, friend2], since: 1995) - r = node.rel(dir: :outgoing, type: '#friends') + rs = node.rels(dir: :outgoing, type: '#friends') - r[:since].should eq(1994) + rs.map(&:end_node).should =~ [friend1, friend2] + rs.each do |r| + r[:since].should eq(1995) + end + end end - end - describe "me.friends.create(name: 'Joe')" do - # TODO: Should be able to create both relationship and node off of an association - # Maybe .create / .push for creating relationship / node (respectively) - # Maybe should be able to create relationships by passing either node object or hash of values + describe "creating relationships and nodes at the same time" do + it 'creates a new relationship when given unpersisted node and given properties' do + node.friends.create(clazz_a.new(name: 'Brad'), {since: 1996}) + + r = node.rel(dir: :outgoing, type: '#friends') + + r[:since].should eq(1996) + r.end_node.name.should == 'Brad' + end + + it 'creates a new relationship when given an array of unpersisted nodes and given properties' do + node.friends.create([clazz_a.new(name: 'James'), clazz_a.new(name: 'Cat')], {since: 1997}) + + rs = node.rels(dir: :outgoing, type: '#friends') + + rs.map(&:end_node).map(&:name).should =~ ['James', 'Cat'] + rs.each do |r| + r[:since].should eq(1997) + end + end + end end end diff --git a/spec/e2e/query_spec.rb b/spec/e2e/query_spec.rb index 31ffe9492..b48dd888a 100644 --- a/spec/e2e/query_spec.rb +++ b/spec/e2e/query_spec.rb @@ -243,8 +243,8 @@ class Teacher context 'othmar likes moster trucks more than samuels' do before(:each) do - samuels.interests.associate(monster_trucks, intensity: 1) - othmar.interests.associate(monster_trucks, intensity: 11) + samuels.interests.create(monster_trucks, intensity: 1) + othmar.interests.create(monster_trucks, intensity: 11) end # Should get both From 86a46b5bfe287d15f53ac2448a465ef6c36198dc Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Wed, 6 Aug 2014 22:22:42 -0700 Subject: [PATCH 43/54] Cleanup of old has_n code. Still some failing specs. Have some questions --- lib/neo4j/active_node/has_n.rb | 26 ++-------------------- lib/neo4j/active_node/query/query_proxy.rb | 6 ++--- spec/integration/node_persistence_spec.rb | 6 +++-- 3 files changed, 9 insertions(+), 29 deletions(-) diff --git a/lib/neo4j/active_node/has_n.rb b/lib/neo4j/active_node/has_n.rb index 956b463da..5b1646584 100644 --- a/lib/neo4j/active_node/has_n.rb +++ b/lib/neo4j/active_node/has_n.rb @@ -2,11 +2,6 @@ module Neo4j::ActiveNode module HasN extend ActiveSupport::Concern - def _decl_rels_for(rel_type) - self.class._decl_rels[rel_type] - end - - module ClassMethods def has_association?(name) @@ -17,27 +12,10 @@ def associations @associations || {} end - def has_relationship?(rel_type) - !!_decl_rels[rel_type] - end - - def has_one_relationship?(rel_type) - has_relationship?(rel_type) && _decl_rels[rel_type].has_one? - end - - def relationship_dir(rel_type) - has_relationship?(rel_type) && _decl_rels[rel_type].dir - end - - def _decl_rels - @_decl_rels ||= {} - end - # make sure the inherited classes inherit the _decl_rels hash def inherited(klass) - copy = _decl_rels.clone - copy.each_pair { |k, v| copy[k] = v.inherit_new } - klass.instance_variable_set(:@_decl_rels, copy) + klass.instance_variable_set(:@associations, associations.clone) + super end diff --git a/lib/neo4j/active_node/query/query_proxy.rb b/lib/neo4j/active_node/query/query_proxy.rb index dd1039fb4..047ac877b 100644 --- a/lib/neo4j/active_node/query/query_proxy.rb +++ b/lib/neo4j/active_node/query/query_proxy.rb @@ -97,7 +97,7 @@ def create(other_nodes, properties) raise ArgumentError, "Node must be of the association's class when model is specified" if @model && other_nodes.any? {|other_node| other_node.class != @model } other_nodes.each do |other_node| - Neo4j::Transaction.run do |tx| + Neo4j::Transaction.run do other_node.save if not other_node.persisted? _association_query_start(:start) @@ -214,9 +214,9 @@ def links_for_where_arg(arg) raise ArgumentError, "Invalid value for '#{key}' condition" if not neo_id.is_a?(Integer) n_string = "n#{node_num}" - dir = @model.relationship_dir(key) + dir = @model.associations[key].direction - arrow = dir == :outgoing ? '-->' : '<--' + arrow = dir == :out ? '-->' : '<--' result << [:match, ->(v) { "#{v}#{arrow}(#{n_string})" }] result << [:where, ->(v) { {"ID(#{n_string})" => neo_id.to_i} }] node_num += 1 diff --git a/spec/integration/node_persistence_spec.rb b/spec/integration/node_persistence_spec.rb index 741afe7b6..b204b068e 100644 --- a/spec/integration/node_persistence_spec.rb +++ b/spec/integration/node_persistence_spec.rb @@ -48,18 +48,19 @@ class MyThing end it 'can create relationships' do - parent = double("parent node", neo_id: 1) + parent = double("parent node", neo_id: 1, persisted?: true) node = double('unwrapped_node', props: {a: 999}, rel: nil, neo_id: 2) @session.should_receive(:create_node).with({a: 1}, [:MyThing]).and_return(node) @session.should_receive(:query).exactly(3).times.and_return(Neo4j::Core::Query.new) @session.should_receive(:_query).exactly(2).times + @session.should_receive(:begin_tx) thing = MyThing.create(a: 1, parent: parent) thing.props.should == {a: 999} end it 'will delete old relationship before creating a new one' do - parent = double("parent node", neo_id: 1) + parent = double("parent node", neo_id: 1, persisted?: true) old_rel = double("old relationship") node = double('unwrapped_node', props: {a: 999}, rel: old_rel, neo_id: 2) @@ -67,6 +68,7 @@ class MyThing @session.should_receive(:create_node).with({a: 1}, [:MyThing]).and_return(node) @session.should_receive(:query).exactly(3).times.and_return(Neo4j::Core::Query.new) @session.should_receive(:_query).exactly(2).times + @session.should_receive(:begin_tx) thing = MyThing.create(a: 1, parent: parent) thing.props.should == {a: 999} From 364edf4ec2812839b673dd81f81153bedafbdb2e Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Thu, 7 Aug 2014 09:45:36 -0700 Subject: [PATCH 44/54] Backing up incomplete changes --- lib/neo4j/active_node/has_n/association.rb | 15 ++++++++ lib/neo4j/active_node/query/query_proxy.rb | 4 ++ spec/e2e/has_n_spec.rb | 43 ++++++++++++++++++++++ 3 files changed, 62 insertions(+) diff --git a/lib/neo4j/active_node/has_n/association.rb b/lib/neo4j/active_node/has_n/association.rb index a389dae77..c17fddc20 100644 --- a/lib/neo4j/active_node/has_n/association.rb +++ b/lib/neo4j/active_node/has_n/association.rb @@ -20,6 +20,8 @@ def initialize(type, direction, name, options = {}) options[:model_class] end + setup_callbacks_from_options!(options) + if options[:type] && options[:origin] raise ArgumentError, "Cannot specify both :type and :origin (#{base_declaration})" else @@ -67,8 +69,19 @@ def target_class raise ArgumentError, "Could not find `#{@target_class}` class and no :model_class specified" end + def callback(type) + + end + private + def setup_callbacks_from_options!(options) + # https://github.com/andreasronge/neo4j/issues/369 + # https://github.com/andreasronge/neo4j/wiki/Neo4j-v3#relationship-callbacks + @before_create = options[:before] + @after_create = options[:after] + end + def relationship_type(create = false) if @relationship_type @relationship_type @@ -93,6 +106,8 @@ def base_declaration # has_many :friends, model_class: Friend # Would return false # has_many :friends, model_class: Person # Would return true def exceptional_target_class? + # TODO: Exceptional if target_class.nil?? (when model_class false) + target_class && target_class.name != @target_class_name_from_name end diff --git a/lib/neo4j/active_node/query/query_proxy.rb b/lib/neo4j/active_node/query/query_proxy.rb index 047ac877b..456e0a009 100644 --- a/lib/neo4j/active_node/query/query_proxy.rb +++ b/lib/neo4j/active_node/query/query_proxy.rb @@ -100,10 +100,14 @@ def create(other_nodes, properties) Neo4j::Transaction.run do other_node.save if not other_node.persisted? + @association.callback(:before) + _association_query_start(:start) .match(end: other_node.class) .where(end: {neo_id: other_node.neo_id}) .create("start#{_association_arrow(properties, true)}end").exec + + @association.callback(:after) end end else diff --git a/spec/e2e/has_n_spec.rb b/spec/e2e/has_n_spec.rb index 2d37df9d7..58945b6ee 100644 --- a/spec/e2e/has_n_spec.rb +++ b/spec/e2e/has_n_spec.rb @@ -160,4 +160,47 @@ end end end + + + describe 'callbacks' do + let(:clazz_c) do + #knows_type = clazz_b + UniqueClass.create do + include Neo4j::ActiveNode + property :name + + has_many :out, :knows, model_class: self, before: :before_callback + has_many :in, :knows_me, origin: :knows, model_class: self, after: :after_callback + has_many :in, :knows_me2, origin: :knows, model_class: self, before: :false_callback + + def before_callback(from, to) + end + + def after_callback(from, to) + end + + def false_callback(from, to) + false + end + end + end + + let(:node) { clazz_a.create } + let(:friend1) { clazz_a.create } + let(:friend2) { clazz_a.create } + + it 'should call before_callback when node added to #knows association' do + expect(node).to receive(:before_callback).with(node, friend1) { node.knows.to_a.size.should == 0 } + + node.knows << friend1 + end + + it 'should call before_callback when node added to #knows association' do + expect(node).to receive(:after_callback).with(node, friend1) { node.knows.to_a.size.should == 1 } + + node.knows << friend1 + end + + + end end From 8920904aab84a0479e49b4adcdf5369809545a8d Mon Sep 17 00:00:00 2001 From: Chris Grigg Date: Mon, 11 Aug 2014 15:32:18 -0400 Subject: [PATCH 45/54] complete callbacks, specs, disable transaction for now --- lib/neo4j/active_node/has_n/association.rb | 8 +- lib/neo4j/active_node/query/query_proxy.rb | 30 +- spec/e2e/has_n_spec.rb | 34 +- spec/e2e/has_one_spec.rb | 37 ++ spec/integration/has_n_spec.rb | 426 ++++++++++----------- spec/integration/node_persistence_spec.rb | 4 +- 6 files changed, 293 insertions(+), 246 deletions(-) diff --git a/lib/neo4j/active_node/has_n/association.rb b/lib/neo4j/active_node/has_n/association.rb index c17fddc20..37746ecbe 100644 --- a/lib/neo4j/active_node/has_n/association.rb +++ b/lib/neo4j/active_node/has_n/association.rb @@ -70,7 +70,12 @@ def target_class end def callback(type) + @callbacks[type] + end + def perform_callback(caller, other_node, type) + return if callback(type).nil? + caller.send(callback(type), other_node) end private @@ -78,8 +83,7 @@ def callback(type) def setup_callbacks_from_options!(options) # https://github.com/andreasronge/neo4j/issues/369 # https://github.com/andreasronge/neo4j/wiki/Neo4j-v3#relationship-callbacks - @before_create = options[:before] - @after_create = options[:after] + @callbacks = {before: options[:before], after: options[:after]} end def relationship_type(create = false) diff --git a/lib/neo4j/active_node/query/query_proxy.rb b/lib/neo4j/active_node/query/query_proxy.rb index 456e0a009..2db3e08bb 100644 --- a/lib/neo4j/active_node/query/query_proxy.rb +++ b/lib/neo4j/active_node/query/query_proxy.rb @@ -91,27 +91,23 @@ def [](index) end def create(other_nodes, properties) - if @association - other_nodes = [other_nodes].flatten + raise "Can only create associations on associations" unless @association + other_nodes = [other_nodes].flatten - raise ArgumentError, "Node must be of the association's class when model is specified" if @model && other_nodes.any? {|other_node| other_node.class != @model } + raise ArgumentError, "Node must be of the association's class when model is specified" if @model && other_nodes.any? {|other_node| other_node.class != @model } + other_nodes.each do |other_node| + #Neo4j::Transaction.run do + other_node.save if not other_node.persisted? - other_nodes.each do |other_node| - Neo4j::Transaction.run do - other_node.save if not other_node.persisted? + return false if @association.perform_callback(@options[:start_object], other_node, :before) == false - @association.callback(:before) + _association_query_start(:start) + .match(end: other_node.class) + .where(end: {neo_id: other_node.neo_id}) + .create("start#{_association_arrow(properties, true)}end").exec - _association_query_start(:start) - .match(end: other_node.class) - .where(end: {neo_id: other_node.neo_id}) - .create("start#{_association_arrow(properties, true)}end").exec - - @association.callback(:after) - end - end - else - raise "Can only create associations on associations" + @association.perform_callback(@options[:start_object], other_node, :after) + #end end end diff --git a/spec/e2e/has_n_spec.rb b/spec/e2e/has_n_spec.rb index 58945b6ee..c7f986acc 100644 --- a/spec/e2e/has_n_spec.rb +++ b/spec/e2e/has_n_spec.rb @@ -139,9 +139,15 @@ end describe "creating relationships and nodes at the same time" do + let(:node2) { double("unpersisted node", props: { name: 'Brad' } )} + it 'creates a new relationship when given unpersisted node and given properties' do node.friends.create(clazz_a.new(name: 'Brad'), {since: 1996}) + #node2.stub(:persisted?).and_return(false) + #node2.stub(:save).and_return(true) + #node2.stub(:neo_id).and_return(2) + #node.friends.create(node2, since: 1996) r = node.rel(dir: :outgoing, type: '#friends') r[:since].should eq(1996) @@ -171,15 +177,15 @@ has_many :out, :knows, model_class: self, before: :before_callback has_many :in, :knows_me, origin: :knows, model_class: self, after: :after_callback - has_many :in, :knows_me2, origin: :knows, model_class: self, before: :false_callback + has_many :in, :will_fail, origin: :knows, model_class: self, before: :false_callback - def before_callback(from, to) + def before_callback(other) end - def after_callback(from, to) + def after_callback(other) end - def false_callback(from, to) + def false_callback(other) false end end @@ -189,18 +195,22 @@ def false_callback(from, to) let(:friend1) { clazz_a.create } let(:friend2) { clazz_a.create } - it 'should call before_callback when node added to #knows association' do - expect(node).to receive(:before_callback).with(node, friend1) { node.knows.to_a.size.should == 0 } + let(:callback_friend1) { clazz_c.create } + let(:callback_friend2) { clazz_c.create } - node.knows << friend1 + it 'calls before_callback when node added to #knows association' do + expect(callback_friend1).to receive(:before_callback).with(callback_friend2) { callback_friend1.knows.to_a.size.should == 0 } + callback_friend1.knows << callback_friend2 end - it 'should call before_callback when node added to #knows association' do - expect(node).to receive(:after_callback).with(node, friend1) { node.knows.to_a.size.should == 1 } - - node.knows << friend1 + it 'calls after_callback when node added to #knows association' do + expect(callback_friend1).to receive(:after_callback).with(callback_friend2) { callback_friend2.knows.to_a.size.should == 1 } + callback_friend1.knows_me << callback_friend2 end - + it 'prevents the association from being created if before returns "false" explicitly' do + callback_friend1.will_fail << callback_friend2 + expect(callback_friend1.knows_me.to_a.size).to eq 0 + end end end diff --git a/spec/e2e/has_one_spec.rb b/spec/e2e/has_one_spec.rb index b1b1e7d9c..dbe5ae866 100644 --- a/spec/e2e/has_one_spec.rb +++ b/spec/e2e/has_one_spec.rb @@ -93,4 +93,41 @@ class File1 end end + describe 'callbacks' do + class CallbackUser + include Neo4j::ActiveNode + + has_one :out, :best_friend, model_class: self, before: :before_callback + has_one :in, :best_friend_of, origin: :best_friend, model_class: self, after: :after_callback + has_one :in, :failing_assoc, origin: :best_friend, model_class: self, before: :false_before_callback + + def before_callback(other) + end + + def after_callback(other) + end + + def false_before_callback(other) + return false + end + end + + let(:node1) { CallbackUser.create } + let(:node2) { CallbackUser.create } + + it 'calls before callback' do + expect(node1).to receive(:before_callback).with(node2) + node1.best_friend = node2 + end + + it 'calls after callback' do + expect(node1).to receive(:after_callback).with(node2) + node1.best_friend_of = node2 + end + + it 'prevents the relationship from beign created if a before callback returns false' do + node1.failing_assoc = node2 + expect(node1.failing_assoc).to be_nil + end + end end diff --git a/spec/integration/has_n_spec.rb b/spec/integration/has_n_spec.rb index 293477a4f..d38fbae6a 100644 --- a/spec/integration/has_n_spec.rb +++ b/spec/integration/has_n_spec.rb @@ -1,217 +1,217 @@ -require 'spec_helper' +# require 'spec_helper' -describe "has_n" do +# describe "has_n" do - let(:clazz) do - UniqueClass.create do - include Neo4j::ActiveNode - end - end +# let(:clazz) do +# UniqueClass.create do +# include Neo4j::ActiveNode +# end +# end - let(:other_clazz) do - UniqueClass.create do - include Neo4j::ActiveNode - end - end - - describe '#_decl_rels' do - it 'is a Hash' do - #clazz.has_n :friends - clazz._decl_rels.should be_a(Hash) - end - - context 'when inherited' do - class TestHasNBase - include Neo4j::ActiveNode - has_n :knows - end - - class TestHasNSub < TestHasNBase +# let(:other_clazz) do +# UniqueClass.create do +# include Neo4j::ActiveNode +# end +# end + +# describe '#_decl_rels' do +# it 'is a Hash' do +# #clazz.has_n :friends +# clazz._decl_rels.should be_a(Hash) +# end + +# context 'when inherited' do +# class TestHasNBase +# include Neo4j::ActiveNode +# has_n :knows +# end + +# class TestHasNSub < TestHasNBase - end - - it 'inherit declared has_n' do - TestHasNSub._decl_rels[:knows].should be_a(Neo4j::ActiveNode::HasN::DeclRel) - end - - it 'impl has_n accessor methods' do - node = TestHasNSub.new - node.should respond_to(:knows) - node.should respond_to(:knows_rels) - end - end - end - - describe 'has_n(:friends)' do - before do - clazz.has_n :friends - end - - let(:core_node) do - double("core node", props: {}) - end - - let(:node) do - session.should_receive(:create_node).and_return(core_node) - clazz.create - end - - - let(:session) do - session = double("Mock Session") - Neo4j::Session.stub(:current).and_return(session) - session - end - - describe 'clazz.friends' do - subject { clazz.friends } - it { should eq(:friends)} - end - - describe 'node.friends << a_node' do - - it 'creates a new relationship' do - a_node = double("a node") - - node.should_receive(:create_rel).with(:friends, a_node, {}) - - # when - node.friends << a_node - end - end - - describe 'node.friends = [a_node, b_node]' do - - it 'creates a new relationship' do - a_node = double("a node") - b_node = double("b node") - - node.should_receive(:rels).with({:dir=>:outgoing, :type=>:friends}).and_return([]) - - node.should_receive(:create_rel).with(:friends, a_node, {}) - node.should_receive(:create_rel).with(:friends, b_node, {}) - - # when - node.friends = [a_node, b_node] - end - end - - describe 'node.friends.to_a' do - - it 'traverse correct relationships' do - core_node.should_receive(:nodes).with(dir: :outgoing, type: :friends).and_return([]) - node.friends.to_a.should eq([]) - end - - it 'can return wrapped nodes' do - friend_node_wrapper = double("friend node wrapper") - core_node.should_receive(:nodes).with(dir: :outgoing, type: :friends).and_return([friend_node_wrapper]) - node.friends.to_a.should eq([friend_node_wrapper]) - end - end - - describe '_decl_rels[:friends]' do - subject do - clazz._decl_rels[:friends] - end - - it { should be_a(Neo4j::ActiveNode::HasN::DeclRel)} - its(:dir) { should eq(:outgoing)} - its(:source_class) { should eq(clazz)} - its(:rel_type) { should eq(:friends)} - end - end - - - describe 'has_n(:friends).to(OtherClass)' do - before do - clazz.has_n(:friends).to(other_clazz) - end - - describe 'clazz.friends' do - subject { clazz.friends } - it { should eq(:"#{clazz}#friends")} - end - - describe '_decl_rels[:friends]' do - subject do - clazz._decl_rels[:friends] - end - - it { should be_a(Neo4j::ActiveNode::HasN::DeclRel) } - its(:dir) { should eq(:outgoing) } - its(:source_class) { should eq(clazz) } - its(:rel_type) { should eq(:"#{clazz}#friends") } - end - end - - describe 'has_n(:known_by).from(OtherClass)' do - before do - clazz.has_n(:known_by).from(other_clazz) - end - - describe 'clazz.known_by' do - subject { clazz.known_by } - it { should eq(:"#{other_clazz}#known_by")} - end - - describe '_decl_rels[:known_by]' do - subject do - clazz._decl_rels[:known_by] - end - - it { should be_a(Neo4j::ActiveNode::HasN::DeclRel) } - its(:dir) { should eq(:incoming) } - its(:source_class) { should eq(clazz) } - its(:rel_type) { should eq(:"#{other_clazz}#known_by") } - end - - end - - describe 'has_n(:known_by).from(OtherClass, :knows)' do - before do - clazz.has_n(:known_by).from(other_clazz, :knows) - end - - describe 'clazz.known_by' do - subject { clazz.known_by } - it { should eq(:"#{other_clazz}#knows")} - end - - describe '_decl_rels[:known_by]' do - subject do - clazz._decl_rels[:known_by] - end - - it { should be_a(Neo4j::ActiveNode::HasN::DeclRel) } - its(:dir) { should eq(:incoming) } - its(:source_class) { should eq(clazz) } - its(:rel_type) { should eq(:"#{other_clazz}#knows") } - end - - end - - describe 'has_n(:known_by).from(:"OtherClass#knows")' do - before do - clazz.has_n(:known_by).from(:"OtherClass#knows") - end - - describe 'clazz.known_by' do - subject { clazz.known_by } - it { should eq(:"OtherClass#knows")} - end - - describe '_decl_rels[:known_by]' do - subject do - clazz._decl_rels[:known_by] - end - - it { should be_a(Neo4j::ActiveNode::HasN::DeclRel) } - its(:dir) { should eq(:incoming) } - its(:source_class) { should eq(clazz) } - its(:rel_type) { should eq(:"OtherClass#knows") } - end - - end - -end +# end + +# it 'inherit declared has_n' do +# TestHasNSub._decl_rels[:knows].should be_a(Neo4j::ActiveNode::HasN::DeclRel) +# end + +# it 'impl has_n accessor methods' do +# node = TestHasNSub.new +# node.should respond_to(:knows) +# node.should respond_to(:knows_rels) +# end +# end +# end + +# describe 'has_n(:friends)' do +# before do +# clazz.has_n :friends +# end + +# let(:core_node) do +# double("core node", props: {}) +# end + +# let(:node) do +# session.should_receive(:create_node).and_return(core_node) +# clazz.create +# end + + +# let(:session) do +# session = double("Mock Session") +# Neo4j::Session.stub(:current).and_return(session) +# session +# end + +# describe 'clazz.friends' do +# subject { clazz.friends } +# it { should eq(:friends)} +# end + +# describe 'node.friends << a_node' do + +# it 'creates a new relationship' do +# a_node = double("a node") + +# node.should_receive(:create_rel).with(:friends, a_node, {}) + +# # when +# node.friends << a_node +# end +# end + +# describe 'node.friends = [a_node, b_node]' do + +# it 'creates a new relationship' do +# a_node = double("a node") +# b_node = double("b node") + +# node.should_receive(:rels).with({:dir=>:outgoing, :type=>:friends}).and_return([]) + +# node.should_receive(:create_rel).with(:friends, a_node, {}) +# node.should_receive(:create_rel).with(:friends, b_node, {}) + +# # when +# node.friends = [a_node, b_node] +# end +# end + +# describe 'node.friends.to_a' do + +# it 'traverse correct relationships' do +# core_node.should_receive(:nodes).with(dir: :outgoing, type: :friends).and_return([]) +# node.friends.to_a.should eq([]) +# end + +# it 'can return wrapped nodes' do +# friend_node_wrapper = double("friend node wrapper") +# core_node.should_receive(:nodes).with(dir: :outgoing, type: :friends).and_return([friend_node_wrapper]) +# node.friends.to_a.should eq([friend_node_wrapper]) +# end +# end + +# describe '_decl_rels[:friends]' do +# subject do +# clazz._decl_rels[:friends] +# end + +# it { should be_a(Neo4j::ActiveNode::HasN::DeclRel)} +# its(:dir) { should eq(:outgoing)} +# its(:source_class) { should eq(clazz)} +# its(:rel_type) { should eq(:friends)} +# end +# end + + +# describe 'has_n(:friends).to(OtherClass)' do +# before do +# clazz.has_n(:friends).to(other_clazz) +# end + +# describe 'clazz.friends' do +# subject { clazz.friends } +# it { should eq(:"#{clazz}#friends")} +# end + +# describe '_decl_rels[:friends]' do +# subject do +# clazz._decl_rels[:friends] +# end + +# it { should be_a(Neo4j::ActiveNode::HasN::DeclRel) } +# its(:dir) { should eq(:outgoing) } +# its(:source_class) { should eq(clazz) } +# its(:rel_type) { should eq(:"#{clazz}#friends") } +# end +# end + +# describe 'has_n(:known_by).from(OtherClass)' do +# before do +# clazz.has_n(:known_by).from(other_clazz) +# end + +# describe 'clazz.known_by' do +# subject { clazz.known_by } +# it { should eq(:"#{other_clazz}#known_by")} +# end + +# describe '_decl_rels[:known_by]' do +# subject do +# clazz._decl_rels[:known_by] +# end + +# it { should be_a(Neo4j::ActiveNode::HasN::DeclRel) } +# its(:dir) { should eq(:incoming) } +# its(:source_class) { should eq(clazz) } +# its(:rel_type) { should eq(:"#{other_clazz}#known_by") } +# end + +# end + +# describe 'has_n(:known_by).from(OtherClass, :knows)' do +# before do +# clazz.has_n(:known_by).from(other_clazz, :knows) +# end + +# describe 'clazz.known_by' do +# subject { clazz.known_by } +# it { should eq(:"#{other_clazz}#knows")} +# end + +# describe '_decl_rels[:known_by]' do +# subject do +# clazz._decl_rels[:known_by] +# end + +# it { should be_a(Neo4j::ActiveNode::HasN::DeclRel) } +# its(:dir) { should eq(:incoming) } +# its(:source_class) { should eq(clazz) } +# its(:rel_type) { should eq(:"#{other_clazz}#knows") } +# end + +# end + +# describe 'has_n(:known_by).from(:"OtherClass#knows")' do +# before do +# clazz.has_n(:known_by).from(:"OtherClass#knows") +# end + +# describe 'clazz.known_by' do +# subject { clazz.known_by } +# it { should eq(:"OtherClass#knows")} +# end + +# describe '_decl_rels[:known_by]' do +# subject do +# clazz._decl_rels[:known_by] +# end + +# it { should be_a(Neo4j::ActiveNode::HasN::DeclRel) } +# its(:dir) { should eq(:incoming) } +# its(:source_class) { should eq(clazz) } +# its(:rel_type) { should eq(:"OtherClass#knows") } +# end + +# end + +# end diff --git a/spec/integration/node_persistence_spec.rb b/spec/integration/node_persistence_spec.rb index b204b068e..4fff7f5e7 100644 --- a/spec/integration/node_persistence_spec.rb +++ b/spec/integration/node_persistence_spec.rb @@ -54,7 +54,7 @@ class MyThing @session.should_receive(:create_node).with({a: 1}, [:MyThing]).and_return(node) @session.should_receive(:query).exactly(3).times.and_return(Neo4j::Core::Query.new) @session.should_receive(:_query).exactly(2).times - @session.should_receive(:begin_tx) + #@session.should_receive(:begin_tx) thing = MyThing.create(a: 1, parent: parent) thing.props.should == {a: 999} end @@ -68,7 +68,7 @@ class MyThing @session.should_receive(:create_node).with({a: 1}, [:MyThing]).and_return(node) @session.should_receive(:query).exactly(3).times.and_return(Neo4j::Core::Query.new) @session.should_receive(:_query).exactly(2).times - @session.should_receive(:begin_tx) + #@session.should_receive(:begin_tx) thing = MyThing.create(a: 1, parent: parent) thing.props.should == {a: 999} From 7181e1591792d3564aece55211b87d14848572a0 Mon Sep 17 00:00:00 2001 From: Chris Grigg Date: Mon, 11 Aug 2014 16:38:45 -0400 Subject: [PATCH 46/54] a wee refactor --- lib/neo4j/active_node/has_n/association.rb | 73 +++++++++++----------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/lib/neo4j/active_node/has_n/association.rb b/lib/neo4j/active_node/has_n/association.rb index 37746ecbe..b92ead7e4 100644 --- a/lib/neo4j/active_node/has_n/association.rb +++ b/lib/neo4j/active_node/has_n/association.rb @@ -9,24 +9,23 @@ class Association def initialize(type, direction, name, options = {}) raise ArgumentError, "Invalid association type: #{type.inspect}" if not [:has_many, :has_one].include?(type.to_sym) raise ArgumentError, "Invalid direction: #{direction.inspect}" if not [:out, :in, :both].include?(direction.to_sym) - @type = type.to_sym @name = name @direction = direction.to_sym - @target_class_name_from_name = name.to_s.classify - @target_class_option = if options[:model_class].nil? - @target_class_name_from_name - elsif options[:model_class] - options[:model_class] - end + raise ArgumentError, "Cannot specify both :type and :origin (#{base_declaration})" if options[:type] && options[:origin] - setup_callbacks_from_options!(options) + @target_class_name_from_name = name.to_s.classify + @target_class_option = target_class_option(options) + @callbacks = {before: options[:before], after: options[:after]} + @relationship_type = options[:type] && options[:type].to_sym + @origin = options[:origin] && options[:origin].to_sym + end - if options[:type] && options[:origin] - raise ArgumentError, "Cannot specify both :type and :origin (#{base_declaration})" - else - @relationship_type = options[:type] && options[:type].to_sym - @origin = options[:origin] && options[:origin].to_sym + def target_class_option(options) + if options[:model_class].nil? + @target_class_name_from_name + elsif options[:model_class] + options[:model_class] end end @@ -37,24 +36,9 @@ def arrow_cypher(var = nil, properties = {}, create = false) relationship_type = relationship_type(create) relationship_name_cypher = ":`#{relationship_type}`" if relationship_type - properties_string = properties.map do |key, value| - "#{key}: #{value.inspect}" - end.join(', ') - properties_string = " {#{properties_string}}" unless properties_string.empty? - - relationship_cypher = "[#{var}#{relationship_name_cypher}#{properties_string}]" - - direction = @direction - direction = :out if create && @direction == :both - - case direction - when :out - "-#{relationship_cypher}->" - when :in - "<-#{relationship_cypher}-" - when :both - "-#{relationship_cypher}-" - end + properties_string = get_properties_string(properties) + relationship_cypher = get_relationship_cypher(var, relationship_name_cypher, properties_string) + get_direction(relationship_cypher, create) end def target_class_name @@ -79,13 +63,30 @@ def perform_callback(caller, other_node, type) end private - - def setup_callbacks_from_options!(options) - # https://github.com/andreasronge/neo4j/issues/369 - # https://github.com/andreasronge/neo4j/wiki/Neo4j-v3#relationship-callbacks - @callbacks = {before: options[:before], after: options[:after]} + + def get_direction(relationship_cypher, create) + dir = (create && @direction == :both) ? :out : @direction + case dir + when :out + "-#{relationship_cypher}->" + when :in + "<-#{relationship_cypher}-" + when :both + "-#{relationship_cypher}-" + end end + def get_relationship_cypher(var, relationship_name_cypher, properties_string) + "[#{var}#{relationship_name_cypher}#{properties_string}]" + end + + def get_properties_string(properties) + p = properties.map do |key, value| + "#{key}: #{value.inspect}" + end.join(', ') + p.size == 0 ? '' : " {#{p}}" + end + def relationship_type(create = false) if @relationship_type @relationship_type From ccb453cc438f2d28da0b9abe57cedd479c2151aa Mon Sep 17 00:00:00 2001 From: Chris Grigg Date: Wed, 13 Aug 2014 01:32:07 -0400 Subject: [PATCH 47/54] remove qq --- lib/neo4j.rb | 1 - lib/neo4j/active_node/query.rb | 8 - lib/neo4j/active_node/quick_query.rb | 245 --------------------------- 3 files changed, 254 deletions(-) delete mode 100644 lib/neo4j/active_node/quick_query.rb diff --git a/lib/neo4j.rb b/lib/neo4j.rb index f41369193..c4aa0d0ba 100644 --- a/lib/neo4j.rb +++ b/lib/neo4j.rb @@ -31,7 +31,6 @@ require 'neo4j/active_node/has_n/nodes' require 'neo4j/active_node/query/query_proxy' require 'neo4j/active_node/query' -require 'neo4j/active_node/quick_query' require 'neo4j/active_node' require 'neo4j/active_node/orm_adapter' diff --git a/lib/neo4j/active_node/query.rb b/lib/neo4j/active_node/query.rb index 59f5b5bfc..5a5dbaa1e 100644 --- a/lib/neo4j/active_node/query.rb +++ b/lib/neo4j/active_node/query.rb @@ -1,10 +1,6 @@ module Neo4j module ActiveNode - def qq(as = :n1) - QuickQuery.new(self, as, self.class) - end - # Helper methods to return Neo4j::Core::Query objects. A query object can be used to successively build a cypher query # # person.query_as(:n).match('n-[:friend]-o').return(o: :name) # Return the names of all the person's friends @@ -56,10 +52,6 @@ def query_proxy(options = {}) @query_proxy || Neo4j::ActiveNode::Query::QueryProxy.new(self, nil, options) end - def qq(as = :n1) - QuickQuery.new(self.name.constantize, as) - end - def as(node_var) query_proxy(node: node_var) end diff --git a/lib/neo4j/active_node/quick_query.rb b/lib/neo4j/active_node/quick_query.rb deleted file mode 100644 index 04c66b9e4..000000000 --- a/lib/neo4j/active_node/quick_query.rb +++ /dev/null @@ -1,245 +0,0 @@ -module Neo4j - module ActiveNode - # An abstraction layer to quickly build and return objects from the Neo4j Core Query class. - # It auto-increments node and relationship identifiers, uses relationships pre-defined in models to create match methods, and automatically maps - # results to collections. - class QuickQuery - attr_reader :quick_query - - # Initialize sets the values of @node_on_deck and defines @rel_on_deck, among other things. - # The objects on deck are the objects implicitly modified when calling a method without specifying an identifier. - # They are auto-incremented at stages throughout the class. - def initialize(caller, as, caller_class = nil) - @caller_class = caller_class || caller - @node_on_deck = @return_obj = as.to_sym - @current_node_index = 2 - @current_rel_index = 1 - @rel_on_deck = nil - @caller = caller - @quick_query = caller.query_as(as) - @identifiers = [@node_on_deck] - set_rel_methods(@caller_class) - return self - end - - # sends the #to_cypher method to the core query class - def to_cypher - @quick_query.return(@return_obj).to_cypher - end - - - # Creates methods that send cleaned up arguments to the Core Query class - # Pass a symbol to specify the target identifier. - # Pass a hash to specify match parameters. - # Pass a valid cypher string to send directly to Core Query. - # If an identifier is not specified, it will apply them to the on-deck node. - # @example - # Student.qq.where(name: 'chris') - # Student.qq.lessons.where(:n2, name: 'history 101') - # Student.qq.lessons() - CUSTOM_METHODS = %w[where set set_props] - - CUSTOM_METHODS.each do |method| - class_eval(%Q{ - def #{method}(*args) - result = prepare(args) - final_query(__method__, result) - return self - end - }, __FILE__, __LINE__) - end - - - # Creates methods that send strings directly to Core Query class - LITERAL_METHODS = %w[limit skip match offset] - - LITERAL_METHODS.each do |method| - class_eval(%Q{ - def #{method}(s) - @quick_query = @quick_query.send(__method__, s) - return self - end - }, __FILE__, __LINE__) - end - - # Sends #return to the core query class, then maps the results to an enumerable. - # This works because it assumes all returned objects will be of the same type. - # Assumes the @return_obj if nothing is specified. - # if you want distinct, pass 'true' as second parameter - # @example - # Student.qq.lessons.return(:n2) - # Student.qq.lessons.return(:n2, true) - #def return(obj_sym = @return_obj, distinct = false) - def return(*args) - obj_sym = args.select{|el| el.is_a?(Symbol) }.first || @return_obj - distinct = args.select{|el| el.is_a?(TrueClass) }.first || false - - r = final_return(obj_sym, distinct) - @quick_query.return(r).to_a.map{|el| el[obj_sym.to_sym] } - end - - # Same as return but uses whatever the current @return_obj is - def to_a(distinct = false) - r = final_return(@return_obj, distinct) - @quick_query.return(r).to_a.map{|el| el[@return_obj.to_sym]} - end - - # Same as to_a but with distinct set true - def to_a! - self.to_a(true) - end - - # Set order for return. - # @param prop_sym [Symbol] a symbol matching the property on the return class use for order - # @param desc_bool [Boolean] boolean to dictate whether to sort descending. Defaults false, use true to descend - def order(prop_sym, desc_bool = false) - arg = "#{@return_obj}.#{prop_sym.to_s}" - end_arg = desc_bool ? arg + 'DESC' : arg - @quick_query = @quick_query.order(end_arg) - return self - end - - private - - def prepare(args) - target = args.select{|el| el.is_a?(Symbol) } - send_target = target.empty? ? @node_on_deck : target.first - result = process_args(args, send_target) - end - - def final_return(return_obj, distinct) - distinct ? "distinct #{return_obj.to_s}" : return_obj.to_sym - end - - def final_query(method, result) - @quick_query = @quick_query.send(method, result) - end - - # Creates match methods based on the caller's relationships defined in the model. - # It works best when a relationship is defined explicitly with a direction and a receiving/incoming model. - # This fires once on initialize, again every time a matcher method is called to build methods for the next step. - # The dynamic classes accept the following: - # -a symbol to refer to the destination node - # -a hash with key :rel_as, value a symbol to act as relationship identifier (otherwise it uses r#{@current_rel_index}) - # -a hash with key :rel_where containing other hashes of {parameter: value} to specify relationship conditions - # -hashes of {parameter: value} that specify conditions for the destination nodes - # @example - # Student.qq.lessons - # Student.qq.lessons(rel_as: :student_status) - # Student.qq.lessons(rel_as: :student_status, rel_where: { grade: 'b-' }) - # Student.qq.lessons(rel_as: :student_status, rel_where: { grade: 'b-' }, :lessinzzz, class: 'history 101').teachers - def set_rel_methods(caller_class) - caller_class._decl_rels.each { |k,v| - if v.target_class.nil? - class_eval(%Q{ - def #{k}(*args) - process_rel(args, "#{v.rel_type}") - return self - end}, __FILE__, __LINE__) - else - class_eval(%Q{ - def #{k}(*args) - process_rel(args, "#{v.rel_type}", "#{v.target_class.name}") - return self - end}, __FILE__, __LINE__) - end - } - end - - # Called when a matcher method is called - # args are the arguments sent along with the matcher method - # rel_type is the defined relationship type - # right_label is the label to use for the destination, if available. It's the "right label" cause it's on the right side... get it? - # A label can only be used if the model defines the destination class explicitly. - def process_rel(args, rel_type, right_label = nil) - from_node = @node_on_deck - hashes, strings = setup_rel_args(args) - set_on_deck(args) - - hashes = process_rel_hashes(hashes) unless hashes.nil? - end_args = process_args([hashes] + strings, @node_on_deck) unless hashes.nil? && strings.nil? - - destination = right_label.nil? ? @node_on_deck : "(#{@node_on_deck}:#{right_label})" - @quick_query = @quick_query.match("#{from_node}-[#{@rel_on_deck}:`#{rel_type}`]-#{destination}") - @quick_query = @quick_query.where(end_args) unless end_args.nil? - set_rel_methods(right_label.constantize) unless right_label.nil? - - return self - end - - # Prepares arguments passed with the relationship matcher. It finds hashes, which contain properties and values, and strings, which - # which literal cypher phrases. - def setup_rel_args(args) - hashes = args.select{|el| el.is_a?(Hash) }.first - strings = args.select{|el| el.is_a?(String) } - return hashes, strings - end - - # Queues up the new node and relationship. This is only used during process_rel. - def set_on_deck(args) - @node_on_deck = @return_obj = node_as(args.select{|el| el.is_a?(Symbol) }.first) - @rel_on_deck = new_rel_id - @identifiers.push([@node_on_deck, @rel_on_deck]) - end - - # Prepares relationship-specific hashes found during the setup_rel_args method. Removes anything it finds from the hash and sends it back. - def process_rel_hashes(hashes) - @rel_on_deck = set_rel_as(hashes) - @identifiers.push @rel_on_deck - - set_rel_where(hashes) - - hashes.delete_if{|k,v| k == :rel_as || k == :rel_where } - end - - # Creates a new node identifier - def new_node_id - n = "n#{@current_node_index}" - @current_node_index += 1 - return n - end - - # Creates a new relationship identifier - def new_rel_id - r = "r#{@current_rel_index}" - @current_rel_index += 1 - return r - end - - def node_as(node_as) - node_as.nil? ? new_node_id : node_as.to_sym - end - - def set_rel_as(h) - h.has_key?(:rel_as) ? h[:rel_as] : new_rel_id - end - - def set_rel_where(h) - if h.has_key?(:rel_where) - @quick_query = @quick_query.where(Hash[@rel_on_deck => h[:rel_where]]) - end - end - - # Utility method used to split up passed values and fix syntax to match Neo4j Core Query class - def process_args(args, where_target) - end_args = [] - args.each do |arg| - if arg.is_a?(String) - end_args.push process_string(arg, where_target) - elsif arg.is_a?(Symbol) - @node_on_deck = arg - @return_obj = arg if @return_obj.nil? - elsif arg.is_a?(Hash) - end_args.push Hash[where_target => arg] unless arg.empty? - end - end - return end_args - end - - # Attempts to determine whether the passed string already contains a node/rel identifier or if it needs one prepended. - def process_string(arg, where_target) - @identifiers.include?(arg.split('.').first.to_sym) ? arg : "#{where_target}.#{arg}" - end - end - end -end From b8b97017de23d4b4125c8738001e90a83eee8d59 Mon Sep 17 00:00:00 2001 From: Chris Grigg Date: Wed, 13 Aug 2014 02:36:01 -0400 Subject: [PATCH 48/54] add each_with_rel and queryproxy unit spec file --- lib/neo4j/active_node/query/query_proxy.rb | 7 +++++ spec/unit/query_proxy_spec.rb | 30 ++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 spec/unit/query_proxy_spec.rb diff --git a/lib/neo4j/active_node/query/query_proxy.rb b/lib/neo4j/active_node/query/query_proxy.rb index 2db3e08bb..8984a3316 100644 --- a/lib/neo4j/active_node/query/query_proxy.rb +++ b/lib/neo4j/active_node/query/query_proxy.rb @@ -22,6 +22,13 @@ def each end end + def each_with_rel + raise "No relationship identifier specified" unless @rel_var + self.pluck((@node_var || :result), @rel_var).each do |obj, rel| + yield obj, rel + end + end + def ==(value) self.to_a == value end diff --git a/spec/unit/query_proxy_spec.rb b/spec/unit/query_proxy_spec.rb new file mode 100644 index 000000000..33e64c0ea --- /dev/null +++ b/spec/unit/query_proxy_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe Neo4j::ActiveNode::Query::QueryProxy do + let (:qp) { Neo4j::ActiveNode::Query::QueryProxy.new(Object) } + let (:session) { double("A session")} + let (:node) { double("A node object") } + let (:rel) { double("A rel object")} + + describe 'each_with_rel' do + it 'yields a node and rel object' do + qp.instance_variable_set(:@node_var, :n1) + qp.instance_variable_set(:@rel_var, :r1) + expect(qp).to receive(:pluck).with(:n1, :r1).and_return([node, rel]) + expect(qp.each_with_rel{|n, r| }).to eq [node, rel] + end + + it 'raises an error if there is no @rel_var' do + expect{qp.each_with_rel{|n, r|}}.to raise_error 'No relationship identifier specified' + end + end + + describe 'to_cypher' do + let(:query_result) { double("the result of calling :query")} + it 'calls query.to_cypher' do + expect(qp).to receive(:query).and_return(query_result) + expect(query_result).to receive(:to_cypher).and_return(String) + qp.to_cypher + end + end +end \ No newline at end of file From f749a7f50e29c2930c8dba04132fa1902e7c9789 Mon Sep 17 00:00:00 2001 From: Chris Grigg Date: Wed, 13 Aug 2014 14:00:13 -0400 Subject: [PATCH 49/54] QP always uses a rel identifier, add select_with_rel, tweak each, a few more specs --- lib/neo4j/active_node/query/query_proxy.rb | 32 ++++++++++++---- spec/unit/query_proxy_spec.rb | 43 ++++++++++++++++++---- 2 files changed, 60 insertions(+), 15 deletions(-) diff --git a/lib/neo4j/active_node/query/query_proxy.rb b/lib/neo4j/active_node/query/query_proxy.rb index 8984a3316..bf0fdfbe1 100644 --- a/lib/neo4j/active_node/query/query_proxy.rb +++ b/lib/neo4j/active_node/query/query_proxy.rb @@ -10,22 +10,33 @@ def initialize(model, association = nil, options = {}) @association = association @options = options @node_var = options[:node] - @rel_var = options[:rel] + @rel_var = options[:rel] || _rel_chain_var @session = options[:session] @chain = [] @params = {} end - def each - self.pluck(@node_var || :result).each do |obj| - yield obj + def each(rel = nil, &block) + if rel + self.pluck((@node_var || :result), @rel_var).each do |obj, rel| + yield obj, rel + end + else + self.pluck(@node_var || :result).each do |obj| + yield obj + end end end - def each_with_rel - raise "No relationship identifier specified" unless @rel_var - self.pluck((@node_var || :result), @rel_var).each do |obj, rel| - yield obj, rel + def each_with_rel(&block) + each(true, &block) + end + + def select_with_rel(&block) + if block_given? + each(true, &block).select(&block) + else + each(true){|n, r|}.select end end @@ -191,6 +202,10 @@ def _association_query_start(var) end end + def _rel_chain_var + :"rel#{_chain_level - 1}" + end + private def build_deeper_query_proxy(method, args) @@ -217,6 +232,7 @@ def links_for_where_arg(arg) if arg.is_a?(Hash) arg.map do |key, value| if @model && @model.has_association?(key) + neo_id = value.try(:neo_id) || value raise ArgumentError, "Invalid value for '#{key}' condition" if not neo_id.is_a?(Integer) diff --git a/spec/unit/query_proxy_spec.rb b/spec/unit/query_proxy_spec.rb index 33e64c0ea..3b683f084 100644 --- a/spec/unit/query_proxy_spec.rb +++ b/spec/unit/query_proxy_spec.rb @@ -3,19 +3,28 @@ describe Neo4j::ActiveNode::Query::QueryProxy do let (:qp) { Neo4j::ActiveNode::Query::QueryProxy.new(Object) } let (:session) { double("A session")} - let (:node) { double("A node object") } + let (:node) { double("A node object", foo: 'bar' ) } let (:rel) { double("A rel object")} describe 'each_with_rel' do it 'yields a node and rel object' do - qp.instance_variable_set(:@node_var, :n1) - qp.instance_variable_set(:@rel_var, :r1) - expect(qp).to receive(:pluck).with(:n1, :r1).and_return([node, rel]) + expect(qp).to receive(:pluck).and_return([node, rel]) expect(qp.each_with_rel{|n, r| }).to eq [node, rel] end + end + + describe 'select_with_rel' do + it 'passes true to :each and calls :select' do + expect(qp).to receive(:each).with(true).and_return([node, rel]) + expect(qp.select_with_rel.to_a).to eq [node, rel] + end - it 'raises an error if there is no @rel_var' do - expect{qp.each_with_rel{|n, r|}}.to raise_error 'No relationship identifier specified' + it 'selects pairs of objects that match the criteria' do + expect(qp).to receive(:each).exactly(2).times.with(true).and_return([[node, rel]]) + expect(node).to receive(:foo).exactly(2).times + expect(rel).not_to receive(:foo) + expect(qp.select_with_rel{|n, r| n.foo == 'bar' }).to eq [[node, rel]] + expect(qp.select_with_rel{|n, r| n.foo == 'foo'}).to eq [] end end @@ -27,4 +36,24 @@ qp.to_cypher end end -end \ No newline at end of file + + describe '_association_chain_var' do + context 'when missing start_object and query_proxy' do + it 'raises a crazy error' do + expect{qp.send(:_association_chain_var)}.to raise_error 'Crazy error' + end + + it 'needs a better error than "crazy error"' + end + end + + describe '_association_query_start' do + context 'when missing start_object and query_proxy' do + it 'raises a crazy error' do + expect{qp.send(:_association_query_start, nil)}.to raise_error 'Crazy error' + end + + it 'needs a better error than "crazy error"' + end + end +end From 44041b0de15656bb682b193234e47d6b75061085 Mon Sep 17 00:00:00 2001 From: Chris Grigg Date: Wed, 13 Aug 2014 15:35:17 -0400 Subject: [PATCH 50/54] remove old associations code and specs --- lib/neo4j.rb | 1 - lib/neo4j/active_node/has_n/decl_rel.rb | 252 ------------------------ spec/integration/has_n_spec.rb | 217 -------------------- spec/unit/decl_rel_spec.rb | 121 ------------ 4 files changed, 591 deletions(-) delete mode 100644 lib/neo4j/active_node/has_n/decl_rel.rb delete mode 100644 spec/integration/has_n_spec.rb delete mode 100644 spec/unit/decl_rel_spec.rb diff --git a/lib/neo4j.rb b/lib/neo4j.rb index 4d75dc91b..b015ede19 100644 --- a/lib/neo4j.rb +++ b/lib/neo4j.rb @@ -28,7 +28,6 @@ require 'neo4j/active_node/validations' require 'neo4j/active_node/rels' require 'neo4j/active_node/has_n' -require 'neo4j/active_node/has_n/decl_rel' require 'neo4j/active_node/has_n/association' require 'neo4j/active_node/has_n/nodes' require 'neo4j/active_node/query/query_proxy' diff --git a/lib/neo4j/active_node/has_n/decl_rel.rb b/lib/neo4j/active_node/has_n/decl_rel.rb deleted file mode 100644 index 8f7d13c94..000000000 --- a/lib/neo4j/active_node/has_n/decl_rel.rb +++ /dev/null @@ -1,252 +0,0 @@ -module Neo4j - module ActiveNode - module HasN - - - # A DSL for declared relationships has_n and has_one - # This DSL will be used to create accessor methods for relationships. - # Instead of using the 'raw' Neo4j::ActiveNode#rels method where one needs to know - # the name of relationship and direction one can use the generated accessor methods. - # - # @example - # - # class Folder - # include Neo4j::ActiveNode - # property :name - # # Declaring a Many relationship to any other node - # has_n(:files) - # end - # - # class File - # include Neo4j::ActiveNode - # # declaring a incoming relationship from Folder's relationship files - # has_one(:folder).from(Folder, :files) - # end - # - # The following methods will be generated: - # Folder#files :: returns an Enumerable of outgoing nodes for relationship 'files' - # Folder#files_rels :: returns an Enumerable of outgoing relationships for relationship 'files' - # File#folder :: for adding one node for the relationship 'files' from the outgoing Folder node - # File#folder_rel :: for accessing relationship 'files' from the outgoing Folder node - # File#folder :: for accessing nodes from relationship 'files' from the outgoing Folder node - # - class DeclRel - attr_reader :source_class, :dir, :rel_type, :method_id - - def initialize(method_id, has_one, source_class, *callbacks) - @method_id = method_id - @has_one = has_one - @dir = :outgoing - @rel_type = method_id.to_sym - @source_class = source_class - unless callbacks.empty? - @before_callback = callbacks.first[:before] || nil - @after_callback = callbacks.first[:after] || nil - end - end - - def inherit_new - base = self - dr = DeclRel.new(@method_id, @has_one, @source_class) - dr.instance_eval do - @dir = base.dir - @rel_type = base.rel_type - @target_name = base.target_name if base.target_name - @source_class = base.source_class - end - dr - end - - def to_s - "DeclRel one #{has_one?}, dir: #{@dir}, rel_id: #{@method_id}, rel_type: #{@rel_type}, target_class:#{@target_name}" - end - - def inspect - to_s - end - - # @return [true, false] - def has_one? - @has_one - end - - # @return [true, false] - def has_n? - !@has_one - end - - # @return [true,false] - def incoming? #:nodoc: - @dir == :incoming - end - - - # Declares an outgoing relationship type. - # It is possible to prefix relationship types so that it's possible to distinguish different incoming relationships. - # There is no validation that the added node is of the specified class. - # - # @example Example - # class FolderNode - # include Neo4j::ActiveNode - # has_n(:files).to(FileNode) - # has_one(:root).to("FileSystem") # also possible, if the class is not defined yet - # end - # - # folder = FolderNode.new - # # generate a relationship between folder and file of type 'FileNode#files' - # folder.files << FileNode.new - # - # @example relationship with a hash, user defined relationship - # - # class FolderNode - # include Neo4j::ActiveNode - # has_n(:files).to('FolderNode#files') - # end - # - # @example without prefix - # - # class FolderNode - # include Neo4j::ActiveNode - # has_n(:files).to(:contains) - # end - # - # file = FileNode.new - # # create an outgoing relationship of type 'contains' from folder node to file - # folder.files << FolderNode.new - # - # @param [Class, String, Symbol] target the other class to which this relationship goes (if String or Class) or the relationship (if Symbol) - # @param [String, Symbol] rel_type the rel_type postfix for the relationships, which defaults to the same as the has_n/one method id - # @return self - def to(target, rel_type = @method_id) - @dir = :outgoing - - case target - when /#/ - @target_name, _ = target.to_s.split("#") - @rel_type = target.to_sym - when Class, String - @target_name = target.to_s - @rel_type = "#{@source_class}##{rel_type}".to_sym - when Symbol - @target_name = nil - @rel_type = target.to_sym - else - raise "Expected a class or a symbol for, got #{target}/#{target.class}" - end - self - end - - # Specifies an incoming relationship. - # Will use the outgoing relationship given by the from class. - # - # @example with prefix FileNode - # class FolderNode - # include Neo4j::NodeMixin - # has_n(:files).to(FileNode) - # end - # - # class FileNode - # include Neo4j::NodeMixin - # # will only traverse any incoming relationship of type files from node FileNode - # has_one(:folder).from(FolderNode.files) - # # alternative: has_one(:folder).from(FolderNode, :files) - # end - # - # file = FileNode.new - # # create an outgoing relationship of type 'FileNode#files' from folder node to file (FileNode is the prefix). - # file.folder = FolderNode.new - # - # @example without prefix - # - # class FolderNode - # include Neo4j::NodeMixin - # has_n(:files) - # end - # - # class FileNode - # include Neo4j::NodeMixin - # has_one(:folder).from(:files) # will traverse any incoming relationship of type files - # end - # - # file = FileNode.new - # # create an outgoing relationship of type 'files' from folder node to file - # file.folder = FolderNode.new - # - # - def from(target, rel_type=@method_id) - @dir = :incoming - - case target - when /#/ - @target_name, _ = target.to_s.split("#") - @rel_type = target - when Class, String - @target_name = target.to_s - @rel_type = "#{@target_name}##{rel_type}".to_sym - when Symbol - @target_name = nil - @rel_type = target.to_sym - else - raise "Expected a class or a symbol for, got #{target}/#{target.class}" - end - self - end - - - # @private - def target_name - @target_name - end - - def target_class - @target_name && @target_name.split("::").inject(Kernel) { |container, name| container.const_get(name.to_s) } - end - - - # @private - def each_node(node, &block) - node.nodes(dir: dir, type: rel_type).each { |n| block.call(n) } - end - - def all_relationships(node) - to_enum(:each_rel, node) - end - - def each_rel(node, &block) #:nodoc: - node.rels(dir: dir, type: rel_type).each { |rel| block.call(rel) } - end - - def single_relationship(node) - node.rel(dir: dir, type: rel_type) - end - - def single_node(node) - node.node(dir: dir, type: rel_type) - end - - # @private - def create_relationship_to(node, other, relationship_props={}) # :nodoc: - from, to = incoming? ? [other, node] : [node, other] - before_callback_result = do_before_callback(node, from, to) - return false if before_callback_result == false - - result = from.create_rel(@rel_type, to, relationship_props) - - after_callback_result = do_after_callback(node, from, to) - after_callback_result == false ? false : result - end - - private - - def do_before_callback(caller, from, to) - @before_callback ? caller.send(@before_callback, from, to) : true - end - - def do_after_callback(caller, from, to) - @after_callback ? caller.send(@after_callback, from, to) : true - end - - end - end - end -end diff --git a/spec/integration/has_n_spec.rb b/spec/integration/has_n_spec.rb deleted file mode 100644 index d38fbae6a..000000000 --- a/spec/integration/has_n_spec.rb +++ /dev/null @@ -1,217 +0,0 @@ -# require 'spec_helper' - -# describe "has_n" do - -# let(:clazz) do -# UniqueClass.create do -# include Neo4j::ActiveNode -# end -# end - -# let(:other_clazz) do -# UniqueClass.create do -# include Neo4j::ActiveNode -# end -# end - -# describe '#_decl_rels' do -# it 'is a Hash' do -# #clazz.has_n :friends -# clazz._decl_rels.should be_a(Hash) -# end - -# context 'when inherited' do -# class TestHasNBase -# include Neo4j::ActiveNode -# has_n :knows -# end - -# class TestHasNSub < TestHasNBase - -# end - -# it 'inherit declared has_n' do -# TestHasNSub._decl_rels[:knows].should be_a(Neo4j::ActiveNode::HasN::DeclRel) -# end - -# it 'impl has_n accessor methods' do -# node = TestHasNSub.new -# node.should respond_to(:knows) -# node.should respond_to(:knows_rels) -# end -# end -# end - -# describe 'has_n(:friends)' do -# before do -# clazz.has_n :friends -# end - -# let(:core_node) do -# double("core node", props: {}) -# end - -# let(:node) do -# session.should_receive(:create_node).and_return(core_node) -# clazz.create -# end - - -# let(:session) do -# session = double("Mock Session") -# Neo4j::Session.stub(:current).and_return(session) -# session -# end - -# describe 'clazz.friends' do -# subject { clazz.friends } -# it { should eq(:friends)} -# end - -# describe 'node.friends << a_node' do - -# it 'creates a new relationship' do -# a_node = double("a node") - -# node.should_receive(:create_rel).with(:friends, a_node, {}) - -# # when -# node.friends << a_node -# end -# end - -# describe 'node.friends = [a_node, b_node]' do - -# it 'creates a new relationship' do -# a_node = double("a node") -# b_node = double("b node") - -# node.should_receive(:rels).with({:dir=>:outgoing, :type=>:friends}).and_return([]) - -# node.should_receive(:create_rel).with(:friends, a_node, {}) -# node.should_receive(:create_rel).with(:friends, b_node, {}) - -# # when -# node.friends = [a_node, b_node] -# end -# end - -# describe 'node.friends.to_a' do - -# it 'traverse correct relationships' do -# core_node.should_receive(:nodes).with(dir: :outgoing, type: :friends).and_return([]) -# node.friends.to_a.should eq([]) -# end - -# it 'can return wrapped nodes' do -# friend_node_wrapper = double("friend node wrapper") -# core_node.should_receive(:nodes).with(dir: :outgoing, type: :friends).and_return([friend_node_wrapper]) -# node.friends.to_a.should eq([friend_node_wrapper]) -# end -# end - -# describe '_decl_rels[:friends]' do -# subject do -# clazz._decl_rels[:friends] -# end - -# it { should be_a(Neo4j::ActiveNode::HasN::DeclRel)} -# its(:dir) { should eq(:outgoing)} -# its(:source_class) { should eq(clazz)} -# its(:rel_type) { should eq(:friends)} -# end -# end - - -# describe 'has_n(:friends).to(OtherClass)' do -# before do -# clazz.has_n(:friends).to(other_clazz) -# end - -# describe 'clazz.friends' do -# subject { clazz.friends } -# it { should eq(:"#{clazz}#friends")} -# end - -# describe '_decl_rels[:friends]' do -# subject do -# clazz._decl_rels[:friends] -# end - -# it { should be_a(Neo4j::ActiveNode::HasN::DeclRel) } -# its(:dir) { should eq(:outgoing) } -# its(:source_class) { should eq(clazz) } -# its(:rel_type) { should eq(:"#{clazz}#friends") } -# end -# end - -# describe 'has_n(:known_by).from(OtherClass)' do -# before do -# clazz.has_n(:known_by).from(other_clazz) -# end - -# describe 'clazz.known_by' do -# subject { clazz.known_by } -# it { should eq(:"#{other_clazz}#known_by")} -# end - -# describe '_decl_rels[:known_by]' do -# subject do -# clazz._decl_rels[:known_by] -# end - -# it { should be_a(Neo4j::ActiveNode::HasN::DeclRel) } -# its(:dir) { should eq(:incoming) } -# its(:source_class) { should eq(clazz) } -# its(:rel_type) { should eq(:"#{other_clazz}#known_by") } -# end - -# end - -# describe 'has_n(:known_by).from(OtherClass, :knows)' do -# before do -# clazz.has_n(:known_by).from(other_clazz, :knows) -# end - -# describe 'clazz.known_by' do -# subject { clazz.known_by } -# it { should eq(:"#{other_clazz}#knows")} -# end - -# describe '_decl_rels[:known_by]' do -# subject do -# clazz._decl_rels[:known_by] -# end - -# it { should be_a(Neo4j::ActiveNode::HasN::DeclRel) } -# its(:dir) { should eq(:incoming) } -# its(:source_class) { should eq(clazz) } -# its(:rel_type) { should eq(:"#{other_clazz}#knows") } -# end - -# end - -# describe 'has_n(:known_by).from(:"OtherClass#knows")' do -# before do -# clazz.has_n(:known_by).from(:"OtherClass#knows") -# end - -# describe 'clazz.known_by' do -# subject { clazz.known_by } -# it { should eq(:"OtherClass#knows")} -# end - -# describe '_decl_rels[:known_by]' do -# subject do -# clazz._decl_rels[:known_by] -# end - -# it { should be_a(Neo4j::ActiveNode::HasN::DeclRel) } -# its(:dir) { should eq(:incoming) } -# its(:source_class) { should eq(clazz) } -# its(:rel_type) { should eq(:"OtherClass#knows") } -# end - -# end - -# end diff --git a/spec/unit/decl_rel_spec.rb b/spec/unit/decl_rel_spec.rb deleted file mode 100644 index bc90e7afe..000000000 --- a/spec/unit/decl_rel_spec.rb +++ /dev/null @@ -1,121 +0,0 @@ -require 'spec_helper' - -describe Neo4j::ActiveNode::HasN::DeclRel do - let(:person_class) {UniqueClass.create } #('Person')} - let(:company_class) {UniqueClass.create } #('Company')} - let(:folder_class) {UniqueClass.create } #('FolderNode')} - let(:files_class) {UniqueClass.create } #('FileNode')} - - describe 'initialize' do - - it 'sets default direction :outgoing' do - dr = Neo4j::ActiveNode::HasN::DeclRel.new(:friends, false, person_class) - dr.dir.should eq(:outgoing) - end - - it 'sets source_class' do - dr = Neo4j::ActiveNode::HasN::DeclRel.new(:friends, false, person_class) - dr.source_class.should eq(person_class) - end - - it 'to_s method works' do - Neo4j::ActiveNode::HasN::DeclRel.new(:friends, false, person_class).to_s.should be_a(String) - end - end - - describe 'to' do - - context 'to(:friends)' do - subject do - Neo4j::ActiveNode::HasN::DeclRel.new(:friends, false, person_class).to(:friends) - end - its(:dir) { should eq(:outgoing)} - its(:rel_type) { should eq(:friends)} - its(:target_name) { should be_nil} - its(:source_class) { should eq(person_class)} - end - - context 'to(Company)' do - subject do - Neo4j::ActiveNode::HasN::DeclRel.new(:friends, false, person_class).to(company_class) - end - its(:dir) { should eq(:outgoing)} - its(:rel_type) { should eq(:"#{person_class}#friends")} - its(:target_name) { should eq(company_class.to_s)} - its(:target_class) { should eq(company_class)} - its(:source_class) { should eq(person_class)} - end - - context 'to("Company")' do - subject do - Neo4j::ActiveNode::HasN::DeclRel.new(:friends, false, person_class).to(company_class.to_s) - end - its(:dir) { should eq(:outgoing)} - its(:rel_type) { should eq(:"#{person_class}#friends")} - its(:target_name) { should eq(company_class.to_s)} - its(:target_class) { should eq(company_class)} - its(:source_class) { should eq(person_class)} - end - - context 'to("Company", :knows)' do - subject do - Neo4j::ActiveNode::HasN::DeclRel.new(:friends, false, person_class).to(company_class.to_s, :knows) - end - its(:dir) { should eq(:outgoing)} - its(:rel_type) { should eq(:"#{person_class}#knows")} - its(:target_name) { should eq(company_class.to_s)} - its(:target_class) { should eq(company_class)} - its(:source_class) { should eq(person_class)} - end - - context 'to("Company#knows")' do - subject do - Neo4j::ActiveNode::HasN::DeclRel.new(:friends, false, person_class).to("#{company_class.to_s}#knows") - end - its(:dir) { should eq(:outgoing)} - its(:rel_type) { should eq(:"#{company_class}#knows")} - its(:target_name) { should eq(company_class.to_s)} - its(:target_class) { should eq(company_class)} - its(:source_class) { should eq(person_class)} - end - - end - - describe 'from' do - context 'FileNode.has_one(:folder).from(:files)' do - # FileNode.has_one(:folder).from(:files) # will traverse any incoming relationship of type files - subject do - Neo4j::ActiveNode::HasN::DeclRel.new(:folder, true, files_class).from(:files) - end - its(:dir) { should eq(:incoming)} - its(:rel_type) { should eq(:files)} - its(:target_name) { should be_nil} - its(:target_class) { should be_nil} - its(:source_class) { should eq(files_class)} - end - - context 'FileNode.has_one(:folder).from(Folder, :files)' do - subject do - Neo4j::ActiveNode::HasN::DeclRel.new(:folder, true, files_class).from(folder_class, :files) - end - its(:dir) { should eq(:incoming)} - its(:rel_type) { should eq(:"#{folder_class}#files")} - its(:target_name) { should eq(folder_class.to_s)} - its(:target_class) { should eq(folder_class)} - its(:source_class) { should eq(files_class)} - end - - context 'from(:friends)' do - subject do - Neo4j::ActiveNode::HasN::DeclRel.new(:friends, false, person_class).from(:friends) - end - its(:dir) { should eq(:incoming)} - its(:rel_type) { should eq(:friends)} - its(:target_name) { should be_nil} - its(:target_class) { should be_nil} - its(:source_class) { should eq(person_class)} - end - - end -end - From e28b7e4564e69920960efa3e0cbaec1e4e76fc5a Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Wed, 13 Aug 2014 21:51:53 +0100 Subject: [PATCH 51/54] Move activesupport dependency declaration to gemspec --- Gemfile | 2 -- neo4j.gemspec | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index 2abe09535..cec263199 100644 --- a/Gemfile +++ b/Gemfile @@ -8,8 +8,6 @@ gem 'neo4j-core', :git => 'https://github.com/andreasronge/neo4j-core.git' gem 'coveralls', require: false -gem 'activesupport' - group 'development' do gem 'pry' gem 'os' # for neo4j-server rake task diff --git a/neo4j.gemspec b/neo4j.gemspec index f8436485f..5bad7f828 100644 --- a/neo4j.gemspec +++ b/neo4j.gemspec @@ -31,6 +31,7 @@ It comes included with the Apache Lucene document database. s.add_dependency('orm_adapter', "~> 0.5.0") s.add_dependency("activemodel", "~> 4") + s.add_dependency("activesupport", "~> 4") s.add_dependency("railties", "~> 4") s.add_dependency('active_attr', "~> 0.8") s.add_dependency("neo4j-core", "= 3.0.0.alpha.18") From d3394a336c0f71fd7e1c2c3b512b5c86c4b03dad Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Wed, 13 Aug 2014 22:51:09 +0100 Subject: [PATCH 52/54] Rename spec file from has_n to has_many --- spec/e2e/has_many_spec.rb | 216 ++++++++++++++++++++++++++++++++++++++ spec/e2e/has_n_spec.rb | 8 +- 2 files changed, 220 insertions(+), 4 deletions(-) create mode 100644 spec/e2e/has_many_spec.rb diff --git a/spec/e2e/has_many_spec.rb b/spec/e2e/has_many_spec.rb new file mode 100644 index 000000000..a216f642e --- /dev/null +++ b/spec/e2e/has_many_spec.rb @@ -0,0 +1,216 @@ +require 'spec_helper' + +describe 'has_n' do + + let(:clazz_b) do + UniqueClass.create do + include Neo4j::ActiveNode + end + end + + let(:clazz_a) do + #knows_type = clazz_b + UniqueClass.create do + include Neo4j::ActiveNode + property :name + + has_many :both, :friends, model_class: false + has_many :out, :knows, model_class: self + has_many :in, :knows_me, origin: :knows, model_class: self + end + end + + let(:node) { clazz_a.create } + let(:friend1) { clazz_a.create } + let(:friend2) { clazz_a.create } + + describe 'rel_type' do + it 'creates the correct type' do + node.friends << friend1 + r = node.rel + expect(r.rel_type).to eq(:'#friends') + end + + it 'creates the correct type' do + node.knows << friend1 + r = node.rel + expect(r.rel_type).to eq(:'#knows') + end + + it 'creates correct incoming relationship' do + node.knows_me << friend1 + expect(friend1.rel(dir: :outgoing).rel_type).to eq(:'#knows') + expect(node.rel(dir: :incoming).rel_type).to eq(:'#knows') + end + end + + it 'access nodes via declared has_n method' do + expect(node.friends.to_a).to eq([]) + expect(node.friends.any?()).to be false + + node.friends << friend1 + expect(node.friends.to_a).to eq([friend1]) + end + + it 'access relationships via declared has_n method' do + node.friends_rels.to_a.should eq([]) + node.friends << friend1 + rels = node.friends_rels + rels.count.should == 1 + rel = rels.first + rel.start_node.should == node + rel.end_node.should == friend1 + end + + describe 'me.friends << friend_1 << friend' do + it 'creates several relationships' do + node.friends << friend1 << friend2 + node.friends.to_a.should =~ [friend1, friend2] + end + end + + describe 'me.friends = ' do + it 'creates several relationships' do + node.friends = [friend1, friend2] + node.friends.to_a.should =~ [friend1, friend2] + end + + context 'node with two friends' do + before(:each) do + node.friends = [friend1, friend2] + end + + it 'is not empty' do + expect(node.friends.any?()).to be true + end + + it 'removes relationships when given a different list' do + friend3 = clazz_a.create + node.friends = [friend3] + node.friends.to_a.should =~ [friend3] + end + + it 'removes relationships when given a partial list' do + node.friends = [friend1] + node.friends.to_a.should =~ [friend1] + end + + it 'removes all relationships when given an empty list' do + node.friends = [] + node.friends.to_a.should =~ [] + end + + it 'can be accessed via [] operator' do + expect([friend1, friend2]).to include(node.friends[0]) + end + + it 'has a to_s method' do + expect(node.friends.to_s).to be_a(String) + end + + it 'has a is_a method' do + expect(node.friends.is_a?(Neo4j::ActiveNode::Query::QueryProxy)).to be true + expect(node.friends.is_a?(Array)).to be false + expect(node.friends.is_a?(String)).to be false + end + end + end + + describe 'me.friends#create(other, since: 1994)' do + describe "creating relationships to existing nodes" do + it 'creates a new relationship when given existing nodes and given properties' do + node.friends.create(friend1, since: 1994) + + r = node.rel(dir: :outgoing, type: '#friends') + + r[:since].should eq(1994) + end + + it 'creates new relationships when given an array of nodes and given properties' do + node.friends.create([friend1, friend2], since: 1995) + + rs = node.rels(dir: :outgoing, type: '#friends') + + rs.map(&:end_node).should =~ [friend1, friend2] + rs.each do |r| + r[:since].should eq(1995) + end + end + end + + describe "creating relationships and nodes at the same time" do + let(:node2) { double("unpersisted node", props: { name: 'Brad' } )} + + it 'creates a new relationship when given unpersisted node and given properties' do + node.friends.create(clazz_a.new(name: 'Brad'), {since: 1996}) + #node2.stub(:persisted?).and_return(false) + #node2.stub(:save).and_return(true) + #node2.stub(:neo_id).and_return(2) + + #node.friends.create(node2, since: 1996) + r = node.rel(dir: :outgoing, type: '#friends') + + r[:since].should eq(1996) + r.end_node.name.should == 'Brad' + end + + it 'creates a new relationship when given an array of unpersisted nodes and given properties' do + node.friends.create([clazz_a.new(name: 'James'), clazz_a.new(name: 'Cat')], {since: 1997}) + + rs = node.rels(dir: :outgoing, type: '#friends') + + rs.map(&:end_node).map(&:name).should =~ ['James', 'Cat'] + rs.each do |r| + r[:since].should eq(1997) + end + end + end + end + + + describe 'callbacks' do + let(:clazz_c) do + #knows_type = clazz_b + UniqueClass.create do + include Neo4j::ActiveNode + property :name + + has_many :out, :knows, model_class: self, before: :before_callback + has_many :in, :knows_me, origin: :knows, model_class: self, after: :after_callback + has_many :in, :will_fail, origin: :knows, model_class: self, before: :false_callback + + def before_callback(other) + end + + def after_callback(other) + end + + def false_callback(other) + false + end + end + end + + let(:node) { clazz_a.create } + let(:friend1) { clazz_a.create } + let(:friend2) { clazz_a.create } + + let(:callback_friend1) { clazz_c.create } + let(:callback_friend2) { clazz_c.create } + + it 'calls before_callback when node added to #knows association' do + expect(callback_friend1).to receive(:before_callback).with(callback_friend2) { callback_friend1.knows.to_a.size.should == 0 } + callback_friend1.knows << callback_friend2 + end + + it 'calls after_callback when node added to #knows association' do + expect(callback_friend1).to receive(:after_callback).with(callback_friend2) { callback_friend2.knows.to_a.size.should == 1 } + callback_friend1.knows_me << callback_friend2 + end + + it 'prevents the association from being created if before returns "false" explicitly' do + callback_friend1.will_fail << callback_friend2 + expect(callback_friend1.knows_me.to_a.size).to eq 0 + end + end +end \ No newline at end of file diff --git a/spec/e2e/has_n_spec.rb b/spec/e2e/has_n_spec.rb index a216f642e..635c31ac7 100644 --- a/spec/e2e/has_n_spec.rb +++ b/spec/e2e/has_n_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'has_n' do +describe 'has_many' do let(:clazz_b) do UniqueClass.create do @@ -44,7 +44,7 @@ end end - it 'access nodes via declared has_n method' do + it 'access nodes via declared has_many method' do expect(node.friends.to_a).to eq([]) expect(node.friends.any?()).to be false @@ -52,7 +52,7 @@ expect(node.friends.to_a).to eq([friend1]) end - it 'access relationships via declared has_n method' do + it 'access relationships via declared has_many method' do node.friends_rels.to_a.should eq([]) node.friends << friend1 rels = node.friends_rels @@ -213,4 +213,4 @@ def false_callback(other) expect(callback_friend1.knows_me.to_a.size).to eq 0 end end -end \ No newline at end of file +end From 453d3e9ceba0a3410e0c7717cf9b4e91decbe86b Mon Sep 17 00:00:00 2001 From: Chris Grigg Date: Wed, 13 Aug 2014 18:24:12 -0400 Subject: [PATCH 53/54] each_with rel and each_rel changes --- lib/neo4j/active_node/query/query_proxy.rb | 19 ++++---- spec/unit/query_proxy_spec.rb | 56 +++++++++++++++++----- 2 files changed, 52 insertions(+), 23 deletions(-) diff --git a/lib/neo4j/active_node/query/query_proxy.rb b/lib/neo4j/active_node/query/query_proxy.rb index bf0fdfbe1..7e9f810c7 100644 --- a/lib/neo4j/active_node/query/query_proxy.rb +++ b/lib/neo4j/active_node/query/query_proxy.rb @@ -16,28 +16,25 @@ def initialize(model, association = nil, options = {}) @params = {} end - def each(rel = nil, &block) - if rel + def each(node = true, rel = nil, &block) + if node && rel self.pluck((@node_var || :result), @rel_var).each do |obj, rel| yield obj, rel end else - self.pluck(@node_var || :result).each do |obj| + pluck_this = !rel ? (@node_var || :result) : @rel_var + self.pluck(pluck_this).each do |obj| yield obj end end end - def each_with_rel(&block) - each(true, &block) + def each_rel(&block) + block_given? ? each(false, true, &block) : to_enum(:each, false, true) end - def select_with_rel(&block) - if block_given? - each(true, &block).select(&block) - else - each(true){|n, r|}.select - end + def each_with_rel(&block) + block_given? ? each(true, true, &block) : to_enum(:each, true, true) end def ==(value) diff --git a/spec/unit/query_proxy_spec.rb b/spec/unit/query_proxy_spec.rb index 3b683f084..0ea26842a 100644 --- a/spec/unit/query_proxy_spec.rb +++ b/spec/unit/query_proxy_spec.rb @@ -13,18 +13,50 @@ end end - describe 'select_with_rel' do - it 'passes true to :each and calls :select' do - expect(qp).to receive(:each).with(true).and_return([node, rel]) - expect(qp.select_with_rel.to_a).to eq [node, rel] - end - - it 'selects pairs of objects that match the criteria' do - expect(qp).to receive(:each).exactly(2).times.with(true).and_return([[node, rel]]) - expect(node).to receive(:foo).exactly(2).times - expect(rel).not_to receive(:foo) - expect(qp.select_with_rel{|n, r| n.foo == 'bar' }).to eq [[node, rel]] - expect(qp.select_with_rel{|n, r| n.foo == 'foo'}).to eq [] + describe 'each_rel' do + context 'without a block' do + it 'calls to_enum, sends :each with node false, rel true' do + expect(qp).to receive(:to_enum).with(:each, false, true) + qp.each_rel + end + end + + context 'with a block' do + it 'sends the block to :each with node false, rel true' do + expect(qp).not_to receive(:to_enum) + expect(qp).to receive(:each).with(false, true) + qp.each_rel{|r| } + end + + it 'calls pluck and executes the block' do + expect(qp).to receive(:pluck).and_return([rel]) + expect(rel).to receive(:name) + qp.each_rel{|r| r.name } + end + end + end + + describe 'each_with_rel' do + context 'without a block' do + it 'calls to_enum, sends :each with node true, rel true' do + expect(qp).to receive(:to_enum).with(:each, true, true) + qp.each_with_rel + end + end + + context 'with a block' do + it 'sends the block to :each with node true, rel true' do + expect(qp).not_to receive(:to_enum) + expect(qp).to receive(:each).with(true, true) + qp.each_with_rel{|n, r| } + end + + it 'calls pluck and executes the block' do + expect(qp).to receive(:pluck).and_return([node, rel]) + expect(node).to receive(:name) + expect(rel).to receive(:name) + qp.each_with_rel{|n, r| n.name and r.name } + end end end From 1dfbc5e331df3a4aa55f68f1ee4135a79c7f89f6 Mon Sep 17 00:00:00 2001 From: Chris Grigg Date: Wed, 13 Aug 2014 22:25:25 -0400 Subject: [PATCH 54/54] fix origin --- lib/neo4j/active_node/has_n/association.rb | 22 +++++++++++++--------- spec/unit/association_spec.rb | 12 ++++++++++++ 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/lib/neo4j/active_node/has_n/association.rb b/lib/neo4j/active_node/has_n/association.rb index b92ead7e4..59943d245 100644 --- a/lib/neo4j/active_node/has_n/association.rb +++ b/lib/neo4j/active_node/has_n/association.rb @@ -62,6 +62,16 @@ def perform_callback(caller, other_node, type) caller.send(callback(type), other_node) end + def relationship_type(create = false) + if @relationship_type + @relationship_type + elsif @origin + origin_type + else + (create || exceptional_target_class?) && "##{@name}" + end + end + private def get_direction(relationship_cypher, create) @@ -86,15 +96,9 @@ def get_properties_string(properties) end.join(', ') p.size == 0 ? '' : " {#{p}}" end - - def relationship_type(create = false) - if @relationship_type - @relationship_type - elsif @origin - "##{@origin}" - else - (create || exceptional_target_class?) && "##{@name}" - end + + def origin_type + target_class.associations[@origin].relationship_type end # Return basic details about association as declared in the model diff --git a/spec/unit/association_spec.rb b/spec/unit/association_spec.rb index 79ac68a6b..32fd26e5e 100644 --- a/spec/unit/association_spec.rb +++ b/spec/unit/association_spec.rb @@ -132,6 +132,18 @@ class Default end + describe 'origin_type' do + let(:start) { Neo4j::ActiveNode::HasN::Association.new(:has_many, :in, 'name') } + let(:myclass) { double("another activenode class") } + let(:myassoc) { double("an association object" )} + let(:assoc_details) { double("the result of calling :associations", relationship_type: 'MyRel')} + it 'examines the specified association to determine type' do + expect(start).to receive(:target_class).and_return(myclass) + expect(myclass).to receive(:associations).and_return(myassoc) + expect(myassoc).to receive(:[]).and_return(assoc_details) + expect(start.send(:origin_type)).to eq 'MyRel' + end + end end