active_node 2.1.1 → 2.2.0
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 +4 -4
- data/.travis.yml +5 -2
- data/lib/active_node.rb +6 -0
- data/lib/active_node/associations/association.rb +2 -3
- data/lib/active_node/base.rb +7 -1
- data/lib/active_node/dirty.rb +26 -0
- data/lib/active_node/errors.rb +5 -1
- data/lib/active_node/graph.rb +213 -4
- data/lib/active_node/graph/finder_methods.rb +326 -0
- data/lib/active_node/graph/query_methods.rb +28 -0
- data/lib/active_node/persistence.rb +29 -13
- data/lib/active_node/reflection.rb +24 -23
- data/lib/active_node/validations.rb +3 -1
- data/lib/active_node/validations/{uniqueness_validator.rb → uniqueness.rb} +9 -1
- data/lib/active_node/version.rb +1 -1
- data/spec/functional/associations_spec.rb +2 -2
- data/spec/functional/graph_spec.rb +35 -0
- data/spec/functional/persistence_spec.rb +5 -5
- metadata +8 -6
- data/lib/active_node/graph/builder.rb +0 -119
- data/spec/functional/graph/builder_spec.rb +0 -34
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 19347532b691863f735cdb799989cf68a1bc624b
|
|
4
|
+
data.tar.gz: 71833a5ff1029940a6965c90c9f86504747f5c6a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9017661492388db4212a1674c9364ff182ea756f48252a8de0221e1194e0697ab2faf2964182d6f5ac310ea9deee3e4045556389ab50ecac4898747009ef301a
|
|
7
|
+
data.tar.gz: 63610493b0a11263928de9fab5bbe3f79741a82e54dd8869f87d9d733444046da275816d803d78946e6f42f7f0cafb213fcd57bc1fc6d88834cb69a75eb1e453
|
data/.travis.yml
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
script: "
|
|
1
|
+
script: "bundle exec rake neo4j:install['enterprise','2.0.3'] neo4j:start spec --trace"
|
|
2
2
|
language: ruby
|
|
3
3
|
rvm:
|
|
4
|
-
- 2.
|
|
4
|
+
- 2.1.1
|
|
5
|
+
addons:
|
|
6
|
+
code_climate:
|
|
7
|
+
repo_token: 88fa9b8eb0bfc48e8c5077253d829e94af9f76aca60df9e75ae0e50a4c7cd9f0
|
data/lib/active_node.rb
CHANGED
|
@@ -8,11 +8,17 @@ module ActiveNode
|
|
|
8
8
|
autoload :Persistence
|
|
9
9
|
autoload :Validations
|
|
10
10
|
autoload :Reflection
|
|
11
|
+
autoload :Dirty
|
|
11
12
|
autoload :VERSION
|
|
12
13
|
autoload :Neo
|
|
13
14
|
autoload :Relationship
|
|
14
15
|
autoload :Graph
|
|
15
16
|
|
|
17
|
+
autoload_under 'graph' do
|
|
18
|
+
autoload :QueryMethods
|
|
19
|
+
autoload :FinderMethods
|
|
20
|
+
end
|
|
21
|
+
|
|
16
22
|
eager_autoload do
|
|
17
23
|
autoload :ActiveNodeError, 'active_node/errors'
|
|
18
24
|
autoload :Associations
|
|
@@ -40,9 +40,8 @@ module ActiveNode
|
|
|
40
40
|
reflection.klass
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
|
|
44
43
|
def rel(*associations)
|
|
45
|
-
owner.
|
|
44
|
+
owner.includes!(reflection.name => associations)
|
|
46
45
|
end
|
|
47
46
|
|
|
48
47
|
# Implements the writer method, e.g. foo.items= for Foo.has_many :items
|
|
@@ -85,7 +84,7 @@ module ActiveNode
|
|
|
85
84
|
def save(fresh=false)
|
|
86
85
|
#return unless @dirty
|
|
87
86
|
#delete all relations missing in new target
|
|
88
|
-
original_rels = fresh ? [] :
|
|
87
|
+
original_rels = fresh ? [] : owner.class.includes(reflection.name).build(owner.id).first.association(reflection.name).rels_reader
|
|
89
88
|
original_rels.each do |r|
|
|
90
89
|
unless ids_reader.include? r.other.id
|
|
91
90
|
Neo.db.delete_relationship(r.id)
|
data/lib/active_node/base.rb
CHANGED
|
@@ -3,7 +3,11 @@ require 'active_node/errors'
|
|
|
3
3
|
|
|
4
4
|
module ActiveNode
|
|
5
5
|
class Base
|
|
6
|
-
include ActiveAttr::
|
|
6
|
+
include ActiveAttr::BasicModel
|
|
7
|
+
include ActiveAttr::Attributes
|
|
8
|
+
include ActiveAttr::MassAssignment
|
|
9
|
+
include ActiveAttr::TypecastedAttributes
|
|
10
|
+
include Dirty
|
|
7
11
|
include Persistence
|
|
8
12
|
include Validations
|
|
9
13
|
include Callbacks
|
|
@@ -15,4 +19,6 @@ module ActiveNode
|
|
|
15
19
|
Class.new(super_class=self) { define_singleton_method(:label) { klass_name } }
|
|
16
20
|
end
|
|
17
21
|
end
|
|
22
|
+
|
|
23
|
+
ActiveSupport.run_load_hooks(:active_node, Base)
|
|
18
24
|
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module ActiveNode
|
|
2
|
+
module Dirty
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
include ActiveModel::Dirty
|
|
5
|
+
|
|
6
|
+
module ClassMethods
|
|
7
|
+
def attribute!(name, options={})
|
|
8
|
+
super(name, options)
|
|
9
|
+
define_method("#{name}=") do |value|
|
|
10
|
+
send("#{name}_will_change!") unless value == read_attribute(name)
|
|
11
|
+
super(value)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(attributes = nil, options = {})
|
|
17
|
+
super(attributes, options)
|
|
18
|
+
(@changed_attributes || {}).clear
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def save(*)
|
|
22
|
+
@previously_changed = changes
|
|
23
|
+
@changed_attributes.clear
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/active_node/errors.rb
CHANGED
|
@@ -6,7 +6,11 @@ module ActiveNode
|
|
|
6
6
|
class ActiveNodeError < StandardError
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
-
# Raised
|
|
9
|
+
# Raised when Active Node cannot find record by given id or set of ids.
|
|
10
|
+
class RecordNotFound < ActiveNodeError
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Raised by ActiveNode::Base.save! and ActiveRecord::Base.create! methods when record cannot be
|
|
10
14
|
# saved because record is invalid.
|
|
11
15
|
class RecordNotSaved < ActiveNodeError
|
|
12
16
|
end
|
data/lib/active_node/graph.rb
CHANGED
|
@@ -1,7 +1,216 @@
|
|
|
1
1
|
module ActiveNode
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
class Graph
|
|
3
|
+
include Neography::Rest::Helpers
|
|
4
|
+
include FinderMethods, QueryMethods
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
attr_reader :reflections, :matches, :klass, :loaded
|
|
7
|
+
alias :loaded? :loaded
|
|
8
|
+
|
|
9
|
+
delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, to: :to_a
|
|
10
|
+
|
|
11
|
+
def initialize klass, *includes
|
|
12
|
+
@klass = klass if klass < ActiveNode::Base
|
|
13
|
+
@matches = []
|
|
14
|
+
@reflections =[]
|
|
15
|
+
@object_cache = {}
|
|
16
|
+
@relationship_cache = {}
|
|
17
|
+
@loaded_assoc_cache = {}
|
|
18
|
+
@where = {}
|
|
19
|
+
@includes = includes
|
|
20
|
+
@offsets = {}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def all
|
|
24
|
+
self
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def count
|
|
28
|
+
to_a.count
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def includes *includes
|
|
32
|
+
@includes += includes
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def where hash
|
|
37
|
+
@where.merge! hash if hash
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def limit count
|
|
42
|
+
@limit = count
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def build *objects
|
|
47
|
+
find objects.map { |o| o.is_a?(ActiveNode::Base) ? o.id.tap { |id| @object_cache[id]=o } : extract_id(o) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def load
|
|
51
|
+
parse_results execute unless loaded?
|
|
52
|
+
self
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def to_a
|
|
56
|
+
load
|
|
57
|
+
@records
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Compares two relations for equality.
|
|
61
|
+
def ==(other)
|
|
62
|
+
case other
|
|
63
|
+
when Graph
|
|
64
|
+
other.to_cypher == to_cypher
|
|
65
|
+
when Array
|
|
66
|
+
to_a == other
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def pretty_print(q)
|
|
71
|
+
q.pp(self.to_a)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# def first
|
|
75
|
+
# limit 1
|
|
76
|
+
# to_a.first
|
|
77
|
+
# end
|
|
78
|
+
|
|
79
|
+
def delete_all
|
|
80
|
+
Neo.db.execute_query("#{initial_match} OPTIONAL MATCH (n0)-[r]-() DELETE n0,r")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
def query
|
|
85
|
+
parse_paths(:n0, @klass, @includes)
|
|
86
|
+
@matches.join ' '
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def conditions
|
|
90
|
+
cond = @where.map { |key, value| "#{cond_left(key)} #{cond_operator(value)} {#{key}}" }
|
|
91
|
+
"where #{cond.join ' and '}" unless cond.empty?
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def cond_left key
|
|
95
|
+
key.to_s == 'id' ? "id(n0)" : "n0.#{key}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def cond_operator value
|
|
99
|
+
value.is_a?(Array) ? 'in' : '='
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def limit_cond
|
|
103
|
+
"limit #{@limit}" if @limit
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def initial_match
|
|
107
|
+
"match (n0#{label @klass})"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def execute
|
|
111
|
+
Neo.db.execute_query(to_cypher, sanitize_where)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def sanitize_where
|
|
115
|
+
@where.each { |key, value| @where[key] = extract_id(value) if key.to_s == 'id'}
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def to_cypher
|
|
119
|
+
[initial_match, conditions, "with n0", limit_cond, query, 'return', list_with_rel(@reflections.size), 'order by', created_at_list(@reflections.size)].compact.join ' '
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def parse_results results
|
|
123
|
+
records = Set.new
|
|
124
|
+
results['data'].each do |record|
|
|
125
|
+
records << wrap(record.first, @klass)
|
|
126
|
+
@reflections.each_with_index do |reflection, index|
|
|
127
|
+
node_rel = record[2*index+1]
|
|
128
|
+
node_rel = node_rel.last if node_rel.is_a? Array
|
|
129
|
+
next unless node_rel
|
|
130
|
+
owner = @object_cache[owner_id node_rel, reflection.direction]
|
|
131
|
+
node = wrap record[2*index + 2], reflection.klass
|
|
132
|
+
rel = reflection.klass.create_rel node_rel, node
|
|
133
|
+
assoc = owner.association(reflection.name)
|
|
134
|
+
assoc.rels_writer((assoc.rel_target || []) << rel) unless previously_loaded?(assoc)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
@loaded = true
|
|
138
|
+
@records = records.to_a
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def previously_loaded?(assoc)
|
|
142
|
+
@loaded_assoc_cache[assoc] = assoc.rel_target unless @loaded_assoc_cache.key? assoc
|
|
143
|
+
@loaded_assoc_cache[assoc]
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def wrap(record, klass)
|
|
147
|
+
@object_cache[extract_id record] ||= ActiveNode::Base.wrap(record, klass)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def owner_id relationship, direction
|
|
151
|
+
extract_id relationship[direction == :incoming ? 'end' : 'start']
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def parse_paths as, klass, includes
|
|
155
|
+
if includes.is_a?(Hash)
|
|
156
|
+
includes.each do |key, value|
|
|
157
|
+
if (value.is_a?(String) || value.is_a?(Numeric))
|
|
158
|
+
add_match(as, klass, key, value)
|
|
159
|
+
else
|
|
160
|
+
parse_paths(as, klass, key)
|
|
161
|
+
parse_paths(latest_alias, @reflections.last.klass, value)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
elsif includes.is_a?(Array)
|
|
165
|
+
includes.each { |inc| parse_paths(as, klass, inc) }
|
|
166
|
+
else
|
|
167
|
+
add_match(as, klass, includes)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def add_match from, klass, key, multiplicity=nil
|
|
172
|
+
reflection = klass.reflect_on_association(key)
|
|
173
|
+
@reflections << reflection
|
|
174
|
+
matches << match(from, reflection, multiplicity)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def latest_alias
|
|
178
|
+
"n#{@reflections.size}"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def match(start_var, reflection, multiplicity)
|
|
182
|
+
"optional match (#{start_var})#{'<' if reflection.direction == :incoming}-[r#{@reflections.size}:#{reflection.type}#{multiplicity(multiplicity)}]-#{'>' if reflection.direction == :outgoing}(#{latest_alias}#{label reflection.klass})"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def label klass
|
|
186
|
+
":`#{klass.label}`" if klass
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def multiplicity multiplicity
|
|
190
|
+
multiplicity.is_a?(Numeric) ? "*1..#{multiplicity}" : multiplicity
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def list_with_rel num
|
|
194
|
+
comma_sep_list(num) { |i| [("r#{i}" if i>0), "n#{i}"] }
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def comma_sep_list num, &block
|
|
198
|
+
(0..num).map(&block).flatten.compact.join(', ')
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def created_at_list num
|
|
202
|
+
comma_sep_list(num) { |i| "n#{i}.created_at" }
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def extract_id(id)
|
|
206
|
+
case id
|
|
207
|
+
when Array
|
|
208
|
+
id.map { |i| extract_id(i) }
|
|
209
|
+
when ActiveNode::Base
|
|
210
|
+
id.id
|
|
211
|
+
else
|
|
212
|
+
get_id(id).to_i
|
|
213
|
+
end
|
|
214
|
+
end
|
|
6
215
|
end
|
|
7
|
-
end
|
|
216
|
+
end
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
module ActiveNode
|
|
2
|
+
module FinderMethods
|
|
3
|
+
# Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]).
|
|
4
|
+
# If no record can be found for all of the listed ids, then RecordNotFound will be raised. If the primary key
|
|
5
|
+
# is an integer, find by id coerces its arguments using +to_i+.
|
|
6
|
+
#
|
|
7
|
+
# Person.find(1) # returns the object for ID = 1
|
|
8
|
+
# Person.find("1") # returns the object for ID = 1
|
|
9
|
+
# Person.find("31-sarah") # returns the object for ID = 31
|
|
10
|
+
# Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
|
|
11
|
+
# Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
|
|
12
|
+
# Person.find([1]) # returns an array for the object with ID = 1
|
|
13
|
+
# Person.where("administrator = 1").order("created_on DESC").find(1)
|
|
14
|
+
#
|
|
15
|
+
# <tt>ActiveRecord::RecordNotFound</tt> will be raised if one or more ids are not found.
|
|
16
|
+
#
|
|
17
|
+
# NOTE: The returned records may not be in the same order as the ids you
|
|
18
|
+
# provide since database rows are unordered. You'd need to provide an explicit <tt>order</tt>
|
|
19
|
+
# option if you want the results are sorted.
|
|
20
|
+
#
|
|
21
|
+
# ==== Find with lock
|
|
22
|
+
#
|
|
23
|
+
# Example for find with a lock: Imagine two concurrent transactions:
|
|
24
|
+
# each will read <tt>person.visits == 2</tt>, add 1 to it, and save, resulting
|
|
25
|
+
# in two saves of <tt>person.visits = 3</tt>. By locking the row, the second
|
|
26
|
+
# transaction has to wait until the first is finished; we get the
|
|
27
|
+
# expected <tt>person.visits == 4</tt>.
|
|
28
|
+
#
|
|
29
|
+
# Person.transaction do
|
|
30
|
+
# person = Person.lock(true).find(1)
|
|
31
|
+
# person.visits += 1
|
|
32
|
+
# person.save!
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# ==== Variations of +find+
|
|
36
|
+
#
|
|
37
|
+
# Person.where(name: 'Spartacus', rating: 4)
|
|
38
|
+
# # returns a chainable list (which can be empty).
|
|
39
|
+
#
|
|
40
|
+
# Person.find_by(name: 'Spartacus', rating: 4)
|
|
41
|
+
# # returns the first item or nil.
|
|
42
|
+
#
|
|
43
|
+
# Person.where(name: 'Spartacus', rating: 4).first_or_initialize
|
|
44
|
+
# # returns the first item or returns a new instance (requires you call .save to persist against the database).
|
|
45
|
+
#
|
|
46
|
+
# Person.where(name: 'Spartacus', rating: 4).first_or_create
|
|
47
|
+
# # returns the first item or creates it and returns it, available since Rails 3.2.1.
|
|
48
|
+
#
|
|
49
|
+
# ==== Alternatives for +find+
|
|
50
|
+
#
|
|
51
|
+
# Person.where(name: 'Spartacus', rating: 4).exists?(conditions = :none)
|
|
52
|
+
# # returns a boolean indicating if any record with the given conditions exist.
|
|
53
|
+
#
|
|
54
|
+
# Person.where(name: 'Spartacus', rating: 4).select("field1, field2, field3")
|
|
55
|
+
# # returns a chainable list of instances with only the mentioned fields.
|
|
56
|
+
#
|
|
57
|
+
# Person.where(name: 'Spartacus', rating: 4).ids
|
|
58
|
+
# # returns an Array of ids, available since Rails 3.2.1.
|
|
59
|
+
#
|
|
60
|
+
# Person.where(name: 'Spartacus', rating: 4).pluck(:field1, :field2)
|
|
61
|
+
# # returns an Array of the required fields, available since Rails 3.1.
|
|
62
|
+
def find(*args)
|
|
63
|
+
if block_given?
|
|
64
|
+
to_a.find { |*block_args| yield(*block_args) }
|
|
65
|
+
else
|
|
66
|
+
find_with_ids(*args)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Finds the first record matching the specified conditions. There
|
|
71
|
+
# is no implied ordering so if order matters, you should specify it
|
|
72
|
+
# yourself.
|
|
73
|
+
#
|
|
74
|
+
# If no record is found, returns <tt>nil</tt>.
|
|
75
|
+
#
|
|
76
|
+
# Post.find_by name: 'Spartacus', rating: 4
|
|
77
|
+
# Post.find_by "published_at < ?", 2.weeks.ago
|
|
78
|
+
def find_by(*args)
|
|
79
|
+
where(*args).take
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Like <tt>find_by</tt>, except that if no record is found, raises
|
|
83
|
+
# an <tt>ActiveRecord::RecordNotFound</tt> error.
|
|
84
|
+
def find_by!(*args)
|
|
85
|
+
where(*args).take!
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Gives a record (or N records if a parameter is supplied) without any implied
|
|
89
|
+
# order. The order will depend on the database implementation.
|
|
90
|
+
# If an order is supplied it will be respected.
|
|
91
|
+
#
|
|
92
|
+
# Person.take # returns an object fetched by SELECT * FROM people LIMIT 1
|
|
93
|
+
# Person.take(5) # returns 5 objects fetched by SELECT * FROM people LIMIT 5
|
|
94
|
+
# Person.where(["name LIKE '%?'", name]).take
|
|
95
|
+
def take(limit = nil)
|
|
96
|
+
limit ? limit(limit).to_a : find_take
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Same as +take+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
|
|
100
|
+
# is found. Note that <tt>take!</tt> accepts no arguments.
|
|
101
|
+
def take!
|
|
102
|
+
take or raise RecordNotFound
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Find the first record (or first N records if a parameter is supplied).
|
|
106
|
+
# If no order is defined it will order by primary key.
|
|
107
|
+
#
|
|
108
|
+
# Person.first # returns the first object fetched by SELECT * FROM people
|
|
109
|
+
# Person.where(["user_name = ?", user_name]).first
|
|
110
|
+
# Person.where(["user_name = :u", { u: user_name }]).first
|
|
111
|
+
# Person.order("created_on DESC").offset(5).first
|
|
112
|
+
# Person.first(3) # returns the first three objects fetched by SELECT * FROM people LIMIT 3
|
|
113
|
+
#
|
|
114
|
+
# ==== Rails 3
|
|
115
|
+
#
|
|
116
|
+
# Person.first # SELECT "people".* FROM "people" LIMIT 1
|
|
117
|
+
#
|
|
118
|
+
# NOTE: Rails 3 may not order this query by the primary key and the order
|
|
119
|
+
# will depend on the database implementation. In order to ensure that behavior,
|
|
120
|
+
# use <tt>User.order(:id).first</tt> instead.
|
|
121
|
+
#
|
|
122
|
+
# ==== Rails 4
|
|
123
|
+
#
|
|
124
|
+
# Person.first # SELECT "people".* FROM "people" ORDER BY "people"."id" ASC LIMIT 1
|
|
125
|
+
#
|
|
126
|
+
def first(limit = nil)
|
|
127
|
+
if limit
|
|
128
|
+
find_nth_with_limit(offset_value, limit)
|
|
129
|
+
else
|
|
130
|
+
find_nth(:first, offset_value)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Same as +first+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
|
|
135
|
+
# is found. Note that <tt>first!</tt> accepts no arguments.
|
|
136
|
+
def first!
|
|
137
|
+
first or raise RecordNotFound
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Find the last record (or last N records if a parameter is supplied).
|
|
141
|
+
# If no order is defined it will order by primary key.
|
|
142
|
+
#
|
|
143
|
+
# Person.last # returns the last object fetched by SELECT * FROM people
|
|
144
|
+
# Person.where(["user_name = ?", user_name]).last
|
|
145
|
+
# Person.order("created_on DESC").offset(5).last
|
|
146
|
+
# Person.last(3) # returns the last three objects fetched by SELECT * FROM people.
|
|
147
|
+
#
|
|
148
|
+
# Take note that in that last case, the results are sorted in ascending order:
|
|
149
|
+
#
|
|
150
|
+
# [#<Person id:2>, #<Person id:3>, #<Person id:4>]
|
|
151
|
+
#
|
|
152
|
+
# and not:
|
|
153
|
+
#
|
|
154
|
+
# [#<Person id:4>, #<Person id:3>, #<Person id:2>]
|
|
155
|
+
def last(limit = nil)
|
|
156
|
+
if limit
|
|
157
|
+
if order_values.empty?
|
|
158
|
+
order(id: :desc).limit(limit).reverse
|
|
159
|
+
else
|
|
160
|
+
to_a.last(limit)
|
|
161
|
+
end
|
|
162
|
+
else
|
|
163
|
+
find_last
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Same as +last+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
|
|
168
|
+
# is found. Note that <tt>last!</tt> accepts no arguments.
|
|
169
|
+
def last!
|
|
170
|
+
last or raise RecordNotFound
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Returns +true+ if a record exists in the table that matches the +id+ or
|
|
174
|
+
# conditions given, or +false+ otherwise. The argument can take six forms:
|
|
175
|
+
#
|
|
176
|
+
# * Integer - Finds the record with this primary key.
|
|
177
|
+
# * String - Finds the record with a primary key corresponding to this
|
|
178
|
+
# string (such as <tt>'5'</tt>).
|
|
179
|
+
# * Array - Finds the record that matches these +find+-style conditions
|
|
180
|
+
# (such as <tt>['name LIKE ?', "%#{query}%"]</tt>).
|
|
181
|
+
# * Hash - Finds the record that matches these +find+-style conditions
|
|
182
|
+
# (such as <tt>{name: 'David'}</tt>).
|
|
183
|
+
# * +false+ - Returns always +false+.
|
|
184
|
+
# * No args - Returns +false+ if the table is empty, +true+ otherwise.
|
|
185
|
+
#
|
|
186
|
+
# For more information about specifying conditions as a hash or array,
|
|
187
|
+
# see the Conditions section in the introduction to <tt>ActiveRecord::Base</tt>.
|
|
188
|
+
#
|
|
189
|
+
# Note: You can't pass in a condition as a string (like <tt>name =
|
|
190
|
+
# 'Jamie'</tt>), since it would be sanitized and then queried against
|
|
191
|
+
# the primary key column, like <tt>id = 'name = \'Jamie\''</tt>.
|
|
192
|
+
#
|
|
193
|
+
# Person.exists?(5)
|
|
194
|
+
# Person.exists?('5')
|
|
195
|
+
# Person.exists?(['name LIKE ?', "%#{query}%"])
|
|
196
|
+
# Person.exists?(id: [1, 4, 8])
|
|
197
|
+
# Person.exists?(name: 'David')
|
|
198
|
+
# Person.exists?(false)
|
|
199
|
+
# Person.exists?
|
|
200
|
+
# def exists?(conditions = :none)
|
|
201
|
+
# conditions = conditions.id if Base === conditions
|
|
202
|
+
# return false if !conditions
|
|
203
|
+
#
|
|
204
|
+
# relation = apply_join_dependency(self, construct_join_dependency)
|
|
205
|
+
# return false if ActiveRecord::NullRelation === relation
|
|
206
|
+
#
|
|
207
|
+
# relation = relation.except(:select, :order).select(ONE_AS_ONE).limit(1)
|
|
208
|
+
#
|
|
209
|
+
# case conditions
|
|
210
|
+
# when Array, Hash
|
|
211
|
+
# relation = relation.where(conditions)
|
|
212
|
+
# else
|
|
213
|
+
# relation = relation.where(table[primary_key].eq(conditions)) if conditions != :none
|
|
214
|
+
# end
|
|
215
|
+
#
|
|
216
|
+
# connection.select_value(relation, "#{name} Exists", relation.bind_values) ? true : false
|
|
217
|
+
# end
|
|
218
|
+
|
|
219
|
+
# This method is called whenever no records are found with either a single
|
|
220
|
+
# id or multiple ids and raises a +ActiveRecord::RecordNotFound+ exception.
|
|
221
|
+
#
|
|
222
|
+
# The error message is different depending on whether a single id or
|
|
223
|
+
# multiple ids are provided. If multiple ids are provided, then the number
|
|
224
|
+
# of results obtained should be provided in the +result_size+ argument and
|
|
225
|
+
# the expected number of results should be provided in the +expected_size+
|
|
226
|
+
# argument.
|
|
227
|
+
def raise_record_not_found_exception!(ids, result_size, expected_size) #:nodoc:
|
|
228
|
+
if Array(ids).size == 1
|
|
229
|
+
error = "Couldn't find #{@klass.name} with 'id'=#{ids}#{conditions}"
|
|
230
|
+
else
|
|
231
|
+
error = "Couldn't find all #{@klass.name.pluralize} with 'id': "
|
|
232
|
+
error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})"
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
raise RecordNotFound, error
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
protected
|
|
239
|
+
|
|
240
|
+
def find_with_ids(*ids)
|
|
241
|
+
expects_array = ids.first.kind_of?(Array)
|
|
242
|
+
return ids.first if expects_array && ids.first.empty?
|
|
243
|
+
|
|
244
|
+
ids = ids.flatten.compact.uniq
|
|
245
|
+
|
|
246
|
+
case ids.size
|
|
247
|
+
when 0
|
|
248
|
+
raise RecordNotFound, "Couldn't find #{@klass.name} without an ID"
|
|
249
|
+
when 1
|
|
250
|
+
result = find_one(ids.first)
|
|
251
|
+
expects_array ? [result] : result
|
|
252
|
+
else
|
|
253
|
+
find_some(ids)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def find_one(id)
|
|
258
|
+
id = id.id if ActiveNode::Base === id
|
|
259
|
+
|
|
260
|
+
record = where(id: id).take
|
|
261
|
+
|
|
262
|
+
raise_record_not_found_exception!(id, 0, 1) unless record
|
|
263
|
+
|
|
264
|
+
record
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def find_some(ids)
|
|
268
|
+
result = where(id: ids).to_a
|
|
269
|
+
|
|
270
|
+
expected_size =
|
|
271
|
+
if limit_value && ids.size > limit_value
|
|
272
|
+
limit_value
|
|
273
|
+
else
|
|
274
|
+
ids.size
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# 11 ids with limit 3, offset 9 should give 2 results.
|
|
278
|
+
if offset_value && (ids.size - offset_value < expected_size)
|
|
279
|
+
expected_size = ids.size - offset_value
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
if result.size == expected_size
|
|
283
|
+
result
|
|
284
|
+
else
|
|
285
|
+
raise_record_not_found_exception!(ids, result.size, expected_size)
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def find_take
|
|
290
|
+
if loaded?
|
|
291
|
+
@records.first
|
|
292
|
+
else
|
|
293
|
+
@take ||= limit(1).to_a.first
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def find_nth(ordinal, offset)
|
|
298
|
+
if loaded?
|
|
299
|
+
@records.send(ordinal)
|
|
300
|
+
else
|
|
301
|
+
@offsets[offset] ||= find_nth_with_limit(offset, 1).first
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def find_nth_with_limit(offset, limit)
|
|
306
|
+
if order_values.empty?
|
|
307
|
+
order(:id).limit(limit).offset(offset).to_a
|
|
308
|
+
else
|
|
309
|
+
limit(limit).offset(offset).to_a
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def find_last
|
|
314
|
+
if loaded?
|
|
315
|
+
@records.last
|
|
316
|
+
else
|
|
317
|
+
@last ||=
|
|
318
|
+
if limit_value
|
|
319
|
+
to_a.last
|
|
320
|
+
else
|
|
321
|
+
reverse_order.limit(1).to_a.first
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module ActiveNode
|
|
2
|
+
module QueryMethods
|
|
3
|
+
def order(*args)
|
|
4
|
+
self
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def offset(value)
|
|
8
|
+
self
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def reverse_order
|
|
12
|
+
self
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
#TODO temporary stubbing
|
|
16
|
+
def offset_value
|
|
17
|
+
0
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def order_values
|
|
21
|
+
[]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def limit_value
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'active_support/core_ext/hash/indifferent_access'
|
|
2
|
+
require 'neography'
|
|
2
3
|
|
|
3
4
|
module ActiveNode
|
|
4
5
|
module Persistence
|
|
@@ -11,24 +12,17 @@ module ActiveNode
|
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
module ClassMethods
|
|
15
|
+
delegate :all, :first, :where, :limit, :includes, :delete_all, :build, :find, :offset, :count, :order, to: :graph
|
|
16
|
+
|
|
14
17
|
def timestamps
|
|
15
18
|
attribute :created_at, type: String
|
|
16
19
|
attribute :updated_at, type: String
|
|
17
20
|
end
|
|
18
21
|
|
|
19
|
-
def find ids, options={}
|
|
20
|
-
array = ActiveNode::Graph::Builder.new(self, *options[:include]).build(*ids)
|
|
21
|
-
ids.is_a?(Array) ? array : array.first
|
|
22
|
-
end
|
|
23
|
-
|
|
24
22
|
def find_by_cypher query, params={}, klass=nil
|
|
25
23
|
wrap(Neo.db.execute_query(query, params)['data'].map(&:first), klass)
|
|
26
24
|
end
|
|
27
25
|
|
|
28
|
-
def all
|
|
29
|
-
new_instances(Neo.db.get_nodes_labeled(label), self)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
26
|
def label
|
|
33
27
|
name
|
|
34
28
|
end
|
|
@@ -50,6 +44,10 @@ module ActiveNode
|
|
|
50
44
|
klass && klass < ActiveNode::Base && klass || default_klass
|
|
51
45
|
end
|
|
52
46
|
|
|
47
|
+
def graph
|
|
48
|
+
ActiveNode::Graph.new(self)
|
|
49
|
+
end
|
|
50
|
+
|
|
53
51
|
private
|
|
54
52
|
def new_instance node, klass=nil
|
|
55
53
|
(klass || find_suitable_class(Neo.db.get_node_labels(node))).try(:new, data(node), :declared?)
|
|
@@ -74,7 +72,7 @@ module ActiveNode
|
|
|
74
72
|
|
|
75
73
|
def []=(attr, value)
|
|
76
74
|
if declared? attr
|
|
77
|
-
|
|
75
|
+
write_attr attr, value
|
|
78
76
|
else
|
|
79
77
|
@hash[attr]=value
|
|
80
78
|
end
|
|
@@ -88,6 +86,10 @@ module ActiveNode
|
|
|
88
86
|
id.to_s if persisted?
|
|
89
87
|
end
|
|
90
88
|
|
|
89
|
+
def to_key
|
|
90
|
+
id
|
|
91
|
+
end
|
|
92
|
+
|
|
91
93
|
def persisted?
|
|
92
94
|
id.present? && !destroyed?
|
|
93
95
|
end
|
|
@@ -103,6 +105,7 @@ module ActiveNode
|
|
|
103
105
|
|
|
104
106
|
def save(*)
|
|
105
107
|
create_or_update
|
|
108
|
+
super
|
|
106
109
|
end
|
|
107
110
|
|
|
108
111
|
alias save! save
|
|
@@ -129,8 +132,13 @@ module ActiveNode
|
|
|
129
132
|
related(:outgoing, type, klass)
|
|
130
133
|
end
|
|
131
134
|
|
|
132
|
-
def
|
|
133
|
-
|
|
135
|
+
def includes!(includes)
|
|
136
|
+
new_record? ? self : self.class.includes(includes).build(self).first
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def update_attributes attributes
|
|
140
|
+
attributes.each { |key, value| respond_to_writer?(attr) ? write_attr(key, value) : self[key]=value }
|
|
141
|
+
save
|
|
134
142
|
end
|
|
135
143
|
|
|
136
144
|
private
|
|
@@ -143,7 +151,15 @@ module ActiveNode
|
|
|
143
151
|
end
|
|
144
152
|
|
|
145
153
|
def respond_to_writer? attr
|
|
146
|
-
respond_to?
|
|
154
|
+
respond_to? writer(attr)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def write_attr attr, value
|
|
158
|
+
send writer(attr), value
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def writer attr
|
|
162
|
+
"#{attr}="
|
|
147
163
|
end
|
|
148
164
|
|
|
149
165
|
def related(direction, type, klass)
|
|
@@ -132,10 +132,6 @@ module ActiveNode
|
|
|
132
132
|
end
|
|
133
133
|
|
|
134
134
|
private
|
|
135
|
-
def derive_class_name
|
|
136
|
-
(direction == :outgoing ? type : name).to_s.camelize
|
|
137
|
-
end
|
|
138
|
-
|
|
139
135
|
def derive_type
|
|
140
136
|
direction == :outgoing ? name.to_s.singularize : @model.name.underscore
|
|
141
137
|
end
|
|
@@ -144,22 +140,23 @@ module ActiveNode
|
|
|
144
140
|
|
|
145
141
|
# Holds all the meta-data about an association as it was specified in the
|
|
146
142
|
# Active Record class.
|
|
147
|
-
class AssociationReflection < MacroReflection
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
143
|
+
class AssociationReflection < MacroReflection
|
|
144
|
+
#:nodoc:
|
|
145
|
+
# Returns the target association's class.
|
|
146
|
+
#
|
|
147
|
+
# class Author < ActiveRecord::Base
|
|
148
|
+
# has_many :books
|
|
149
|
+
# end
|
|
150
|
+
#
|
|
151
|
+
# Author.reflect_on_association(:books).klass
|
|
152
|
+
# # => Book
|
|
153
|
+
#
|
|
154
|
+
# <b>Note:</b> Do not call +klass.new+ or +klass.create+ to instantiate
|
|
155
|
+
# a new association object. Use +build_association+ or +create_association+
|
|
156
|
+
# instead. This allows plugins to hook into association object creation.
|
|
157
|
+
#def klass
|
|
158
|
+
# @klass ||= model.send(:compute_type, class_name)
|
|
159
|
+
#end
|
|
163
160
|
|
|
164
161
|
def initialize(*args)
|
|
165
162
|
super
|
|
@@ -225,9 +222,13 @@ module ActiveNode
|
|
|
225
222
|
|
|
226
223
|
private
|
|
227
224
|
def derive_class_name
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
225
|
+
if direction == :outgoing
|
|
226
|
+
class_name = type.to_s
|
|
227
|
+
else
|
|
228
|
+
class_name = name.to_s
|
|
229
|
+
class_name = class_name.singularize if collection?
|
|
230
|
+
end
|
|
231
|
+
model.name.sub(/[^:]*$/, class_name.camelize)
|
|
231
232
|
end
|
|
232
233
|
end
|
|
233
234
|
|
|
@@ -31,7 +31,7 @@ module ActiveNode
|
|
|
31
31
|
extend ActiveSupport::Concern
|
|
32
32
|
include ActiveModel::Validations
|
|
33
33
|
|
|
34
|
-
autoload :UniquenessValidator, 'active_node/validations/
|
|
34
|
+
autoload :UniquenessValidator, 'active_node/validations/uniqueness'
|
|
35
35
|
|
|
36
36
|
module ClassMethods
|
|
37
37
|
# Creates an object just like Base.create but calls <tt>save!</tt> instead of +save+
|
|
@@ -82,3 +82,5 @@ module ActiveNode
|
|
|
82
82
|
end
|
|
83
83
|
end
|
|
84
84
|
end
|
|
85
|
+
|
|
86
|
+
require "active_node/validations/uniqueness"
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
module ActiveNode
|
|
2
2
|
module Validations
|
|
3
|
-
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
class UniquenessValidator < ActiveModel::EachValidator # :nodoc:
|
|
4
6
|
def validate_each(record, attribute, value)
|
|
5
7
|
if value && other_matching_records(record, attribute, value).any?
|
|
6
8
|
record.errors.add(attribute, :taken, value: value)
|
|
@@ -16,5 +18,11 @@ module ActiveNode
|
|
|
16
18
|
)
|
|
17
19
|
end
|
|
18
20
|
end
|
|
21
|
+
|
|
22
|
+
module ClassMethods
|
|
23
|
+
def validates_uniqueness_of(*attr_names)
|
|
24
|
+
validates_with UniquenessValidator, _merge_attributes(attr_names)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
19
27
|
end
|
|
20
28
|
end
|
data/lib/active_node/version.rb
CHANGED
|
@@ -19,7 +19,7 @@ describe ActiveNode::Associations do
|
|
|
19
19
|
it "can set association by id" do
|
|
20
20
|
user = NeoUser.create!(name: 'Heinrich')
|
|
21
21
|
client = Client.create!(name: 'a', user_ids: [user.id])
|
|
22
|
-
client.users.should == [user]
|
|
22
|
+
client.users.to_a.should == [user]
|
|
23
23
|
client.user_ids.should == [user.id]
|
|
24
24
|
client.users.first.clients.first.should == client
|
|
25
25
|
client.user_ids = []
|
|
@@ -80,7 +80,7 @@ describe ActiveNode::Associations do
|
|
|
80
80
|
person.people = [person]
|
|
81
81
|
person.save
|
|
82
82
|
person.people.first == person
|
|
83
|
-
Person.
|
|
83
|
+
Person.count.should == 1
|
|
84
84
|
end
|
|
85
85
|
|
|
86
86
|
it 'can handle reference to the same class' do
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe ActiveNode::Graph do
|
|
4
|
+
describe "#path" do
|
|
5
|
+
it "should parse includes" do
|
|
6
|
+
ActiveNode::Graph.new(Person, :father).send(:query).should == "optional match (n0)<-[r1:child]-(n1:`Person`)"
|
|
7
|
+
ActiveNode::Graph.new(Person, :father, :children).send(:query).should == "optional match (n0)<-[r1:child]-(n1:`Person`) optional match (n0)-[r2:child]->(n2:`Person`)"
|
|
8
|
+
ActiveNode::Graph.new(Person, children: 2).send(:query).should == "optional match (n0)-[r1:child*1..2]->(n1:`Person`)"
|
|
9
|
+
ActiveNode::Graph.new(Person, {children: :address}, :address).send(:query).should == "optional match (n0)-[r1:child]->(n1:`Person`) optional match (n1)-[r2:address]->(n2:`Address`) optional match (n0)-[r3:address]->(n3:`Address`)"
|
|
10
|
+
ActiveNode::Graph.new(Person, children: [:address, :father]).send(:query).should == "optional match (n0)-[r1:child]->(n1:`Person`) optional match (n1)-[r2:address]->(n2:`Address`) optional match (n1)<-[r3:child]-(n3:`Person`)"
|
|
11
|
+
ActiveNode::Graph.new(Person, {children: 2} => [:address, :father]).send(:query).should == "optional match (n0)-[r1:child*1..2]->(n1:`Person`) optional match (n1)-[r2:address]->(n2:`Address`) optional match (n1)<-[r3:child]-(n3:`Person`)"
|
|
12
|
+
ActiveNode::Graph.new(Person, {children: '*'} => [:address, :father]).send(:query).should == "optional match (n0)-[r1:child*]->(n1:`Person`) optional match (n1)-[r2:address]->(n2:`Address`) optional match (n1)<-[r3:child]-(n3:`Person`)"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "should build graph" do
|
|
16
|
+
person = Person.create! children: [c1=Person.create!, c2=Person.create!(address: a=Address.create!)]
|
|
17
|
+
g_person = person.includes!({children: '*'} => [:address, :father])
|
|
18
|
+
g_person = ActiveNode::Graph.new(Person, {children: '*'} => [:address, :father]).build(person).first
|
|
19
|
+
g_person.should == person
|
|
20
|
+
g_person.object_id.should == person.object_id
|
|
21
|
+
ActiveNode::Neo.should_not_receive(:db)
|
|
22
|
+
g_person.children.last.address.should == a
|
|
23
|
+
g_person.children.first.father = person
|
|
24
|
+
g_person.children.should == [c1, c2]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "should not query db twice" do
|
|
28
|
+
pending
|
|
29
|
+
person = Person.create!
|
|
30
|
+
person.includes! :children
|
|
31
|
+
ActiveNode::Neo.should_not_receive(:db)
|
|
32
|
+
person.children
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -31,21 +31,21 @@ describe ActiveNode::Persistence do
|
|
|
31
31
|
|
|
32
32
|
it 'should destroy node' do
|
|
33
33
|
user = NeoUser.create!(name: 'abc')
|
|
34
|
-
NeoUser.
|
|
34
|
+
NeoUser.count.should == 1
|
|
35
35
|
user.destroy.should be_true
|
|
36
|
-
NeoUser.
|
|
36
|
+
NeoUser.count.should == 0
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
it 'should not destroy node with relationships' do
|
|
40
40
|
person = Person.create! children: [Person.create!, Person.create!]
|
|
41
41
|
person.destroy.should be_false
|
|
42
|
-
Person.
|
|
42
|
+
Person.count.should == 3
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
it 'should destroy! node with relationships' do
|
|
46
46
|
person = Person.create! children: [Person.create!, Person.create!]
|
|
47
47
|
person.destroy!.should be_true
|
|
48
|
-
Person.
|
|
48
|
+
Person.count.should == 2
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
it 'should record timestamp' do
|
|
@@ -100,7 +100,7 @@ describe ActiveNode::Persistence do
|
|
|
100
100
|
it 'should find objects passing multiple ids' do
|
|
101
101
|
person1 = Person.create!
|
|
102
102
|
person2 = Person.create!
|
|
103
|
-
Person.find([person1.id, person2.id]).should == [person1, person2]
|
|
103
|
+
Person.find([person1.id, person2.id]).to_a.should == [person1, person2]
|
|
104
104
|
end
|
|
105
105
|
|
|
106
106
|
it 'should find an object with id of an unknown model' do
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: active_node
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Heinrich Klobuczek
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2014-
|
|
11
|
+
date: 2014-05-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: active_attr
|
|
@@ -165,19 +165,21 @@ files:
|
|
|
165
165
|
- lib/active_node/base.rb
|
|
166
166
|
- lib/active_node/callbacks.rb
|
|
167
167
|
- lib/active_node/core.rb
|
|
168
|
+
- lib/active_node/dirty.rb
|
|
168
169
|
- lib/active_node/errors.rb
|
|
169
170
|
- lib/active_node/graph.rb
|
|
170
|
-
- lib/active_node/graph/
|
|
171
|
+
- lib/active_node/graph/finder_methods.rb
|
|
172
|
+
- lib/active_node/graph/query_methods.rb
|
|
171
173
|
- lib/active_node/neo.rb
|
|
172
174
|
- lib/active_node/persistence.rb
|
|
173
175
|
- lib/active_node/reflection.rb
|
|
174
176
|
- lib/active_node/relationship.rb
|
|
175
177
|
- lib/active_node/validations.rb
|
|
176
|
-
- lib/active_node/validations/
|
|
178
|
+
- lib/active_node/validations/uniqueness.rb
|
|
177
179
|
- lib/active_node/version.rb
|
|
178
180
|
- spec/functional/associations_spec.rb
|
|
179
181
|
- spec/functional/base_spec.rb
|
|
180
|
-
- spec/functional/
|
|
182
|
+
- spec/functional/graph_spec.rb
|
|
181
183
|
- spec/functional/persistence_spec.rb
|
|
182
184
|
- spec/functional/validations_spec.rb
|
|
183
185
|
- spec/models/address.rb
|
|
@@ -213,7 +215,7 @@ summary: ActiveRecord style Object Graph Mapping for neo4j
|
|
|
213
215
|
test_files:
|
|
214
216
|
- spec/functional/associations_spec.rb
|
|
215
217
|
- spec/functional/base_spec.rb
|
|
216
|
-
- spec/functional/
|
|
218
|
+
- spec/functional/graph_spec.rb
|
|
217
219
|
- spec/functional/persistence_spec.rb
|
|
218
220
|
- spec/functional/validations_spec.rb
|
|
219
221
|
- spec/models/address.rb
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
module ActiveNode
|
|
2
|
-
module Graph
|
|
3
|
-
class Builder
|
|
4
|
-
include Neography::Rest::Helpers
|
|
5
|
-
|
|
6
|
-
attr_reader :reflections, :matches, :klass
|
|
7
|
-
|
|
8
|
-
def initialize klass, *includes
|
|
9
|
-
@klass = klass if klass < ActiveNode::Base
|
|
10
|
-
@matches = []
|
|
11
|
-
@reflections =[]
|
|
12
|
-
@object_cache = {}
|
|
13
|
-
@relationship_cache = {}
|
|
14
|
-
@loaded_assoc_cache = {}
|
|
15
|
-
parse_paths(:n0, klass, includes)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def build *objects
|
|
19
|
-
ids = objects.map { |o| o.is_a?(ActiveNode::Base) ? o.id.tap { |id| @object_cache[id]=o } : extract_id(o) }
|
|
20
|
-
parse_results execute(ids.compact)
|
|
21
|
-
@object_cache.slice(*ids).values
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
private
|
|
25
|
-
def query
|
|
26
|
-
@matches.join " "
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def execute(ids)
|
|
30
|
-
q="start n0=node({ids}) #{query} #{"where n0#{label @klass}" if @klass} return #{list_with_rel(@reflections.size)} order by #{created_at_list(@reflections.size)}"
|
|
31
|
-
Neo.db.execute_query(q, ids: ids)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def parse_results results
|
|
35
|
-
results['data'].each do |record|
|
|
36
|
-
wrap(record.first, @klass)
|
|
37
|
-
@reflections.each_with_index do |reflection, index|
|
|
38
|
-
node_rel = record[2*index+1]
|
|
39
|
-
node_rel = node_rel.last if node_rel.is_a? Array
|
|
40
|
-
next unless node_rel
|
|
41
|
-
owner = @object_cache[owner_id node_rel, reflection.direction]
|
|
42
|
-
node = wrap record[2*index + 2], reflection.klass
|
|
43
|
-
rel = reflection.klass.create_rel node_rel, node
|
|
44
|
-
assoc = owner.association(reflection.name)
|
|
45
|
-
assoc.rels_writer((assoc.rel_target || []) << rel) unless previously_loaded?(assoc)
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def previously_loaded?(assoc)
|
|
51
|
-
@loaded_assoc_cache[assoc] = assoc.rel_target unless @loaded_assoc_cache.key? assoc
|
|
52
|
-
@loaded_assoc_cache[assoc]
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def wrap(record, klass)
|
|
56
|
-
@object_cache[extract_id record] ||= ActiveNode::Base.wrap(record, klass)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def owner_id relationship, direction
|
|
60
|
-
extract_id relationship[direction == :incoming ? 'end' : 'start']
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def parse_paths as, klass, includes
|
|
64
|
-
if includes.is_a?(Hash)
|
|
65
|
-
includes.each do |key, value|
|
|
66
|
-
if (value.is_a?(String) || value.is_a?(Numeric))
|
|
67
|
-
add_match(as, klass, key, value)
|
|
68
|
-
else
|
|
69
|
-
parse_paths(as, klass, key)
|
|
70
|
-
parse_paths(latest_alias, @reflections.last.klass, value)
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
elsif includes.is_a?(Array)
|
|
74
|
-
includes.each { |inc| parse_paths(as, klass, inc) }
|
|
75
|
-
else
|
|
76
|
-
add_match(as, klass, includes)
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def add_match from, klass, key, multiplicity=nil
|
|
81
|
-
reflection = klass.reflect_on_association(key)
|
|
82
|
-
@reflections << reflection
|
|
83
|
-
matches << match(from, reflection, multiplicity)
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def latest_alias
|
|
87
|
-
"n#{@reflections.size}"
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def match(start_var, reflection, multiplicity)
|
|
91
|
-
"optional match (#{start_var})#{'<' if reflection.direction == :incoming}-[r#{@reflections.size}:#{reflection.type}#{multiplicity(multiplicity)}]-#{'>' if reflection.direction == :outgoing}(#{latest_alias}#{label reflection.klass})"
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def label klass
|
|
95
|
-
":#{klass.label}" if klass
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def multiplicity multiplicity
|
|
99
|
-
multiplicity.is_a?(Numeric) ? "*1..#{multiplicity}" : multiplicity
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def list_with_rel num
|
|
103
|
-
comma_sep_list(num) { |i| [("r#{i}" if i>0), "n#{i}"] }
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def comma_sep_list num, &block
|
|
107
|
-
(0..num).map(&block).flatten.compact.join(', ')
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def created_at_list num
|
|
111
|
-
comma_sep_list(num) { |i| "n#{i}.created_at" }
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def extract_id(id)
|
|
115
|
-
get_id(id).to_i
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
end
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
require 'spec_helper'
|
|
2
|
-
|
|
3
|
-
describe ActiveNode::Graph::Builder do
|
|
4
|
-
describe "#path" do
|
|
5
|
-
it "should parse includes" do
|
|
6
|
-
ActiveNode::Graph::Builder.new(Person, :father).send(:query).should == "optional match (n0)<-[r1:child]-(n1:Person)"
|
|
7
|
-
ActiveNode::Graph::Builder.new(Person, :father, :children).send(:query).should == "optional match (n0)<-[r1:child]-(n1:Person) optional match (n0)-[r2:child]->(n2:Person)"
|
|
8
|
-
ActiveNode::Graph::Builder.new(Person, children: 2).send(:query).should == "optional match (n0)-[r1:child*1..2]->(n1:Person)"
|
|
9
|
-
ActiveNode::Graph::Builder.new(Person, {children: :address}, :address).send(:query).should == "optional match (n0)-[r1:child]->(n1:Person) optional match (n1)-[r2:address]->(n2:Address) optional match (n0)-[r3:address]->(n3:Address)"
|
|
10
|
-
ActiveNode::Graph::Builder.new(Person, children: [:address, :father]).send(:query).should == "optional match (n0)-[r1:child]->(n1:Person) optional match (n1)-[r2:address]->(n2:Address) optional match (n1)<-[r3:child]-(n3:Person)"
|
|
11
|
-
ActiveNode::Graph::Builder.new(Person, {children: 2} => [:address, :father]).send(:query).should == "optional match (n0)-[r1:child*1..2]->(n1:Person) optional match (n1)-[r2:address]->(n2:Address) optional match (n1)<-[r3:child]-(n3:Person)"
|
|
12
|
-
ActiveNode::Graph::Builder.new(Person, {children: '*'} => [:address, :father]).send(:query).should == "optional match (n0)-[r1:child*]->(n1:Person) optional match (n1)-[r2:address]->(n2:Address) optional match (n1)<-[r3:child]-(n3:Person)"
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
it "should build graph" do
|
|
16
|
-
person = Person.create! children: [c1=Person.create!, c2=Person.create!(address: a=Address.create!)]
|
|
17
|
-
g_person = ActiveNode::Graph::Builder.new(Person, {children: '*'} => [:address, :father]).build(person).first
|
|
18
|
-
g_person.should == person
|
|
19
|
-
g_person.object_id.should == person.object_id
|
|
20
|
-
ActiveNode::Neo.should_not_receive(:db)
|
|
21
|
-
g_person.children.last.address.should == a
|
|
22
|
-
g_person.children.first.father = person
|
|
23
|
-
g_person.children.should == [c1, c2]
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
it "should not query db twice" do
|
|
27
|
-
pending
|
|
28
|
-
person = Person.create!
|
|
29
|
-
ActiveNode::Graph::Builder.new(Person, :children).build(person)
|
|
30
|
-
ActiveNode::Neo.should_not_receive(:db)
|
|
31
|
-
person.children
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|