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 +7 -0
- data/Specs.md +29 -0
- data/architect4r.gemspec +1 -1
- data/lib/architect4r.rb +1 -0
- data/lib/architect4r/core/cypher_methods.rb +50 -13
- data/lib/architect4r/model/node.rb +6 -7
- data/lib/architect4r/model/queries.rb +7 -5
- data/lib/architect4r/server.rb +24 -11
- data/lib/architect4r/version.rb +1 -1
- data/spec/core/cypher_methods_spec.rb +22 -0
- data/spec/model/node_spec.rb +1 -1
- data/spec/model/queries_spec.rb +14 -1
- data/spec/spec_helper.rb +2 -1
- metadata +5 -6
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 = "
|
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
@@ -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
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
29
|
-
the_root = connection.
|
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
|
-
|
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
|
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
|
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
|
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
|
29
|
-
|
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
|
data/lib/architect4r/server.rb
CHANGED
@@ -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
|
-
|
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
|
|
data/lib/architect4r/version.rb
CHANGED
@@ -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
|
data/spec/model/node_spec.rb
CHANGED
@@ -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
|
data/spec/model/queries_spec.rb
CHANGED
@@ -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(
|
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.
|
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:
|
4
|
+
hash: 27
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 3
|
9
|
-
-
|
10
|
-
|
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-
|
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:
|
268
|
+
homepage: https://github.com/namxam/architect4r/
|
270
269
|
licenses: []
|
271
270
|
|
272
271
|
post_install_message:
|