architect4r 0.3.3.1 → 0.3.4

Sign up to get free protection for your applications and to get access to all the features.
data/ReleaseNotes.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Release Notes
2
2
 
3
+ ## v0.3.4
4
+
5
+ * Allow fetching items, such as nodes and relationships in one query
6
+
7
+ By using the server's cypher_query() method, it is possible to fetch
8
+ multiple items of different types by using a single query.
9
+
3
10
  ## v0.3.3
4
11
 
5
12
  * Add magic timestamp! property for tracking timestamps
data/Specs.md CHANGED
@@ -2,6 +2,35 @@
2
2
 
3
3
  Some code which might define the future interface of the gem.
4
4
 
5
+ # Defining
6
+
7
+ class Character < Architect4r::Model::Node
8
+ property :name, :cast_to => String
9
+ property :human, :cast_to => TrueClass
10
+ timestamps!
11
+
12
+ def ships
13
+ class.find_by_cypher("start s=node(#{id}) match s-[:CrewMembership]->d return d", "d")
14
+ end
15
+
16
+ def crew_memberships
17
+ class.find_by_cypher("start s=node(#{id}) match s-[r:CrewMembership]->d return d, r", "d")
18
+ end
19
+ end
20
+
21
+ class Ship < Architect4r::Model::Node
22
+ property :name, :cast_to => String
23
+ end
24
+
25
+ class CrewMembership < Architect4r::Model::Relationship
26
+ property :rank, :cast_to => String
27
+ end
28
+
29
+ neo = Character.find_by_id(15)
30
+ neo.
31
+
32
+
33
+
5
34
  # Finding records
6
35
  Instrument.all
7
36
  Instrument.find_by_name("Piano")
data/architect4r.gemspec CHANGED
@@ -7,7 +7,7 @@ Gem::Specification.new do |s|
7
7
  s.version = Architect4r::VERSION
8
8
  s.authors = ["Maximilian Schulz"]
9
9
  s.email = ["m.schulz@kulturfluss.de"]
10
- s.homepage = "http://www.kulturfluss.de/"
10
+ s.homepage = "https://github.com/namxam/architect4r/"
11
11
  s.summary = %q{A gem for working with the neo4j REST interface.}
12
12
  s.description = %q{This gem is intended for making a neo4j graph db accessible by providing a ruby wrapper for the REST API.}
13
13
 
data/lib/architect4r.rb CHANGED
@@ -62,5 +62,6 @@ module Architect4r
62
62
  end
63
63
 
64
64
  class InvalidCypherQuery < SyntaxError; end
65
+ class RecordNotFound < StandardError; end
65
66
 
66
67
  end
@@ -6,7 +6,6 @@ module Architect4r
6
6
  module InstanceMethods
7
7
 
8
8
  def execute_cypher(query)
9
-
10
9
  query = self.interpolate_node_model_root_references(query)
11
10
 
12
11
  url = prepend_base_url("/ext/CypherPlugin/graphdb/execute_query")
@@ -28,22 +27,60 @@ module Architect4r
28
27
  elsif response.code == 204
29
28
  nil
30
29
  else
31
- result_data = JSON.parse(response.body)
32
-
33
- # Convert the columnized result array to hashes within an array
34
- results = []
35
-
36
- result_data['data'].each_with_index do |row, ri|
37
- result_row = {}
38
- result_data['columns'].each_with_index do |column, ci|
39
- result_row[column] = convert_if_possible(row[ci])
40
- end
41
- results << result_row
30
+ JSON.parse(response.body)
31
+ end
32
+ end
33
+
34
+ def cypher_query(query, transform=true)
35
+ # Get data from server
36
+ data = execute_cypher(query)
37
+
38
+ # Create native ruby objects
39
+ data['data'].map! do |set|
40
+ set.map { |item| convert_if_possible(item) }
41
+ end
42
+
43
+ # Transform to hash form
44
+ if transform
45
+ transform_cypher_result_to_hash(data)
46
+ else
47
+ data
48
+ end
49
+ end
50
+
51
+ # Convert the columnized result array to a hashes within an array
52
+ #
53
+ # The input would look like:
54
+ #
55
+ # { 'data' => [['r1', 'n1'], ['r2', 'n2'], …], 'columns' => ['rel', 'node']}
56
+ #
57
+ # And it would be transformed to:
58
+ # [ { 'rel' => 'r1', 'node' => 'n1' }, { 'rel' => 'r2', 'node' => 'n2' } ]
59
+ #
60
+ def transform_cypher_result_to_hash(input)
61
+ results = []
62
+ input['data'].each_with_index do |row, ri|
63
+ result_row = {}
64
+ input['columns'].each_with_index do |column, ci|
65
+ result_row[column] = row[ci]
42
66
  end
43
- results
67
+ results << result_row
44
68
  end
69
+ results
45
70
  end
46
71
 
72
+ # Parses a cypher query and replaces all placeholders for model roots
73
+ # with the neo4j ids. It should make development faster, as you can
74
+ # skip manual id retrieval:
75
+ #
76
+ # So instead of writing:
77
+ #
78
+ # "start s=node(#{Person.model_root.id}) match s-->d return d"
79
+ #
80
+ # you can write:
81
+ #
82
+ # "start s=node(:person_root) match s-->d return d"
83
+ #
47
84
  def interpolate_node_model_root_references(query)
48
85
  query.scan(/node\((:[^)]*_root)\)/i).flatten.uniq.each do |str|
49
86
  model_name = str.match(/^:(.*)_root$/)[1].to_s.classify
@@ -25,19 +25,18 @@ module Architect4r
25
25
  def self.model_root
26
26
  @model_root ||= begin
27
27
  # Check if there is already a model root,
28
- query = "start root = node(0) match (root)-[r:#{ model_root_relation_type}]->(x) where r.architect4r_type and r.architect4r_type = '#{name}' return x"
29
- the_root = connection.execute_cypher(query).to_a.first
28
+ query = "start root = node(0) match root-[r:#{model_root_relation_type}]->x where r.architect4r_type and r.architect4r_type = '#{name}' return x"
29
+ the_root = connection.cypher_query(query).to_a.first
30
30
  the_root &&= the_root['x']
31
31
 
32
32
  # otherwise create one
33
33
  the_root ||= begin
34
- m_root = connection.create_node(:name => "#{name} Root")
34
+ m_root = connection.create_node(:name => "#{name} Root", :root_for => name)
35
35
  connection.create_relationship(0, m_root, model_root_relation_type, { 'architect4r_type' => name })
36
- m_root
36
+
37
+ # Return model root node
38
+ GenericNode.send(:build_from_database, m_root)
37
39
  end
38
-
39
- # Return model root node
40
- GenericNode.send(:build_from_database, the_root)
41
40
  end
42
41
  end
43
42
 
@@ -11,22 +11,24 @@ module Architect4r
11
11
 
12
12
  def count(opts = {}, &block)
13
13
  data = connection.execute_cypher("start s=node(#{self.model_root.id}) match (s)<-[:model_type]-(d) return count(d)")
14
- data.first['count(d)']
14
+ data['data'].flatten.first
15
15
  end
16
16
 
17
17
  def find_by_id(id)
18
18
  data = connection.execute_cypher("start s=node(#{self.model_root.id}), d=node(#{id.to_i}) match s<-[r:model_type]-d return d")
19
- data &&= data.first && data.first['d']
19
+ data &&= data['data'] && data['data'].flatten.first
20
20
  self.build_from_database(data)
21
21
  end
22
22
 
23
23
  def find_by_id!(id)
24
- raise 'not implemented'
24
+ self.find_by_id(id) || raise(Architect4r::RecordNotFound.new("Could not find the #{self.name} with id #{id}!"))
25
25
  end
26
26
 
27
+ # Use this method only to fetch items of the same class.
27
28
  def find_by_cypher(query, identifier)
28
- if data = connection.execute_cypher(query)
29
- data.map { |item| build_from_database(item[identifier]) }
29
+ if result_data = connection.execute_cypher(query)
30
+ result_data = connection.transform_cypher_result_to_hash(result_data)
31
+ result_data.map { |item| connection.convert_if_possible(item[identifier]) }
30
32
  else
31
33
  nil
32
34
  end
@@ -39,16 +39,6 @@ module Architect4r
39
39
  response.success? ? JSON.parse(response.body) : nil
40
40
  end
41
41
 
42
- protected
43
-
44
- def prepend_base_url(url)
45
- if url[0,4] == "http"
46
- url
47
- else
48
- "http://#{configuration.host}:#{configuration.port}#{configuration.path}/db/data#{url}"
49
- end
50
- end
51
-
52
42
  def node_url(url_or_id)
53
43
  if url_or_id.is_a?(Hash)
54
44
  url_or_id['self'].to_s
@@ -69,10 +59,33 @@ module Architect4r
69
59
  end
70
60
  end
71
61
 
72
- def convert_if_possible(data)
62
+ # Build a node or relationship from the hash neo4j returns.
63
+ def convert_if_possible(object_hash)
64
+ if klass = object_hash.is_a?(Hash) && object_hash['data']
65
+ if model_string = object_hash['data']['architect4r_type']
66
+ data = begin
67
+ eval("#{model_string}.send(:build_from_database, object_hash)")
68
+ rescue => ex
69
+ data
70
+ end
71
+ elsif object_hash['self'].match(/node\/\d+$/i)
72
+ data = GenericNode.send(:build_from_database, object_hash)
73
+ end
74
+ end
73
75
  data
74
76
  end
75
77
 
78
+
79
+ protected
80
+
81
+ def prepend_base_url(url)
82
+ if url[0,4] == "http"
83
+ url
84
+ else
85
+ "http://#{configuration.host}:#{configuration.port}#{configuration.path}/db/data#{url}"
86
+ end
87
+ end
88
+
76
89
  end
77
90
 
78
91
 
@@ -1,3 +1,3 @@
1
1
  module Architect4r
2
- VERSION = "0.3.3.1"
2
+ VERSION = "0.3.4"
3
3
  end
@@ -67,4 +67,26 @@ describe Architect4r::Server do
67
67
 
68
68
  end
69
69
 
70
+ describe "custom cypher queries" do
71
+
72
+ it "should return all native objects from queries" do
73
+ neo = Person.create(:name => 'Neo', :human => true)
74
+ niobe = Person.create(:name => 'Niobe', :human => true)
75
+ logos = Ship.create(:name => 'Logos', :crew_size => 2)
76
+
77
+ CrewMembership.create(logos, niobe, { :rank => 'Captain' })
78
+ CrewMembership.create(logos, neo, { :rank => 'Member' })
79
+
80
+ result = subject.cypher_query("start s=node(#{logos.id}) match s-[membership:CrewMembership]->person return membership, person")
81
+ result.should be_a(Array)
82
+
83
+ result[0]['membership'].should be_a(CrewMembership)
84
+ result[0]['person'].should be_a(Person)
85
+
86
+ result[1]['membership'].should be_a(CrewMembership)
87
+ result[1]['person'].should be_a(Person)
88
+ end
89
+
90
+ end
91
+
70
92
  end
@@ -40,7 +40,7 @@ describe "Model Node" do
40
40
  Person.create(:name => 'Morpheus', :human => true)
41
41
  m_root = Person.model_root
42
42
  Person.create(:name => 'Trinity', :human => true)
43
- Person.model_root.should == m_root
43
+ Person.model_root.id.should == m_root.id
44
44
  end
45
45
 
46
46
  end
@@ -22,6 +22,19 @@ describe "Node Queries" do
22
22
  Person.find_by_id(ship.id).should be_nil
23
23
  end
24
24
 
25
+ it "should return nil when the node is not found" do
26
+ Person.find_by_id(-1).should be_nil
27
+ end
28
+
29
+ it "should raise an exception when banged finder is used an no record found" do
30
+ lambda { Person.find_by_id!(-1) }.should raise_error(Architect4r::RecordNotFound)
31
+ end
32
+
33
+ it "should not raise an exception when banged finder is used an a record is found" do
34
+ person = Person.create(:name => 'The Architect', :human => false)
35
+ lambda { Person.find_by_id!(person.id) }.should_not raise_error(Architect4r::RecordNotFound)
36
+ end
37
+
25
38
  end
26
39
 
27
40
  describe "counting records" do
@@ -40,7 +53,7 @@ describe "Node Queries" do
40
53
  Person.create(:name => 'Neo', :human => true)
41
54
  Person.create(:name => 'Trinity', :human => true)
42
55
  Person.create(:name => 'Morpheus', :human => true)
43
- results = Person.find_by_cypher("start s=node(#{Person.model_root.id}) match s<-[:model_type]-d return d limit 2", 'd')
56
+ results = Person.find_by_cypher("start s=node(:person_root) match s<-[:model_type]-d return d limit 2", 'd')
44
57
  results.size.should == 2
45
58
  results.first.should be_a(Person)
46
59
  end
data/spec/spec_helper.rb CHANGED
@@ -24,10 +24,11 @@ RSpec.configure do |config|
24
24
 
25
25
  config.before(:suite) do
26
26
  neo_manager.reset_to_sample_data(File.join(File.dirname(__FILE__), "fixtures/graph.db.default/"))
27
+ neo_manager.start
27
28
  end
28
29
 
29
30
  config.after(:suite) do
30
- neo_manager.start
31
+ neo_manager.stop
31
32
  end
32
33
  end
33
34
 
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: architect4r
3
3
  version: !ruby/object:Gem::Version
4
- hash: 89
4
+ hash: 27
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 3
9
- - 3
10
- - 1
11
- version: 0.3.3.1
9
+ - 4
10
+ version: 0.3.4
12
11
  platform: ruby
13
12
  authors:
14
13
  - Maximilian Schulz
@@ -16,7 +15,7 @@ autorequire:
16
15
  bindir: bin
17
16
  cert_chain: []
18
17
 
19
- date: 2011-10-26 00:00:00 Z
18
+ date: 2011-10-27 00:00:00 Z
20
19
  dependencies:
21
20
  - !ruby/object:Gem::Dependency
22
21
  name: activemodel
@@ -266,7 +265,7 @@ files:
266
265
  - spec/model/validations_spec.rb
267
266
  - spec/server_spec.rb
268
267
  - spec/spec_helper.rb
269
- homepage: http://www.kulturfluss.de/
268
+ homepage: https://github.com/namxam/architect4r/
270
269
  licenses: []
271
270
 
272
271
  post_install_message: