architect4r 0.3.3.1 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: