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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 26171a6609bb3a3042fd11f4202bb9abc700315f
4
- data.tar.gz: e888b8fb7acf8cdee146d4a2aaa29052c7ce219a
3
+ metadata.gz: 19347532b691863f735cdb799989cf68a1bc624b
4
+ data.tar.gz: 71833a5ff1029940a6965c90c9f86504747f5c6a
5
5
  SHA512:
6
- metadata.gz: 2ac77c3a6654c341b368fcf60c317ef04bdee15d793332373070bd08658a8daae18e719ebdb23c758f5515b7e8829e1f385a7a4cd8a7c4261c8e81621c5db932
7
- data.tar.gz: 075a8582b6c6e28fd0b60483fcd141d9d680b9e34dfcc411ee528e6e7d34e1948ac1c9b0e5658ea609262b4a9633116da35ea4eb50ac6e2348b5a2704b13596a
6
+ metadata.gz: 9017661492388db4212a1674c9364ff182ea756f48252a8de0221e1194e0697ab2faf2964182d6f5ac310ea9deee3e4045556389ab50ecac4898747009ef301a
7
+ data.tar.gz: 63610493b0a11263928de9fab5bbe3f79741a82e54dd8869f87d9d733444046da275816d803d78946e6f42f7f0cafb213fcd57bc1fc6d88834cb69a75eb1e453
data/.travis.yml CHANGED
@@ -1,4 +1,7 @@
1
- script: "CODECLIMATE_REPO_TOKEN=88fa9b8eb0bfc48e8c5077253d829e94af9f76aca60df9e75ae0e50a4c7cd9f0 bundle exec rake neo4j:install['enterprise','2.0.2'] neo4j:start spec --trace"
1
+ script: "bundle exec rake neo4j:install['enterprise','2.0.3'] neo4j:start spec --trace"
2
2
  language: ruby
3
3
  rvm:
4
- - 2.0.0
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.relationships(reflection, *associations)
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 ? [] : ActiveNode::Graph::Builder.new(owner.class, reflection.name).build(owner.id).first.association(reflection.name).rels_reader
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)
@@ -3,7 +3,11 @@ require 'active_node/errors'
3
3
 
4
4
  module ActiveNode
5
5
  class Base
6
- include ActiveAttr::Model
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
@@ -6,7 +6,11 @@ module ActiveNode
6
6
  class ActiveNodeError < StandardError
7
7
  end
8
8
 
9
- # Raised by ActiveRecord::Base.save! and ActiveRecord::Base.create! methods when record cannot be
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
@@ -1,7 +1,216 @@
1
1
  module ActiveNode
2
- module Graph
3
- extend ActiveSupport::Autoload
2
+ class Graph
3
+ include Neography::Rest::Helpers
4
+ include FinderMethods, QueryMethods
4
5
 
5
- autoload :Builder, 'active_node/graph/builder'
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
- send "#{attr}=", value
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 relationships(reflection, *associations)
133
- ActiveNode::Graph::Builder.new(self.class, reflection.name => associations).build(self)
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? "#{attr}="
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 #:nodoc:
148
- # Returns the target association's class.
149
- #
150
- # class Author < ActiveRecord::Base
151
- # has_many :books
152
- # end
153
- #
154
- # Author.reflect_on_association(:books).klass
155
- # # => Book
156
- #
157
- # <b>Note:</b> Do not call +klass.new+ or +klass.create+ to instantiate
158
- # a new association object. Use +build_association+ or +create_association+
159
- # instead. This allows plugins to hook into association object creation.
160
- #def klass
161
- # @klass ||= model.send(:compute_type, class_name)
162
- #end
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
- class_name = name.to_s.camelize
229
- class_name = class_name.singularize if collection?
230
- class_name
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/uniqueness_validator'
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
- class UniquenessValidator < ActiveModel::EachValidator # :nodoc:
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
@@ -1,3 +1,3 @@
1
1
  module ActiveNode
2
- VERSION = "2.1.1"
2
+ VERSION = "2.2.0"
3
3
  end
@@ -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.all.count.should == 1
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.all.count.should == 1
34
+ NeoUser.count.should == 1
35
35
  user.destroy.should be_true
36
- NeoUser.all.count.should == 0
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.all.count.should == 3
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.all.count.should == 2
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.1.1
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-04-15 00:00:00.000000000 Z
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/builder.rb
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/uniqueness_validator.rb
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/graph/builder_spec.rb
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/graph/builder_spec.rb
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