active_node 2.1.1 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
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