neo4r 0.0.3
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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +130 -0
- data/Rakefile +2 -0
- data/lib/neo4r.rb +20 -0
- data/lib/neo4r/attributes.rb +68 -0
- data/lib/neo4r/finder.rb +68 -0
- data/lib/neo4r/index_config_loader.rb +35 -0
- data/lib/neo4r/node.rb +54 -0
- data/lib/neo4r/node_relationship.rb +39 -0
- data/lib/neo4r/node_traverser.rb +154 -0
- data/lib/neo4r/paginated.rb +25 -0
- data/lib/neo4r/property_container.rb +38 -0
- data/lib/neo4r/relation.rb +174 -0
- data/lib/neo4r/relationship.rb +51 -0
- data/lib/neo4r/relationship_traverser.rb +86 -0
- data/lib/neo4r/rest_wrapper.rb +10 -0
- data/lib/neo4r/type_converters.rb +336 -0
- data/lib/neo4r/version.rb +3 -0
- data/lib/neo4r/will_paginate.rb +20 -0
- data/neo4r.gemspec +29 -0
- metadata +165 -0
@@ -0,0 +1,154 @@
|
|
1
|
+
module Neo4r
|
2
|
+
# Node traverser class
|
3
|
+
class NodeTraverser
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
attr_accessor :order, :uniqueness, :depth, :prune, :filter, :relationships
|
7
|
+
attr_reader :rest
|
8
|
+
|
9
|
+
def initialize(from, types = nil, dir = "all")
|
10
|
+
@rest = RestWrapper.new
|
11
|
+
@from = from
|
12
|
+
@order = "depth first"
|
13
|
+
@uniqueness = "none"
|
14
|
+
@relationships = []
|
15
|
+
types.each do |type|
|
16
|
+
@relationships << { "type" => type.to_s, "direction" => dir.to_s }
|
17
|
+
end unless types.nil?
|
18
|
+
end
|
19
|
+
|
20
|
+
def <<(other_node)
|
21
|
+
create(other_node)
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def create(other_node)
|
26
|
+
case @relationships.first["direction"]
|
27
|
+
when "outgoing", "out"
|
28
|
+
rel = Relationship.create(
|
29
|
+
type: @relationships.first["type"], start: @from, end: other_node)
|
30
|
+
when "incoming", "in"
|
31
|
+
rel = Relationship.create(
|
32
|
+
type: @relationships.first["type"], start: other_node, end: @from)
|
33
|
+
else
|
34
|
+
rel = []
|
35
|
+
rel << Relationship.create(
|
36
|
+
type: @relationships.first["type"], start: @from, end: other_node)
|
37
|
+
rel << Relationship.create(
|
38
|
+
type: @relationships.first["type"], start: other_node, end: @from)
|
39
|
+
end
|
40
|
+
rel
|
41
|
+
end
|
42
|
+
|
43
|
+
def both(type)
|
44
|
+
@relationships << { "type" => type.to_s, "direction" => "all" }
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
def outgoing(type)
|
49
|
+
@relationships << { "type" => type.to_s, "direction" => "out" }
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
def incoming(type)
|
54
|
+
@relationships << { "type" => type.to_s, "direction" => "in" }
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
def uniqueness(u)
|
59
|
+
@uniqueness = u
|
60
|
+
self
|
61
|
+
end
|
62
|
+
|
63
|
+
def order(o)
|
64
|
+
@order = o
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
def filter(body)
|
69
|
+
@filter = {
|
70
|
+
"language" => "javascript",
|
71
|
+
"body" => body
|
72
|
+
}
|
73
|
+
self
|
74
|
+
end
|
75
|
+
|
76
|
+
def prune(body)
|
77
|
+
@prune = {
|
78
|
+
"language" => "javascript",
|
79
|
+
"body" => body
|
80
|
+
}
|
81
|
+
self
|
82
|
+
end
|
83
|
+
|
84
|
+
def depth(d)
|
85
|
+
d = 2_147_483_647 if d == :all
|
86
|
+
@depth = d
|
87
|
+
self
|
88
|
+
end
|
89
|
+
|
90
|
+
def include_start_node
|
91
|
+
@filter = {
|
92
|
+
"language" => "builtin",
|
93
|
+
"name" => "all"
|
94
|
+
}
|
95
|
+
self
|
96
|
+
end
|
97
|
+
|
98
|
+
def size
|
99
|
+
[*self].size
|
100
|
+
end
|
101
|
+
|
102
|
+
alias_method :length, :size
|
103
|
+
|
104
|
+
def [](index)
|
105
|
+
each_with_index { |node, i| break node if index == i }
|
106
|
+
end
|
107
|
+
|
108
|
+
def empty?
|
109
|
+
first.nil?
|
110
|
+
end
|
111
|
+
|
112
|
+
def each
|
113
|
+
nodes = []
|
114
|
+
iterator.each do |i|
|
115
|
+
node = @from.class.new(i)
|
116
|
+
nodes << node
|
117
|
+
yield node
|
118
|
+
end
|
119
|
+
nodes
|
120
|
+
end
|
121
|
+
|
122
|
+
def iterator
|
123
|
+
options = {
|
124
|
+
"order" => @order,
|
125
|
+
"uniqueness" => @uniqueness,
|
126
|
+
"relationships" => @relationships
|
127
|
+
}
|
128
|
+
options["prune evaluator"] = @prune unless @prune.nil?
|
129
|
+
options["return filter"] = @filter unless @filter.nil?
|
130
|
+
options["depth"] = @depth unless @depth.nil?
|
131
|
+
|
132
|
+
if @relationships[0]["type"].empty?
|
133
|
+
rels = rest.get_node_relationships(
|
134
|
+
@from.neo_id, @relationships[0]["direction"]) || []
|
135
|
+
case @relationships[0]["direction"]
|
136
|
+
when "in"
|
137
|
+
rels.map { |r| rest.get_node(r["start"]) }
|
138
|
+
when "out"
|
139
|
+
rels.map { |r| rest.get_node(r["end"]) }
|
140
|
+
else
|
141
|
+
rels.map do |r|
|
142
|
+
if @from.neo_id == r["start"].split('/').last.to_i
|
143
|
+
rest.get_node(r["end"])
|
144
|
+
else
|
145
|
+
rest.get_node(r["start"])
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
else
|
150
|
+
rest.traverse(@from.neo_id, "nodes", options)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Neo4r
|
2
|
+
# The class provides the pagination based on the given source.
|
3
|
+
# The source must be an Enumerable implementing methods drop,
|
4
|
+
# first and count (or size).
|
5
|
+
# This can be used to paginage any Enumerable collection and
|
6
|
+
# provides the integration point for other gems, like
|
7
|
+
# will_paginate and kaminari.
|
8
|
+
class Paginated
|
9
|
+
include Enumerable
|
10
|
+
attr_accessor :items, :total, :current_page
|
11
|
+
delegate :each, to: :items
|
12
|
+
|
13
|
+
def initialize(items, total, current_page)
|
14
|
+
@items = items
|
15
|
+
@total = total
|
16
|
+
@current_page = current_page
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.create_from(source, page, per_page)
|
20
|
+
dup = source.dup
|
21
|
+
partial = dup.offset((page - 1) * per_page).limit(per_page)
|
22
|
+
Paginated.new(partial, dup.count, page)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Neo4r
|
2
|
+
# Peoperty container class
|
3
|
+
class PropertyContainer < OpenStruct
|
4
|
+
include Neo4r::Attributes
|
5
|
+
|
6
|
+
attr_reader :neo_id, :rest
|
7
|
+
|
8
|
+
def initialize(args = {})
|
9
|
+
@rest = RestWrapper.new
|
10
|
+
if args["self"] && args["data"]
|
11
|
+
@neo_id = args["self"].split("/").last.to_i
|
12
|
+
super(self.class.to_ruby(args["data"]))
|
13
|
+
else
|
14
|
+
super(args)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def is_new?
|
19
|
+
@neo_id.nil?
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_id(target)
|
23
|
+
if target.is_a?(String)
|
24
|
+
target.split("/").last.to_i
|
25
|
+
elsif target.class < PropertyContainer
|
26
|
+
target.neo_id
|
27
|
+
else
|
28
|
+
target
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def inspect
|
33
|
+
str = super
|
34
|
+
str.sub!(/ /, "[#{@neo_id}] ") unless @neo_id.nil?
|
35
|
+
str
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
require "neo4r/will_paginate"
|
2
|
+
require "neo4r/rest_wrapper"
|
3
|
+
|
4
|
+
module Neo4r
|
5
|
+
# Neo4r relation class
|
6
|
+
class Relation
|
7
|
+
include Enumerable
|
8
|
+
include ::Neo4r::WillPaginate
|
9
|
+
|
10
|
+
alias_method :all, :to_a
|
11
|
+
alias_method :size, :count
|
12
|
+
|
13
|
+
def initialize(klass)
|
14
|
+
@klass = klass
|
15
|
+
@labels = klass.name.split("::")
|
16
|
+
@props = {}
|
17
|
+
@has = []
|
18
|
+
@order_by = []
|
19
|
+
@limit = nil
|
20
|
+
@offset = nil
|
21
|
+
end
|
22
|
+
|
23
|
+
def clear
|
24
|
+
@labels = []
|
25
|
+
@props = {}
|
26
|
+
@has = []
|
27
|
+
@order_by = []
|
28
|
+
@limit = nil
|
29
|
+
@offset = nil
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
def count
|
34
|
+
rest = RestWrapper.new
|
35
|
+
rest.execute_query(to_cypher(return: :count))["data"].first.first
|
36
|
+
end
|
37
|
+
|
38
|
+
def each
|
39
|
+
rest = RestWrapper.new
|
40
|
+
data = rest.execute_query(to_cypher)["data"]
|
41
|
+
data.map do |d|
|
42
|
+
yield @klass.new(d.first)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def labels(*labels)
|
47
|
+
@labels.concat(labels).uniq!
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
def where(hash)
|
52
|
+
@props.merge!(hash)
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
56
|
+
def asc(*props)
|
57
|
+
props.each do |prop|
|
58
|
+
@order_by << "a.#{prop}"
|
59
|
+
end
|
60
|
+
self
|
61
|
+
end
|
62
|
+
|
63
|
+
def desc(*props)
|
64
|
+
props.each do |prop|
|
65
|
+
@order_by << "a.#{prop} DESC"
|
66
|
+
end
|
67
|
+
self
|
68
|
+
end
|
69
|
+
|
70
|
+
def limit(n)
|
71
|
+
@limit = n
|
72
|
+
self
|
73
|
+
end
|
74
|
+
|
75
|
+
def offset(n)
|
76
|
+
@offset = n
|
77
|
+
self
|
78
|
+
end
|
79
|
+
|
80
|
+
def has(*props)
|
81
|
+
@has.concat(props).uniq!
|
82
|
+
self
|
83
|
+
end
|
84
|
+
|
85
|
+
def to_cypher(options = {})
|
86
|
+
[
|
87
|
+
to_match,
|
88
|
+
to_where,
|
89
|
+
to_return(options),
|
90
|
+
to_order_by(options),
|
91
|
+
to_skip(options),
|
92
|
+
to_limit(options)
|
93
|
+
].compact.join("\n")
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def to_match
|
99
|
+
"MATCH (a#{to_labels}#{to_props})"
|
100
|
+
end
|
101
|
+
|
102
|
+
def to_labels
|
103
|
+
@labels.reduce("") do |result, label|
|
104
|
+
result + ":#{label}"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def to_props
|
109
|
+
format_props = @props.map do |name, value|
|
110
|
+
"#{name}: #{format_value(value)}"
|
111
|
+
end.join(", ")
|
112
|
+
|
113
|
+
if format_props.length > 0
|
114
|
+
" {#{format_props}}"
|
115
|
+
else
|
116
|
+
""
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def format_value(value)
|
121
|
+
case value
|
122
|
+
when String
|
123
|
+
'"' + value.gsub('"', '\"') + '"'
|
124
|
+
else
|
125
|
+
value.to_s
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def to_return(options)
|
130
|
+
case options[:return]
|
131
|
+
when :count
|
132
|
+
"RETURN count(a)"
|
133
|
+
else
|
134
|
+
"RETURN a"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def to_order_by(options)
|
139
|
+
if @order_by.length > 0 && options[:return].nil?
|
140
|
+
"ORDER BY " + @order_by.join(", ")
|
141
|
+
else
|
142
|
+
nil
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def to_skip(options)
|
147
|
+
if @offset && options[:return].nil?
|
148
|
+
"SKIP #{@offset}"
|
149
|
+
else
|
150
|
+
nil
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def to_limit(options)
|
155
|
+
if @limit && options[:return].nil?
|
156
|
+
"LIMIT #{@limit}"
|
157
|
+
else
|
158
|
+
nil
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def to_where
|
163
|
+
expr = @has.map do |prop|
|
164
|
+
"HAS (a.#{prop})"
|
165
|
+
end.join(" AND ")
|
166
|
+
|
167
|
+
if expr.length > 0
|
168
|
+
"WHERE #{expr}"
|
169
|
+
else
|
170
|
+
nil
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Neo4r
|
2
|
+
class Relationship < PropertyContainer
|
3
|
+
def self.create(args)
|
4
|
+
rel = Relationship.new(args)
|
5
|
+
if rel.save
|
6
|
+
rel
|
7
|
+
else
|
8
|
+
nil
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :start_node_id, :end_node_id, :rel_type
|
13
|
+
|
14
|
+
def initialize(args)
|
15
|
+
super(args)
|
16
|
+
if is_new?
|
17
|
+
@start_node_id = to_id(delete_field(:start))
|
18
|
+
@end_node_id = to_id(delete_field(:end))
|
19
|
+
@rel_type = delete_field(:type)
|
20
|
+
else
|
21
|
+
@start_node_id = to_id(args["start"])
|
22
|
+
@end_node_id = to_id(args["end"])
|
23
|
+
@rel_type = args["type"]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def save
|
28
|
+
if is_new?
|
29
|
+
rel = rest.create_relationship(
|
30
|
+
@rel_type, @start_node_id, @end_node_id, @table)
|
31
|
+
@neo_id = rel["self"].split("/").last.to_i
|
32
|
+
else
|
33
|
+
rest.reset_relationship_properties(@neo_id, @table)
|
34
|
+
end
|
35
|
+
true
|
36
|
+
end
|
37
|
+
|
38
|
+
def destroy
|
39
|
+
rest.delete_relationship(neo_id)
|
40
|
+
true
|
41
|
+
end
|
42
|
+
|
43
|
+
def start_node
|
44
|
+
@start_node ||= Experiment.find(@start_node_id)
|
45
|
+
end
|
46
|
+
|
47
|
+
def end_node
|
48
|
+
@end_node ||= Experiment.find(@end_node_id)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require "neo4r/relationship"
|
2
|
+
|
3
|
+
module Neo4r
|
4
|
+
# Relationship traverser class
|
5
|
+
class RelationshipTraverser
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
attr_reader :rest
|
9
|
+
|
10
|
+
def initialize(node, types, direction)
|
11
|
+
@rest = RestWrapper.new
|
12
|
+
@node = node
|
13
|
+
@types = [types]
|
14
|
+
@direction = direction
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
if @types.size == 1 && !@types.empty?
|
19
|
+
"#{self.class} [type: #{@type} dir:#{@direction}]"
|
20
|
+
elsif !@types.empty?
|
21
|
+
"#{self.class} [types: #{@types.join(',')} dir:#{@direction}]"
|
22
|
+
else
|
23
|
+
"#{self.class} [types: ANY dir:#{@direction}]"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def each
|
28
|
+
rels = []
|
29
|
+
iterator.each do |i|
|
30
|
+
rel = Relationship.new(i)
|
31
|
+
rels << rel
|
32
|
+
yield rel if match_to_other?(rel)
|
33
|
+
end
|
34
|
+
rels
|
35
|
+
end
|
36
|
+
|
37
|
+
def empty?
|
38
|
+
first.nil?
|
39
|
+
end
|
40
|
+
|
41
|
+
def iterator
|
42
|
+
Array(rest.get_node_relationships(@node.neo_id, @direction, @types))
|
43
|
+
end
|
44
|
+
|
45
|
+
def match_to_other?(rel)
|
46
|
+
if @to_other.nil?
|
47
|
+
true
|
48
|
+
elsif @direction == :outgoing
|
49
|
+
rel.end_node_id == @to_other.neo_id
|
50
|
+
elsif @direction == :incoming
|
51
|
+
rel.start_node_id == @to_other.neo_id
|
52
|
+
else
|
53
|
+
rel.start_node_id == @to_other.neo_id || rel.end_node_id == @to_other.neo_id
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_other(to_other)
|
58
|
+
@to_other = to_other
|
59
|
+
self
|
60
|
+
end
|
61
|
+
|
62
|
+
def destroy
|
63
|
+
each { |rel| rel.destroy }
|
64
|
+
end
|
65
|
+
|
66
|
+
def size
|
67
|
+
[*self].size
|
68
|
+
end
|
69
|
+
|
70
|
+
def both
|
71
|
+
@direction = :both
|
72
|
+
self
|
73
|
+
end
|
74
|
+
|
75
|
+
def incoming
|
76
|
+
@direction = :incoming
|
77
|
+
self
|
78
|
+
end
|
79
|
+
|
80
|
+
def outgoing
|
81
|
+
@direction = :outgoing
|
82
|
+
self
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|