neo4j 3.0.0.alpha.8 → 3.0.0.alpha.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +8 -0
- data/Gemfile +1 -1
- data/config/neo4j/config.yml +19 -84
- data/lib/neo4j.rb +5 -2
- data/lib/neo4j.rb~ +40 -0
- data/lib/neo4j/active_node.rb +19 -1
- data/lib/neo4j/active_node/has_n.rb +64 -124
- data/lib/neo4j/active_node/has_n/association.rb +142 -0
- data/lib/neo4j/active_node/id_property.rb +119 -0
- data/lib/neo4j/active_node/identity.rb +0 -5
- data/lib/neo4j/active_node/initialize.rb +1 -0
- data/lib/neo4j/active_node/labels.rb +62 -25
- data/lib/neo4j/active_node/persistence.rb +26 -5
- data/lib/neo4j/active_node/property.rb +121 -18
- data/lib/neo4j/active_node/query.rb +17 -9
- data/lib/neo4j/active_node/query/query_proxy.rb +202 -46
- data/lib/neo4j/active_node/serialized_properties.rb +21 -0
- data/lib/neo4j/config.rb +119 -0
- data/lib/neo4j/paginated.rb +19 -0
- data/lib/neo4j/railtie.rb +1 -0
- data/lib/neo4j/type_converters.rb +49 -3
- data/lib/neo4j/version.rb +1 -1
- data/lib/neo4j/wrapper.rb +15 -10
- data/lib/person.rb~ +9 -0
- data/lib/rails/generators/neo4j_generator.rb +1 -1
- data/lib/test.rb +21 -0
- data/lib/test.rb~ +21 -0
- data/neo4j.gemspec +2 -1
- metadata +27 -5
- data/lib/neo4j/active_node/has_n/decl_rel.rb +0 -252
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'active_support/inflector/inflections'
|
2
|
+
|
3
|
+
module Neo4j
|
4
|
+
module ActiveNode
|
5
|
+
module HasN
|
6
|
+
class Association
|
7
|
+
attr_reader :type, :name, :relationship, :direction
|
8
|
+
|
9
|
+
def initialize(type, direction, name, options = {})
|
10
|
+
raise ArgumentError, "Invalid association type: #{type.inspect}" if not [:has_many, :has_one].include?(type.to_sym)
|
11
|
+
raise ArgumentError, "Invalid direction: #{direction.inspect}" if not [:out, :in, :both].include?(direction.to_sym)
|
12
|
+
@type = type.to_sym
|
13
|
+
@name = name
|
14
|
+
@direction = direction.to_sym
|
15
|
+
raise ArgumentError, "Cannot specify both :type and :origin (#{base_declaration})" if options[:type] && options[:origin]
|
16
|
+
|
17
|
+
@target_class_name_from_name = name.to_s.classify
|
18
|
+
@target_class_option = target_class_option(options)
|
19
|
+
@callbacks = {before: options[:before], after: options[:after]}
|
20
|
+
@relationship_type = options[:type] && options[:type].to_sym
|
21
|
+
@origin = options[:origin] && options[:origin].to_sym
|
22
|
+
end
|
23
|
+
|
24
|
+
def target_class_option(options)
|
25
|
+
if options[:model_class].nil?
|
26
|
+
@target_class_name_from_name
|
27
|
+
elsif options[:model_class]
|
28
|
+
options[:model_class]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Return cypher partial query string for the relationship part of a MATCH (arrow / relationship definition)
|
33
|
+
def arrow_cypher(var = nil, properties = {}, create = false)
|
34
|
+
validate_origin!
|
35
|
+
|
36
|
+
relationship_type = relationship_type(create)
|
37
|
+
relationship_name_cypher = ":`#{relationship_type}`" if relationship_type
|
38
|
+
|
39
|
+
properties_string = get_properties_string(properties)
|
40
|
+
relationship_cypher = get_relationship_cypher(var, relationship_name_cypher, properties_string)
|
41
|
+
get_direction(relationship_cypher, create)
|
42
|
+
end
|
43
|
+
|
44
|
+
def target_class_name
|
45
|
+
@target_class_option.to_s if @target_class_option
|
46
|
+
end
|
47
|
+
|
48
|
+
def target_class
|
49
|
+
return @target_class if @target_class
|
50
|
+
|
51
|
+
@target_class = target_class_name.constantize if target_class_name
|
52
|
+
rescue NameError
|
53
|
+
raise ArgumentError, "Could not find `#{@target_class}` class and no :model_class specified"
|
54
|
+
end
|
55
|
+
|
56
|
+
def callback(type)
|
57
|
+
@callbacks[type]
|
58
|
+
end
|
59
|
+
|
60
|
+
def perform_callback(caller, other_node, type)
|
61
|
+
return if callback(type).nil?
|
62
|
+
caller.send(callback(type), other_node)
|
63
|
+
end
|
64
|
+
|
65
|
+
def relationship_type(create = false)
|
66
|
+
if @relationship_type
|
67
|
+
@relationship_type
|
68
|
+
elsif @origin
|
69
|
+
origin_type
|
70
|
+
else
|
71
|
+
(create || exceptional_target_class?) && "##{@name}"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def get_direction(relationship_cypher, create)
|
78
|
+
dir = (create && @direction == :both) ? :out : @direction
|
79
|
+
case dir
|
80
|
+
when :out
|
81
|
+
"-#{relationship_cypher}->"
|
82
|
+
when :in
|
83
|
+
"<-#{relationship_cypher}-"
|
84
|
+
when :both
|
85
|
+
"-#{relationship_cypher}-"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def get_relationship_cypher(var, relationship_name_cypher, properties_string)
|
90
|
+
"[#{var}#{relationship_name_cypher}#{properties_string}]"
|
91
|
+
end
|
92
|
+
|
93
|
+
def get_properties_string(properties)
|
94
|
+
p = properties.map do |key, value|
|
95
|
+
"#{key}: #{value.inspect}"
|
96
|
+
end.join(', ')
|
97
|
+
p.size == 0 ? '' : " {#{p}}"
|
98
|
+
end
|
99
|
+
|
100
|
+
def origin_type
|
101
|
+
target_class.associations[@origin].relationship_type
|
102
|
+
end
|
103
|
+
|
104
|
+
# Return basic details about association as declared in the model
|
105
|
+
# @example
|
106
|
+
# has_many :in, :bands
|
107
|
+
def base_declaration
|
108
|
+
"#{type} #{direction.inspect}, #{name.inspect}"
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
# Determine if model class as derived from the association name would be different than the one specified via the model_class key
|
113
|
+
# @example
|
114
|
+
# has_many :friends # Would return false
|
115
|
+
# has_many :friends, model_class: Friend # Would return false
|
116
|
+
# has_many :friends, model_class: Person # Would return true
|
117
|
+
def exceptional_target_class?
|
118
|
+
# TODO: Exceptional if target_class.nil?? (when model_class false)
|
119
|
+
|
120
|
+
target_class && target_class.name != @target_class_name_from_name
|
121
|
+
end
|
122
|
+
|
123
|
+
def validate_origin!
|
124
|
+
if @origin
|
125
|
+
if target_class
|
126
|
+
if association = target_class.associations[@origin]
|
127
|
+
if @direction == association.direction
|
128
|
+
raise ArgumentError, "Origin `#{@origin.inspect}` (specified in #{base_declaration}) has same direction `#{@direction}`)"
|
129
|
+
end
|
130
|
+
else
|
131
|
+
raise ArgumentError, "Origin `#{@origin.inspect}` association not found for #{target_class} (specified in #{base_declaration})"
|
132
|
+
end
|
133
|
+
else
|
134
|
+
raise ArgumentError, "Cannot use :origin without a model_class (implied or explicit)"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
@@ -0,0 +1,119 @@
|
|
1
|
+
module Neo4j::ActiveNode
|
2
|
+
|
3
|
+
# This module makes it possible to use other IDs than the build it neo4j id (neo_id)
|
4
|
+
#
|
5
|
+
# @example using generated UUIDs
|
6
|
+
# class Person
|
7
|
+
# include Neo4j::ActiveNode
|
8
|
+
# # creates a 'secret' neo4j property my_uuid which will be used as primary key
|
9
|
+
# id_property :my_uuid, auto: :uuid
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# @example using user defined ids
|
13
|
+
# class Person
|
14
|
+
# include Neo4j::ActiveNode
|
15
|
+
# property :title
|
16
|
+
# validates :title, :presence => true
|
17
|
+
# id_property :title_id, on: :title_to_url
|
18
|
+
#
|
19
|
+
# def title_to_url
|
20
|
+
# self.title.urlize # uses https://github.com/cheef/string-urlize gem
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
module IdProperty
|
25
|
+
extend ActiveSupport::Concern
|
26
|
+
|
27
|
+
|
28
|
+
module TypeMethods
|
29
|
+
def define_id_methods(clazz, name, conf)
|
30
|
+
validate_conf(conf)
|
31
|
+
if conf[:on]
|
32
|
+
define_custom_method(clazz, name, conf[:on])
|
33
|
+
elsif conf[:auto]
|
34
|
+
raise "only :uuid auto id_property allowed, got #{conf[:auto]}" unless conf[:auto] == :uuid
|
35
|
+
define_uuid_method(clazz, name)
|
36
|
+
else conf.empty?
|
37
|
+
define_property_method(clazz, name)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def validate_conf(conf)
|
44
|
+
return if conf.empty?
|
45
|
+
|
46
|
+
raise "Expected a Hash, got #{conf.class} (#{conf.to_s}) for id_property" unless conf.is_a?(Hash)
|
47
|
+
|
48
|
+
unless conf.include?(:auto) || conf.include?(:on)
|
49
|
+
raise "Illegal value #{conf.inspect} for id_property, expected :on or :auto"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def define_property_method(clazz, name)
|
54
|
+
clazz.module_eval(%Q{
|
55
|
+
def id
|
56
|
+
persisted? ? #{name} : nil
|
57
|
+
end
|
58
|
+
|
59
|
+
property :#{name}
|
60
|
+
validates_uniqueness_of :#{name}
|
61
|
+
}, __FILE__, __LINE__)
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
def define_uuid_method(clazz, name)
|
66
|
+
clazz.module_eval(%Q{
|
67
|
+
default_property :#{name} do
|
68
|
+
::SecureRandom.uuid
|
69
|
+
end
|
70
|
+
|
71
|
+
def #{name}
|
72
|
+
default_property :#{name}
|
73
|
+
end
|
74
|
+
|
75
|
+
alias_method :id, :#{name}
|
76
|
+
}, __FILE__, __LINE__)
|
77
|
+
end
|
78
|
+
|
79
|
+
def define_custom_method(clazz, name, on)
|
80
|
+
clazz.module_eval(%Q{
|
81
|
+
default_property :#{name} do |instance|
|
82
|
+
raise "Specifying custom id_property #{name} on none existing method #{on}" unless instance.respond_to?(:#{on})
|
83
|
+
instance.#{on}
|
84
|
+
end
|
85
|
+
|
86
|
+
def #{name}
|
87
|
+
default_property :#{name}
|
88
|
+
end
|
89
|
+
|
90
|
+
alias_method :id, :#{name}
|
91
|
+
}, __FILE__, __LINE__)
|
92
|
+
end
|
93
|
+
|
94
|
+
extend self
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
module ClassMethods
|
99
|
+
|
100
|
+
def find_by_id(key)
|
101
|
+
Neo4j::Node.load(key.to_i)
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
def id_property(name, conf = {})
|
106
|
+
TypeMethods.define_id_methods(self, name, conf)
|
107
|
+
|
108
|
+
constraint name, type: :unique
|
109
|
+
|
110
|
+
self.define_singleton_method(:find_by_id) do |key|
|
111
|
+
self.where(name => key).first
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
@@ -1,6 +1,5 @@
|
|
1
1
|
module Neo4j::ActiveNode
|
2
2
|
module Identity
|
3
|
-
|
4
3
|
def ==(o)
|
5
4
|
o.class == self.class && o.id == id
|
6
5
|
end
|
@@ -17,13 +16,9 @@ module Neo4j::ActiveNode
|
|
17
16
|
_persisted_node ? _persisted_node.neo_id : nil
|
18
17
|
end
|
19
18
|
|
20
|
-
# @return [String, nil] same as #neo_id
|
21
19
|
def id
|
22
20
|
id = neo_id
|
23
21
|
id.is_a?(Integer) ? id : nil
|
24
22
|
end
|
25
|
-
|
26
|
-
|
27
23
|
end
|
28
|
-
|
29
24
|
end
|
@@ -11,6 +11,7 @@ module Neo4j::ActiveNode::Initialize
|
|
11
11
|
@_persisted_node = persisted_node
|
12
12
|
changed_attributes && changed_attributes.clear
|
13
13
|
@attributes = attributes.merge(properties.stringify_keys)
|
14
|
+
self.default_properties=properties
|
14
15
|
@attributes = convert_properties_to :ruby, @attributes
|
15
16
|
end
|
16
17
|
|
@@ -59,20 +59,19 @@ module Neo4j
|
|
59
59
|
end
|
60
60
|
|
61
61
|
module ClassMethods
|
62
|
-
|
63
62
|
# Find all nodes/objects of this class
|
64
63
|
def all
|
65
64
|
self.query_as(:n).pluck(:n)
|
66
65
|
end
|
67
66
|
|
67
|
+
# Returns the first node of this class, sorted by ID. Note that this may not be the first node created since Neo4j recycles IDs.
|
68
68
|
def first
|
69
|
-
self.query_as(:n).limit(1).order('n
|
69
|
+
self.query_as(:n).limit(1).order('ID(n)').pluck(:n).first
|
70
70
|
end
|
71
71
|
|
72
|
+
# Returns the last node of this class, sorted by ID. Note that this may not be the first node created since Neo4j recycles IDs.
|
72
73
|
def last
|
73
|
-
|
74
|
-
final_count = count == 0 ? 0 : count - 1
|
75
|
-
self.query_as(:n).order('n.neo_id').skip(final_count).limit(1).pluck(:n).first
|
74
|
+
self.query_as(:n).order('ID(n) DESC').limit(1).pluck(:n).first
|
76
75
|
end
|
77
76
|
|
78
77
|
# @return [Fixnum] number of nodes of this class
|
@@ -81,15 +80,14 @@ module Neo4j
|
|
81
80
|
end
|
82
81
|
|
83
82
|
# Returns the object with the specified neo4j id.
|
84
|
-
# @param [String,Fixnum]
|
83
|
+
# @param [String,Fixnum] id of node to find
|
85
84
|
def find(id)
|
86
|
-
raise "Unknown argument #{id.class} in find method" if not [String, Fixnum].include?(id.class)
|
87
|
-
|
88
|
-
Neo4j::Node.load(id.to_i)
|
85
|
+
raise "Unknown argument #{id.class} in find method (expected String or Fixnum)" if not [String, Fixnum].include?(id.class)
|
86
|
+
find_by_id(id)
|
89
87
|
end
|
90
88
|
|
91
89
|
# Finds the first record matching the specified conditions. There is no implied ordering so if order matters, you should specify it yourself.
|
92
|
-
# @param [Hash]
|
90
|
+
# @param [Hash] args of arguments to find
|
93
91
|
def find_by(*args)
|
94
92
|
self.query_as(:n).where(n: eval(args.join)).limit(1).pluck(:n).first
|
95
93
|
end
|
@@ -100,21 +98,51 @@ module Neo4j
|
|
100
98
|
find_by(args) or raise RecordNotFound, "#{self.query_as(:n).where(n: a).limit(1).to_cypher} returned no results"
|
101
99
|
end
|
102
100
|
|
103
|
-
# Destroy all nodes
|
101
|
+
# Destroy all nodes and connected relationships
|
104
102
|
def destroy_all
|
105
103
|
self.neo4j_session._query("MATCH (n:`#{mapped_label_name}`)-[r]-() DELETE n,r")
|
106
104
|
self.neo4j_session._query("MATCH (n:`#{mapped_label_name}`) DELETE n")
|
107
105
|
end
|
108
106
|
|
109
107
|
# Creates a Neo4j index on given property
|
108
|
+
#
|
109
|
+
# This can also be done on the property directly, see Neo4j::ActiveNode::Property::ClassMethods#property.
|
110
|
+
#
|
110
111
|
# @param [Symbol] property the property we want a Neo4j index on
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
112
|
+
# @param [Hash] conf optional property configuration
|
113
|
+
#
|
114
|
+
# @example
|
115
|
+
# class Person
|
116
|
+
# include Neo4j::ActiveNode
|
117
|
+
# property :name
|
118
|
+
# index :name
|
119
|
+
# end
|
120
|
+
#
|
121
|
+
# @example with constraint
|
122
|
+
# class Person
|
123
|
+
# include Neo4j::ActiveNode
|
124
|
+
# property :name
|
125
|
+
#
|
126
|
+
# # below is same as: index :name, index: :exact, constraint: {type: :unique}
|
127
|
+
# index :name, constraint: {type: :unique}
|
128
|
+
# end
|
129
|
+
def index(property, conf = {})
|
130
|
+
Neo4j::Session.on_session_available do |_|
|
131
|
+
_index(property, conf)
|
132
|
+
end
|
133
|
+
@_indexed_properties ||= []
|
134
|
+
@_indexed_properties.push property unless @_indexed_properties.include? property
|
135
|
+
end
|
136
|
+
|
137
|
+
# Creates a neo4j constraint on this class for given property
|
138
|
+
#
|
139
|
+
# @example
|
140
|
+
# Person.constraint :name, type: :unique
|
141
|
+
#
|
142
|
+
def constraint(property, constraints, session = Neo4j::Session.current)
|
143
|
+
Neo4j::Session.on_session_available do |_|
|
144
|
+
label = Neo4j::Label.create(mapped_label_name)
|
145
|
+
label.create_constraint(property, constraints, session)
|
118
146
|
end
|
119
147
|
end
|
120
148
|
|
@@ -132,17 +160,30 @@ module Neo4j
|
|
132
160
|
@_label_name || self.to_s.to_sym
|
133
161
|
end
|
134
162
|
|
135
|
-
|
163
|
+
# @return [Neo4j::Label] the label for this class
|
164
|
+
def mapped_label
|
165
|
+
Neo4j::Label.create(mapped_label_name)
|
166
|
+
end
|
136
167
|
|
168
|
+
def indexed_properties
|
169
|
+
@_indexed_properties
|
137
170
|
end
|
138
171
|
|
172
|
+
|
139
173
|
protected
|
140
174
|
|
141
|
-
def _index(property)
|
175
|
+
def _index(property, conf)
|
142
176
|
mapped_labels.each do |label|
|
143
177
|
# make sure the property is not indexed twice
|
144
178
|
existing = label.indexes[:property_keys]
|
145
|
-
|
179
|
+
|
180
|
+
# In neo4j constraint automatically creates an index
|
181
|
+
if conf[:constraint]
|
182
|
+
constraint(property, conf[:constraint])
|
183
|
+
else
|
184
|
+
label.create_index(property) unless existing.flatten.include?(property)
|
185
|
+
end
|
186
|
+
|
146
187
|
end
|
147
188
|
end
|
148
189
|
|
@@ -150,10 +191,6 @@ module Neo4j
|
|
150
191
|
mapped_label_names.map{|label_name| Neo4j::Label.create(label_name)}
|
151
192
|
end
|
152
193
|
|
153
|
-
def mapped_label
|
154
|
-
Neo4j::Label.create(mapped_label_name)
|
155
|
-
end
|
156
|
-
|
157
194
|
def set_mapped_label_name(name)
|
158
195
|
@_label_name = name.to_sym
|
159
196
|
end
|
@@ -28,6 +28,7 @@ module Neo4j::ActiveNode
|
|
28
28
|
# @return true
|
29
29
|
def create_model(*)
|
30
30
|
set_timestamps
|
31
|
+
create_magic_properties
|
31
32
|
properties = convert_properties_to :db, props
|
32
33
|
node = _create_node(properties)
|
33
34
|
init_on_load(node, node.props)
|
@@ -129,7 +130,9 @@ module Neo4j::ActiveNode
|
|
129
130
|
|
130
131
|
def _create_node(*args)
|
131
132
|
session = self.class.neo4j_session
|
132
|
-
props =
|
133
|
+
props = self.class.default_property_values(self)
|
134
|
+
props.merge!(args[0]) if args[0].is_a?(Hash)
|
135
|
+
set_classname(props)
|
133
136
|
labels = self.class.mapped_label_names
|
134
137
|
session.create_node(props, labels)
|
135
138
|
end
|
@@ -193,15 +196,25 @@ module Neo4j::ActiveNode
|
|
193
196
|
end
|
194
197
|
alias_method :update_attributes!, :update!
|
195
198
|
|
199
|
+
def cache_key
|
200
|
+
if self.new_record?
|
201
|
+
"#{self.class.model_name.cache_key}/new"
|
202
|
+
elsif self.respond_to?(:updated_at) && !self.updated_at.blank?
|
203
|
+
"#{self.class.model_name.cache_key}/#{neo_id}-#{self.updated_at.utc.to_s(:number)}"
|
204
|
+
else
|
205
|
+
"#{self.class.model_name.cache_key}/#{neo_id}"
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
196
209
|
module ClassMethods
|
197
210
|
# Creates a saves a new node
|
198
211
|
# @param [Hash] props the properties the new node should have
|
199
212
|
def create(props = {})
|
200
|
-
|
213
|
+
association_props = extract_association_attributes!(props)
|
201
214
|
|
202
215
|
new(props).tap do |obj|
|
203
216
|
obj.save
|
204
|
-
|
217
|
+
association_props.each do |prop, value|
|
205
218
|
obj.send("#{prop}=", value)
|
206
219
|
end
|
207
220
|
end
|
@@ -210,12 +223,12 @@ module Neo4j::ActiveNode
|
|
210
223
|
# Same as #create, but raises an error if there is a problem during save.
|
211
224
|
def create!(*args)
|
212
225
|
props = args[0] || {}
|
213
|
-
|
226
|
+
association_props = extract_association_attributes!(props)
|
214
227
|
|
215
228
|
new(*args).tap do |o|
|
216
229
|
yield o if block_given?
|
217
230
|
o.save!
|
218
|
-
|
231
|
+
association_props.each do |prop, value|
|
219
232
|
o.send("#{prop}=", value)
|
220
233
|
end
|
221
234
|
end
|
@@ -229,10 +242,18 @@ module Neo4j::ActiveNode
|
|
229
242
|
|
230
243
|
private
|
231
244
|
|
245
|
+
def create_magic_properties
|
246
|
+
|
247
|
+
end
|
248
|
+
|
232
249
|
def update_magic_properties
|
233
250
|
self.updated_at = DateTime.now if respond_to?(:updated_at=) && changed?
|
234
251
|
end
|
235
252
|
|
253
|
+
def set_classname(props)
|
254
|
+
props[Neo4j::Config.class_name_property] = self.class.name if self.class.cached_class?
|
255
|
+
end
|
256
|
+
|
236
257
|
def assign_attributes(attributes)
|
237
258
|
attributes.each do |attribute, value|
|
238
259
|
send("#{attribute}=", value)
|