diff --git a/README.md b/README.md index fa5416e..28bd921 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,9 @@ To implement a model using Deja, inherit from Deja::Node class Person < Deja::Node attr_accessor :name, :permalink, :type - relationship :invested_in - relationship :friends - relationship :hates + relationship :invested_in, :out => investment, :in => investor + relationship :friends, :out => friend + relationship :hates, :out => hates end ``` Relationship Structure: @@ -49,7 +49,7 @@ Interface: ### Loading Nodes: To load a node with a given id, use the **find** method: ```ruby - Person.find(3) + Person.find(3, :include => :none) # does not include any related nodes ``` To load a person with a given id, and eager load a specific relationship, use the **:include** option: ```ruby @@ -93,6 +93,30 @@ By default Deja supports lazy loading. To load a given relationship on the fly, end ``` +### Count: +To count the number of related nodes without actually fetching them, call the count method passing in the name of the relationship alias as an argument. + ```ruby + node.count(:investments) # returns the total count of all investments + ``` + +### Order: +To order by a given property on end nodes of a relationship, pass an order option into the relationship alias method. + ```ruby + node.investments(:order => 'name ASC') # returns the related nodes ordered by name + ``` + +### Limit: +To limit the results of relationship load query, pass in a limit argument. + ```ruby + node.investments(:limit => 10) # returns only the first 10 investments + ``` + +### Offset: +To offset the results of relationship load query, pass in a offset argument. + ```ruby + node.investments(:offset => 5) # returns all investments offset by the first 5 + ``` + ### Index Methods: Deja allows you to create indexes for both nodes and relationships. ```ruby @@ -102,7 +126,7 @@ Deja allows you to create indexes for both nodes and relationships. Deja also supports finding by index ```ruby - Person.find_by_index('idx_Person', :permalink, 'john_smith') + Person.find({:index => 'idx_Person', :key => :permalink, :value => 'john_smith'}) ``` And for relationships ```ruby diff --git a/lib/deja/bridge.rb b/lib/deja/bridge.rb index 5ec14e5..7af27d9 100644 --- a/lib/deja/bridge.rb +++ b/lib/deja/bridge.rb @@ -27,9 +27,33 @@ def rel(id, context, return_root = true) end end - def attach_filter(result, filter = nil) - result.where{|n| n[:type] == filter.to_s.camelize} if filter - result + def apply_options(context, options = {}) + context = filter(context, options[:filter]) if options[:filter] + context = order(context, options[:order]) if options[:order] + context = limit(context, options[:limit]) if options[:limit] + context = skip(context, options[:offset]) if options[:offset] + context + end + + def filter(context, filter) + context.where{|n| n[:type] == filter.to_s.camelize} + end + + def order(context, order_string) + property, order = order_string.split(' ') + if order == 'ASC' + context.asc(property.to_sym) + else + context.desc(property.to_sym) + end + end + + def limit(context, size) + context.limit(size) + end + + def skip(context, offset) + context.skip(offset) end def create_node(attributes = {}) @@ -39,7 +63,7 @@ def create_node(attributes = {}) end def delete_node(id) - cypher{ + cypher { Deja::Bridge.node(id, self, false).del.both(rel().as(:r).del) } end @@ -82,36 +106,40 @@ def delete_relationship(id) } end - def get_related_nodes(id, opts = {}) + def get_nodes(id, opts = {}) + return single_node(id) if opts[:include] == :none opts[:direction] ||= :both - opts[:filter] ||= nil rels = opts[:include] == :all ? nil : opts[:include] case opts[:direction] - when :out then outgoing_rel(id, rels, opts[:return_root], opts[:filter]) - when :in then incoming_rel(id, rels, opts[:return_root], opts[:filter]) - when :both then in_out_rel(id, rels, opts[:return_root], opts[:filter]) + when :out then outgoing_rel(id, rels, opts[:return_root], opts) + when :in then incoming_rel(id, rels, opts[:return_root], opts) + when :both then in_out_rel(id, rels, opts[:return_root], opts) else false end end - def outgoing_rel(id, rels = nil, root = nil, filter = nil) + def single_node(id) + cypher { Deja::Bridge.node(id, self, true) } + end + + def outgoing_rel(id, rels = nil, root = nil, opts = nil) cypher { r = Deja::Bridge.node(id, self, root).outgoing(rel(*rels).ret) - ret Deja::Bridge.attach_filter(r, filter) + ret Deja::Bridge.apply_options(r, opts) } end - def incoming_rel(id, rels = nil, root = nil, filter = nil) + def incoming_rel(id, rels = nil, root = nil, opts = nil) cypher { r = Deja::Bridge.node(id, self, root).incoming(rel(*rels).ret) - ret Deja::Bridge.attach_filter(r, filter) + ret Deja::Bridge.apply_options(r, opts) } end - def in_out_rel(id, rels = nil, root = nil, filter = nil) + def in_out_rel(id, rels = nil, root = nil, opts = nil) cypher { r = Deja::Bridge.node(id, self, root).both(rel(*rels).ret) - ret Deja::Bridge.attach_filter(r, filter) + ret Deja::Bridge.apply_options(r, opts) } end diff --git a/lib/deja/finders.rb b/lib/deja/finders.rb index f767652..96ff1d3 100644 --- a/lib/deja/finders.rb +++ b/lib/deja/finders.rb @@ -2,17 +2,13 @@ module Deja module Finders extend ActiveSupport::Concern module ClassMethods - def find_by_index(index, key, value, options = {}) - option_query { Deja::Query.load_node({:index => index, :key => key, :value => value}, options) } - end - - def find_by_neo_id(neo_id, options = {}) - option_query { Deja::Query.load_node(neo_id, options) } + def find(id, options = {}) + option_query { Deja::Query.load_node(id, options) } end def where(key, value, options = {}) options[:include] ||= :all - find_by_index("idx_#{self.name}", key, value, options) + find({:index => "idx_#{self.name}", :key => key, :value => value}, options) end private diff --git a/lib/deja/node.rb b/lib/deja/node.rb index 3211570..06dbe7e 100644 --- a/lib/deja/node.rb +++ b/lib/deja/node.rb @@ -53,8 +53,8 @@ def initialize(*args) def define_alias_methods(rel, aliases) self.class_eval do if aliases[:out_plural] and aliases[:out_singular] - define_method aliases[:out_plural] do |filter = nil| - send(:related_nodes, {:include => rel, :direction => :out, :filter => filter}) + define_method aliases[:out_plural] do |opts = {}| + send(:related_nodes, {:include => rel, :direction => :out}.merge(opts)) instance_variable_get("@#{rel}") end @@ -74,8 +74,8 @@ def define_alias_methods(rel, aliases) end if aliases[:in_plural] and aliases[:in_singular] - define_method aliases[:in_plural] do |filter = nil| - send(:related_nodes, {:include => rel, :direction => :in, :filter => filter}) + define_method aliases[:in_plural] do |opts = {}| + send(:related_nodes, {:include => rel, :direction => :in}.merge(opts)) instance_variable_get("@#{rel}") end diff --git a/lib/deja/query.rb b/lib/deja/query.rb index 54f3d0a..f18e3c9 100644 --- a/lib/deja/query.rb +++ b/lib/deja/query.rb @@ -4,11 +4,17 @@ class Query class << self def load_node(neo_id, options = {}) - load_node_with_args(neo_id, options) + options[:return_root] ||= true + cypher_query = Deja::Bridge.get_nodes(neo_id, options) + result_hash = Deja.execute_cypher(cypher_query) + normalize(result_hash) end def load_related_nodes(neo_id, options = {}) - load_related_nodes_with_args(neo_id, options) + options[:return_root] ||= false + cypher_query = Deja::Bridge.get_nodes(neo_id, options) + result_hash = Deja.execute_cypher(cypher_query) + normalize(result_hash, :lazy) end def create_node(attributes = {}) @@ -62,20 +68,6 @@ def update_relationship(rel_id, attributes = {}) result_hash = Deja.execute_cypher(cypher_query) result_hash["data"].empty? ? false : true end - - def load_node_with_args(neo_id, options) - options[:return_root] ||= true - cypher_query = Deja::Bridge.get_related_nodes(neo_id, options) - result_hash = Deja.execute_cypher(cypher_query) - normalize(result_hash) - end - - def load_related_nodes_with_args(neo_id, options) - options[:return_root] ||= false - cypher_query = Deja::Bridge.get_related_nodes(neo_id, options) - result_hash = Deja.execute_cypher(cypher_query) - normalize(result_hash, :lazy) - end end end end diff --git a/spec/bridge_spec.rb b/spec/bridge_spec.rb index 3b1a5fe..bb51283 100644 --- a/spec/bridge_spec.rb +++ b/spec/bridge_spec.rb @@ -62,7 +62,7 @@ end end - describe ".update_node_by_id" do + describe ".update_node" do context "given a node id" do it "should return a cypher result" do query = Deja::Bridge.update_node(1, {:some => :attr}) @@ -101,7 +101,7 @@ describe ".get_related_nodes" do context "given a node id" do it "should return a cypher result" do - query = Deja::Bridge.get_related_nodes(1, :include => :all) + query = Deja::Bridge.get_nodes(1, :include => :all) query.should be_a(Neo4j::Cypher::Result) end end diff --git a/spec/finders_spec.rb b/spec/finders_spec.rb index 60dc757..cff14f0 100644 --- a/spec/finders_spec.rb +++ b/spec/finders_spec.rb @@ -42,26 +42,27 @@ class HasHate < Relationship; end @invested_in = InvestedIn.new(@first_node, @second_node).create @friends = FriendsWith.new(@first_node, @second_node).create @hates = HasHate.new(@first_node, @third_node).create + @hates2 = HasHate.new(@first_node, @second_node).create end - describe ".find_by_neo_id" do + describe ".find" do context "given a node id and no filters" do before :each do - @node = Person.find_by_neo_id(@first_node.id, :include => :all) + @node = Person.find(@first_node.id, :include => :all) end - it "should return a node and all related nodes by default" do + it "should return a node and all related nodes" do @node.should_not_receive(:related_nodes) @node.name.should eq(@first_node.name) @node.permalink.should eq(@first_node.permalink) end - it "calling invested_in should not call related_nodes" do - @node.should_not_receive(:related_nodes).with(:invested_in) + it "calling invested_in should call related_nodes" do + @node.should_receive(:related_nodes).and_call_original @node.investment end - it "calling invested_in should return an array of relNodeWrappers" do + it "calling invested_in should return an array of rel/node pairs" do @node.investments.should be_a(Array) @node.investments[0].should be_a(Array) end @@ -69,11 +70,11 @@ class HasHate < Relationship; end context "given a node id with an :invested_in argument" do it "should not call related_nodes when eager loading" do - Person.find_by_neo_id(@first_node.id, :include => :invested_in).should_not_receive(:related_nodes) + Person.find(@first_node.id, :include => :invested_in).should_not_receive(:related_nodes) end it "should return only the invested_in relationship" do - first_node = Person.find_by_neo_id(@first_node.id, :include => :invested_in) + first_node = Person.find(@first_node.id, :include => :invested_in) first_node.name.should eq(@first_node.name) first_node.permalink.should eq(@first_node.permalink) node_type_test(first_node, :investments) @@ -82,11 +83,11 @@ class HasHate < Relationship; end context "given a node id with an :invested_in and :friends argument" do it "should not call related_nodes when eager loading multiple relations" do - first_node = Person.find_by_neo_id(@first_node.id, :include => [:invested_in, :friends]).should_not_receive(:related_nodes) + first_node = Person.find(@first_node.id, :include => [:invested_in, :friends]).should_not_receive(:related_nodes) end it "should return both relationships" do - first_node = Person.find_by_neo_id(@first_node.id, :include => [:invested_in, :friends]) + first_node = Person.find(@first_node.id, :include => [:invested_in, :friends]) first_node.name.should eq(@first_node.name) first_node.permalink.should eq(@first_node.permalink) node_type_test(first_node, :investments) @@ -96,7 +97,7 @@ class HasHate < Relationship; end context "given a node id with a :none filter" do it "should return a node and no related nodes" do - first_node = Person.find_by_neo_id(@first_node.id) + first_node = Person.find(@first_node.id, :include => :none) first_node.should_receive(:related_nodes).at_least(:once).and_call_original full_node_type_test(first_node) end @@ -106,7 +107,7 @@ class HasHate < Relationship; end describe ".find" do context "given a neo_id with associated nodes and :all argument" do it "should return node objects with relationships" do - first_node = Person.find_by_neo_id(@first_node.id, :include => :all) + first_node = Person.find(@first_node.id, :include => :all) first_node.investments.should_not be_nil first_node.friends.should_not be_nil first_node.hates.should_not be_nil @@ -118,12 +119,12 @@ class HasHate < Relationship; end describe ".related_nodes" do context "on an instance of a single node" do before :each do - @node = Person.find_by_neo_id(@first_node.id, :include => :all) + @node = Person.find(@first_node.id, :include => :none) end - it "should not call related_nodes on already loaded relations" do + it "should call related_nodes on relations" do @node.should_receive(:related_nodes).and_call_original - @node.investments(:person).each do |node, rel| + @node.investments(:filter => :person).each do |node, rel| node.should be_a(Node) rel.should be_a(Relationship) end diff --git a/spec/node_spec.rb b/spec/node_spec.rb index 5c660b0..d8a8749 100644 --- a/spec/node_spec.rb +++ b/spec/node_spec.rb @@ -36,7 +36,7 @@ class InvestedIn < Relationship; end @first_node.name = 'M' @first_node.save! @first_node.save.should be_true - graph_node = Person.find_by_neo_id(id) + graph_node = Person.find(id) expect(graph_node.name).to eq('M') end end @@ -58,7 +58,7 @@ class InvestedIn < Relationship; end id = @first_node.id @first_node.delete.should be_true expect(@first_node.id).to be_nil - expect{Person.find_by_neo_id(id)}.to raise_error() + expect{Person.find(id)}.to raise_error() end end @@ -106,13 +106,50 @@ class InvestedIn < Relationship; end end end + describe "relationship filters and options" do + before :each do + @first_node.save() + @second_node = FactoryGirl.create(:person); + 10.times do + InvestedIn.new(@first_node, FactoryGirl.create(:company)).create + end + InvestedIn.new(@first_node, @second_node).create + end + + context "given a filter" do + it "should filter results" do + @first_node.investments(:filter => :person).size.should be 1 + end + end + + context "given an order" do + it "should order results" do + desc = @first_node.investments(:order => 'name DESC').collect {|node, rel| node.name } + asc = @first_node.investments(:order => 'name ASC').collect {|node, rel| node.name } + desc.should eq(asc.reverse) + end + end + + context "given a limit" do + it "should limit results" do + @first_node.investments(:limit => 2).size.should be 2 + end + end + + context "given a offset" do + it "should offset results" do + @first_node.investments(:offset => 2).size.should be @first_node.count(:investments) - 2 + end + end + end + describe "in batch" do context "with two nodes" do before :each do @first_node.save() - @first_node = Person.find_by_neo_id(@first_node.id) + @first_node = Person.find(@first_node.id) @second_node.save() - @second_node = Person.find_by_neo_id(@second_node.id) + @second_node = Person.find(@second_node.id) end it "should commit in single request" do @@ -124,8 +161,8 @@ class InvestedIn < Relationship; end @second_node.save() end - @first_node_new = Person.find_by_neo_id(@first_node.id) - @second_node_new = Person.find_by_neo_id(@second_node.id) + @first_node_new = Person.find(@first_node.id) + @second_node_new = Person.find(@second_node.id) expect(@first_node_new.name).to eq("shark") expect(@second_node_new.name).to eq("speak")