hyper-model 1.0.alpha1.3 → 1.0.alpha1.4

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
  SHA256:
3
- metadata.gz: 071e8cc562ebddf1c0a260587a123d91db9f2a7c52d43b06fcf063079d66129f
4
- data.tar.gz: 4cb7539129b7367f3156c0726ac3ed164e1db391f7806457253d303e9940941c
3
+ metadata.gz: '0583cd6276013eed8abad6c7e12454455c6915e9773eb195b51977c8d748217f'
4
+ data.tar.gz: a3e3f3e0aa5065281103566c6178bd2e4045a09863e1697d7587a5d2321f17dc
5
5
  SHA512:
6
- metadata.gz: 2c1bcd8fee396593e2f31d072f35cbf88467be2b02c198d523c052b67a211d9fea4501ca86c4f0a388cee35bc34f4ac9156147a13aac9aec4842616a5c3c1772
7
- data.tar.gz: c226e31b1882e96c8a7a080114aea90aacdb0bb77331fc36854f79fe118fabd119f278bc20b94d3693bcc69d7382bc6e4d2f1e91c042b41eb660d2214e75c45d
6
+ metadata.gz: 7d21d5c7def5a3217d00757c611042da17efcaf639eba5fd5c021f79ffaa7e9f98334186d51c5c5353188c56b528286b91cc3d48ed4e8ecadf73cdae40cae04e
7
+ data.tar.gz: 5aeaf368b37b2f4224ec0d7d3af83f44708f9093e97f7261d49d65e78af6ac0e5078c055d4fb974b4c2041491911087175e12e5988fc6873bec17ed388dee8ff
data/.gitignore CHANGED
@@ -11,7 +11,6 @@ spec/test_app/tmp/
11
11
  spec/test_app/db/test.sqlite3
12
12
  spec/test_app/log/test.log
13
13
  spec/test_app/log/development.log
14
- spec/test_app/Gemfile.lock
15
14
  /synchromesh-simple-poller-store
16
15
  /synchromesh-pusher-channel-store
17
16
  /examples/action-cable/rails_cache_dir/
@@ -34,3 +33,7 @@ public/assets/*
34
33
  # ingore Idea
35
34
  .idea
36
35
  .vscode
36
+
37
+ # ignore Gemfile.locks https://yehudakatz.com/2010/12/16/clarifying-the-roles-of-the-gemspec-and-gemfile/
38
+ /spec/test_app/Gemfile.lock
39
+ /Gemfile.lock
data/Rakefile CHANGED
@@ -1,20 +1,33 @@
1
1
  require "bundler/gem_tasks"
2
2
  require "rspec/core/rake_task"
3
3
 
4
+ def run_batches(batches)
5
+ failed = false
6
+ batches.each do |batch|
7
+ begin
8
+ Rake::Task["spec:batch#{batch}"].invoke
9
+ rescue SystemExit
10
+ failed = true
11
+ end
12
+ end
13
+ exit 1 if failed
14
+ end
15
+
16
+
4
17
  task :part1 do
5
- (1..2).each { |batch| Rake::Task["spec:batch#{batch}"].invoke rescue nil }
18
+ run_batches(1..2)
6
19
  end
7
20
 
8
21
  task :part2 do
9
- (3..4).each { |batch| Rake::Task["spec:batch#{batch}"].invoke rescue nil }
22
+ run_batches(3..4)
10
23
  end
11
24
 
12
25
  task :part3 do
13
- (5..7).each { |batch| Rake::Task["spec:batch#{batch}"].invoke rescue nil }
26
+ run_batches(5..7)
14
27
  end
15
28
 
16
29
  task :spec do
17
- (1..7).each { |batch| Rake::Task["spec:batch#{batch}"].invoke rescue nil }
30
+ run_batches(1..7)
18
31
  end
19
32
 
20
33
  namespace :spec do
@@ -23,7 +36,6 @@ namespace :spec do
23
36
  end
24
37
  (1..7).each do |batch|
25
38
  RSpec::Core::RakeTask.new(:"batch#{batch}") do |t|
26
- t.fail_on_error = false unless batch == 7
27
39
  t.pattern = "spec/batch#{batch}/**/*_spec.rb"
28
40
  end
29
41
  end
@@ -63,7 +63,7 @@ Gem::Specification.new do |spec|
63
63
  spec.add_development_dependency 'shoulda'
64
64
  spec.add_development_dependency 'shoulda-matchers'
65
65
  spec.add_development_dependency 'spring-commands-rspec'
66
- spec.add_development_dependency 'sqlite3'
66
+ spec.add_development_dependency 'sqlite3', '~> 1.3.6' # see https://github.com/rails/rails/issues/35153, '~> 1.3.6'
67
67
  spec.add_development_dependency 'timecop', '~> 0.8.1'
68
68
  spec.add_development_dependency 'unparser'
69
69
  end
@@ -87,7 +87,7 @@ module ActiveRecord
87
87
  this.acting_user = acting_user
88
88
  # returns a PsuedoRelationArray which will respond to the
89
89
  # __secure_collection_check method
90
- ReactiveRecordPsuedoRelationArray.new([this.instance_exec(*args, &block)])
90
+ ReactiveRecordPsuedoRelationArray.new([*this.instance_exec(*args, &block)])
91
91
  ensure
92
92
  this.acting_user = old
93
93
  end
@@ -255,20 +255,6 @@ module ActiveRecord
255
255
  pre_syncromesh_has_many name, *args, opts.except(:regulate), &block
256
256
  end
257
257
 
258
- # add secure access for find, find_by, and belongs_to and has_one relations.
259
- # No explicit security checks are needed here, as the data returned by these objects
260
- # will be further processedand checked before returning. I.e. it is not possible to
261
- # simply return `find(1)` but if you try returning `find(1).name` the permission system
262
- # will check to see if the name attribute can be legally sent to the current acting user.
263
-
264
- def __secure_remote_access_to_find(_self, _acting_user, *args)
265
- find(*args)
266
- end
267
-
268
- def __secure_remote_access_to_find_by(_self, _acting_user, *args)
269
- find_by(*args)
270
- end
271
-
272
258
  %i[belongs_to has_one].each do |macro|
273
259
  alias_method :"pre_syncromesh_#{macro}", macro
274
260
  define_method(macro) do |name, *aargs, &block|
@@ -329,6 +315,37 @@ module ActiveRecord
329
315
  %i[limit offset].each do |scope|
330
316
  regulate_scope(scope) {}
331
317
  end
318
+
319
+ finder_method :__hyperstack_internal_scoped_last do
320
+ last
321
+ end
322
+
323
+ scope :__hyperstack_internal_scoped_last_n, ->(n) { last(n) }
324
+
325
+ # implements find_by inside of scopes. For security reasons we return nil
326
+ # if we cannot view at least the id of found record. Otherwise a hacker
327
+ # could tell if a record exists depending on whether an access violation
328
+ # (i.e. it exists) or nil (it doesn't exist is returned.) Note that
329
+ # view of id is permitted as long as any attribute of the record is
330
+ # accessible.
331
+ finder_method :__hyperstack_internal_scoped_find_by do |attrs|
332
+ begin
333
+ found = find_by(attrs)
334
+ found && found.check_permission_with_acting_user(acting_user, :view_permitted?, :id)
335
+ rescue Hyperstack::AccessViolation => e
336
+ message = []
337
+ message << Pastel.new.red("\n\nHYPERSTACK Access violation during find_by operation.")
338
+ message << Pastel.new.red("Access to the found record's id is not permitted. nil will be returned")
339
+ message << " #{self.name}.find_by("
340
+ message << attrs.collect do |attr, value|
341
+ " #{attr}: '#{value.inspect.truncate(120, separator: '...')}'"
342
+ end.join(",\n")
343
+ message << " )"
344
+ message << "\n#{e.details}\n"
345
+ Hyperstack.on_error('find_by', self, attrs, message.join("\n"))
346
+ nil
347
+ end
348
+ end
332
349
  end
333
350
  end
334
351
 
@@ -1,3 +1,3 @@
1
1
  module HyperModel
2
- VERSION = '1.0.alpha1.3'
2
+ VERSION = '1.0.alpha1.4'
3
3
  end
@@ -6,5 +6,22 @@ module ActiveRecord
6
6
 
7
7
  scope :limit, ->() {}
8
8
  scope :offset, ->() {}
9
+
10
+ finder_method :__hyperstack_internal_scoped_last
11
+ scope :__hyperstack_internal_scoped_last_n, ->(n) { last(n) }
12
+
13
+ ReactiveRecord::ScopeDescription.new(
14
+ self, :___hyperstack_internal_scoped_find_by,
15
+ client: ->(attrs) { !attrs.detect { |attr, value| attributes[attr] != value } }
16
+ )
17
+
18
+ def self.__hyperstack_internal_scoped_find_by(attrs)
19
+ collection = all.apply_scope(:___hyperstack_internal_scoped_find_by, attrs)
20
+ if !collection.collection
21
+ collection._find_by_initializer(self, attrs)
22
+ else
23
+ collection.first
24
+ end
25
+ end
9
26
  end
10
27
  end
@@ -51,14 +51,24 @@ module ActiveRecord
51
51
  @model_name ||= ActiveModel::Name.new(self)
52
52
  end
53
53
 
54
+ def __hyperstack_preprocess_attrs(attrs)
55
+ if inheritance_column && self < base_class && !attrs.key?(inheritance_column)
56
+ attrs = attrs.merge(inheritance_column => model_name.to_s)
57
+ end
58
+ dealiased_attrs = {}
59
+ attrs.each { |attr, value| dealiased_attrs[_dealias_attribute(attr)] = value }
60
+ end
61
+
54
62
  def find(id)
55
- ReactiveRecord::Base.find(self, primary_key => id)
63
+ find_by(primary_key => id)
56
64
  end
57
65
 
58
- def find_by(opts = {})
59
- dealiased_opts = {}
60
- opts.each { |attr, value| dealiased_opts[_dealias_attribute(attr)] = value }
61
- ReactiveRecord::Base.find(self, dealiased_opts)
66
+ def find_by(attrs = {})
67
+ attrs = __hyperstack_preprocess_attrs(attrs)
68
+ # r = ReactiveRecord::Base.find_locally(self, attrs, new_only: true)
69
+ # return r.ar_instance if r
70
+ (r = __hyperstack_internal_scoped_find_by(attrs)) || return
71
+ r.backing_record.sync_attributes(attrs).set_ar_instance!
62
72
  end
63
73
 
64
74
  def enum(*args)
@@ -176,7 +186,7 @@ module ActiveRecord
176
186
 
177
187
  def method_missing(name, *args, &block)
178
188
  if args.count == 1 && name.start_with?("find_by_") && !block
179
- find_by(_dealias_attribute(name.sub(/^find_by_/, "")) => args[0])
189
+ find_by(name.sub(/^find_by_/, '') => args[0])
180
190
  elsif [].respond_to?(name)
181
191
  all.send(name, *args, &block)
182
192
  elsif name.end_with?('!')
@@ -193,16 +203,6 @@ module ActiveRecord
193
203
  # Any method ending with ! just means apply the method after forcing a reload
194
204
  # from the DB.
195
205
 
196
- # alias pre_synchromesh_method_missing method_missing
197
- #
198
- # def method_missing(name, *args, &block)
199
- # return all.send(name, *args, &block) if [].respond_to?(name)
200
- # if name.end_with?('!')
201
- # return send(name.chop, *args, &block).send(:reload_from_db) rescue nil
202
- # end
203
- # pre_synchromesh_method_missing(name, *args, &block)
204
- # end
205
-
206
206
  def create(*args, &block)
207
207
  new(*args).save(&block)
208
208
  end
@@ -213,9 +213,6 @@ module ActiveRecord
213
213
  singleton_class.send(:define_method, name) do |*vargs|
214
214
  all.build_child_scope(scope_description, *name, *vargs)
215
215
  end
216
- # singleton_class.send(:define_method, "#{name}=") do |_collection|
217
- # raise 'NO LONGER IMPLEMENTED - DOESNT PLAY WELL WITH SYNCHROMESH'
218
- # end
219
216
  end
220
217
 
221
218
  def default_scope(*args, &block)
@@ -249,10 +246,6 @@ module ActiveRecord
249
246
  end
250
247
  end
251
248
 
252
- # def all=(_collection)
253
- # raise "NO LONGER IMPLEMENTED DOESNT PLAY WELL WITH SYNCHROMESH"
254
- # end
255
-
256
249
  def unscoped
257
250
  ReactiveRecord::Base.unscoped[self] ||=
258
251
  ReactiveRecord::Collection
@@ -261,10 +254,11 @@ module ActiveRecord
261
254
  end
262
255
 
263
256
  def finder_method(name)
264
- ReactiveRecord::ScopeDescription.new(self, "_#{name}", {}) # was adding _ to front
257
+ ReactiveRecord::ScopeDescription.new(self, "_#{name}", {})
265
258
  [name, "#{name}!"].each do |method|
266
259
  singleton_class.send(:define_method, method) do |*vargs|
267
- all.apply_scope("_#{method}", *vargs).first # was adding _ to front
260
+ collection = all.apply_scope("_#{method}", *vargs)
261
+ collection.first
268
262
  end
269
263
  end
270
264
  end
@@ -370,7 +364,9 @@ module ActiveRecord
370
364
  # TODO: changed values as changes while just updating the synced values.
371
365
  target =
372
366
  if param[primary_key]
373
- find(param[primary_key])
367
+ ReactiveRecord::Base.find(self, primary_key => param[primary_key]).tap do |r|
368
+ r.backing_record.loaded_id = param[primary_key]
369
+ end
374
370
  else
375
371
  new
376
372
  end
@@ -144,10 +144,12 @@ module ActiveRecord
144
144
  @backing_record.destroyed
145
145
  end
146
146
 
147
- def new?
147
+ def new_record?
148
148
  @backing_record.new?
149
149
  end
150
150
 
151
+ alias new? new_record?
152
+
151
153
  def errors
152
154
  Hyperstack::Internal::State::Variable.get(@backing_record, @backing_record)
153
155
  @backing_record.errors
@@ -26,11 +26,28 @@ module ReactiveRecord
26
26
  end
27
27
 
28
28
  def loading_details
29
- "[loading #{vector}]"
29
+ "[loading #{pretty_vector}]"
30
30
  end
31
31
 
32
32
  def dirty_details
33
33
  "[changed id: #{id} #{changes}]"
34
34
  end
35
+
36
+ def pretty_vector
37
+ v = []
38
+ i = 0
39
+ while i < vector.length
40
+ if vector[i] == 'all' && vector[i + 1].is_a?(Array) &&
41
+ vector[i + 1][0] == '___hyperstack_internal_scoped_find_by' &&
42
+ vector[i + 2] == '*0'
43
+ v << ['find_by', vector[i + 1][1]]
44
+ i += 3
45
+ else
46
+ v << vector[i]
47
+ i += 1
48
+ end
49
+ end
50
+ v
51
+ end
35
52
  end
36
53
  end
@@ -67,30 +67,35 @@ module ReactiveRecord
67
67
  load_data { ServerDataCache.load_from_json(json, target) }
68
68
  end
69
69
 
70
+ def self.find_locally(model, attrs, new_only: nil)
71
+ if (id_to_find = attrs[model.primary_key])
72
+ !new_only && lookup_by_id(model, id_to_find)
73
+ else
74
+ @records[model].detect do |r|
75
+ (r.new? || !new_only) &&
76
+ !attrs.detect { |attr, value| r.synced_attributes[attr] != value }
77
+ end
78
+ end
79
+ end
80
+
81
+ def self.find_by_id(model, id)
82
+ find(model, model.primary_key => id)
83
+ end
84
+
70
85
  def self.find(model, attrs)
71
86
  # will return the unique record with this attribute-value pair
72
87
  # value cannot be an association or aggregation
73
88
 
74
89
  # add the inheritance column if this is an STI subclass
75
90
 
76
- inher_col = model.inheritance_column
77
- if inher_col && model < model.base_class && !attrs.key?(inher_col)
78
- attrs = attrs.merge(inher_col => model.model_name.to_s)
79
- end
91
+ attrs = model.__hyperstack_preprocess_attrs(attrs)
80
92
 
81
93
  model = model.base_class
82
94
  primary_key = model.primary_key
83
95
 
84
96
  # already have a record with these attribute-value pairs?
85
97
 
86
- record =
87
- if (id_to_find = attrs[primary_key])
88
- lookup_by_id(model, id_to_find)
89
- else
90
- @records[model].detect do |r|
91
- !attrs.detect { |attr, value| r.synced_attributes[attr] != value }
92
- end
93
- end
98
+ record = find_locally(model, attrs)
94
99
 
95
100
  unless record
96
101
  # if not, and then the record may be loaded, but not have this attribute set yet,
@@ -102,10 +107,8 @@ module ReactiveRecord
102
107
  attrs = attrs.merge primary_key => id
103
108
  end
104
109
  # if we don't have a record then create one
105
- # (record = new(model)).vector = [model, [:find_by, attribute => value]] unless record
106
- record ||= set_vector_lookup(new(model), [model, [:find_by, attrs]])
107
- # and set the values
108
- attrs.each { |attr, value| record.sync_attribute(attr, value) }
110
+ record ||= set_vector_lookup(new(model), [model, *find_by_vector(attrs)])
111
+ record.sync_attributes(attrs)
109
112
  end
110
113
  # finally initialize and return the ar_instance
111
114
  record.set_ar_instance!
@@ -122,7 +125,6 @@ module ReactiveRecord
122
125
  # record = @records[model].detect { |record| record.vector == vector }
123
126
  record = lookup_by_vector(vector)
124
127
  unless record
125
-
126
128
  record = new model
127
129
  set_vector_lookup(record, vector)
128
130
  end
@@ -175,6 +177,7 @@ module ReactiveRecord
175
177
  @ar_instance.instance_variable_set(:@backing_record, existing_record)
176
178
  existing_record.attributes.merge!(attributes) { |key, v1, v2| v1 }
177
179
  end
180
+ @id = value
178
181
  value
179
182
  end
180
183
 
@@ -211,7 +214,7 @@ module ReactiveRecord
211
214
 
212
215
  def initialize_collections
213
216
  if (!vector || vector.empty?) && id && id != ''
214
- Base.set_vector_lookup(self, [@model, [:find_by, @model.primary_key => id]])
217
+ Base.set_vector_lookup(self, [@model, *find_by_vector(@model.primary_key => id)])
215
218
  end
216
219
  Base.load_data do
217
220
  @model.reflect_on_all_associations.each do |assoc|
@@ -251,8 +254,12 @@ module ReactiveRecord
251
254
  @synced_with_unscoped = !@synced_with_unscoped
252
255
  end
253
256
 
254
- def sync_attribute(attribute, value)
257
+ def sync_attributes(attrs)
258
+ attrs.each { |attr, value| sync_attribute(attr, value) }
259
+ self
260
+ end
255
261
 
262
+ def sync_attribute(attribute, value)
256
263
  @synced_attributes[attribute] = @attributes[attribute] = value
257
264
  Base.set_id_lookup(self) if attribute == primary_key
258
265
 
@@ -260,7 +267,7 @@ module ReactiveRecord
260
267
 
261
268
  if value.is_a? Collection
262
269
  @synced_attributes[attribute] = value.dup_for_sync
263
- elsif aggregation = model.reflect_on_aggregation(attribute) and (aggregation.klass < ActiveRecord::Base)
270
+ elsif (aggregation = model.reflect_on_aggregation(attribute)) && (aggregation.klass < ActiveRecord::Base)
264
271
  value.backing_record.sync!
265
272
  elsif aggregation
266
273
  @synced_attributes[attribute] = aggregation.deserialize(aggregation.serialize(value))
@@ -278,6 +285,14 @@ module ReactiveRecord
278
285
  Base.lookup_by_id(model, id)
279
286
  end
280
287
 
288
+ def id_loaded?
289
+ @id
290
+ end
291
+
292
+ def loaded_id=(id)
293
+ @id = id
294
+ end
295
+
281
296
  def revert
282
297
  @changed_attributes.dup.each do |attribute|
283
298
  @ar_instance.send("#{attribute}=", @synced_attributes[attribute])
@@ -29,7 +29,7 @@ module ReactiveRecord
29
29
  @owner = owner # can be nil if this is an outer most scope
30
30
  @association = association
31
31
  @target_klass = target_klass
32
- if owner and !owner.id and vector.length <= 1
32
+ if owner && !owner.id && vector.length <= 1
33
33
  @collection = []
34
34
  elsif vector.length > 0
35
35
  @vector = vector
@@ -59,10 +59,12 @@ module ReactiveRecord
59
59
  @collection = []
60
60
  if ids = ReactiveRecord::Base.fetch_from_db([*@vector, "*all"])
61
61
  ids.each do |id|
62
- @collection << @target_klass.find_by(@target_klass.primary_key => id)
62
+ @collection << ReactiveRecord::Base.find_by_id(@target_klass, id)
63
63
  end
64
64
  else
65
65
  @dummy_collection = ReactiveRecord::Base.load_from_db(nil, *@vector, "*all")
66
+ # this calls back to all now that the collection is initialized,
67
+ # so it has the side effect of creating a dummy value in collection[0]
66
68
  @dummy_record = self[0]
67
69
  end
68
70
  end
@@ -103,7 +105,7 @@ module ReactiveRecord
103
105
  end
104
106
  # todo move following to a separate module related to scope updates ******************
105
107
  attr_reader :vector
106
- attr_writer :scope_description
108
+ attr_accessor :scope_description
107
109
  attr_writer :parent
108
110
  attr_reader :pre_sync_related_records
109
111
 
@@ -222,7 +224,6 @@ To determine this sync_scopes first asks if the record being changed is in the s
222
224
  end
223
225
 
224
226
  def filter_records(related_records)
225
- # possibly we should never get here???
226
227
  scope_args = @vector.last.is_a?(Array) ? @vector.last[1..-1] : []
227
228
  @scope_description.filter_records(related_records, scope_args)
228
229
  end
@@ -231,16 +232,22 @@ To determine this sync_scopes first asks if the record being changed is in the s
231
232
  @live_scopes ||= Set.new
232
233
  end
233
234
 
235
+ def in_this_collection(related_records)
236
+ return related_records unless @association
237
+ related_records.select do |r|
238
+ r.backing_record.attributes[@association.inverse_of] == @owner
239
+ end
240
+ end
241
+
234
242
  def set_pre_sync_related_records(related_records, _record = nil)
235
- #related_records = related_records.intersection([*@collection]) <- deleting this works
236
- @pre_sync_related_records = related_records #in_this_collection related_records <- not sure if this works
243
+ @pre_sync_related_records = in_this_collection(related_records)
237
244
  live_scopes.each { |scope| scope.set_pre_sync_related_records(@pre_sync_related_records) }
238
245
  end
239
246
 
240
247
  # NOTE sync_scopes is overridden in scope_description.rb
241
248
  def sync_scopes(related_records, record, filtering = true)
242
249
  #related_records = related_records.intersection([*@collection])
243
- #related_records = in_this_collection related_records
250
+ related_records = in_this_collection(related_records) if filtering
244
251
  live_scopes.each { |scope| scope.sync_scopes(related_records, record, filtering) }
245
252
  notify_of_change unless related_records.empty?
246
253
  ensure
@@ -301,7 +308,7 @@ To determine this sync_scopes first asks if the record being changed is in the s
301
308
  @collection = []
302
309
  elsif filter?
303
310
  # puts "#{self}.sync_collection_with_parent (@parent = #{@parent}) calling filter records on (#{@parent.collection})"
304
- @collection = filter_records(@parent.collection) # .tap { |rr| puts "returns #{rr} #{rr.to_a}" }
311
+ @collection = filter_records(@parent.collection).to_a
305
312
  end
306
313
  elsif !@linked && @parent._count_internal(false).zero?
307
314
  # don't check _count_internal if already linked as this cause an unnecessary rendering cycle
@@ -391,7 +398,7 @@ To determine this sync_scopes first asks if the record being changed is in the s
391
398
  # child.test_model = 1
392
399
  # so... we go back starting at this collection and look for the first
393
400
  # collection with an owner... that is our guy
394
- child = proxy_association.klass.find(id)
401
+ child = ReactiveRecord::Base.find_by_id(proxy_association.klass, id)
395
402
  push child
396
403
  set_belongs_to child
397
404
  end
@@ -463,13 +470,29 @@ To determine this sync_scopes first asks if the record being changed is in the s
463
470
  notify_of_change self
464
471
  end
465
472
 
466
- [:first, :last].each do |method|
467
- define_method method do |*args|
468
- if args.count == 0
469
- all.send(method)
470
- else
471
- apply_scope(method, *args)
472
- end
473
+ # [:first, :last].each do |method|
474
+ # define_method method do |*args|
475
+ # if args.count == 0
476
+ # all.send(method)
477
+ # else
478
+ # apply_scope(method, *args)
479
+ # end
480
+ # end
481
+ # end
482
+
483
+ def first(n = nil)
484
+ if n
485
+ apply_scope(:first, n)
486
+ else
487
+ self[0]
488
+ end
489
+ end
490
+
491
+ def last(n = nil)
492
+ if n
493
+ apply_scope(:__hyperstack_internal_scoped_last_n, n)
494
+ else
495
+ __hyperstack_internal_scoped_last
473
496
  end
474
497
  end
475
498
 
@@ -539,19 +562,47 @@ To determine this sync_scopes first asks if the record being changed is in the s
539
562
  @dummy_collection.loading?
540
563
  end
541
564
 
565
+ def loaded?
566
+ false && @collection && (!@dummy_collection || !@dummy_collection.loading?) && (!@owner || @owner.id || @vector.length > 1)
567
+ end
568
+
542
569
  def empty?
543
570
  # should be handled by method missing below, but opal-rspec does not deal well
544
571
  # with method missing, so to test...
545
572
  all.empty?
546
573
  end
547
574
 
575
+ def find_by(attrs)
576
+ attrs = @target_klass.__hyperstack_preprocess_attrs(attrs)
577
+ # r = @collection&.detect { |lr| lr.new_record? && !attrs.detect { |k, v| lr.attributes[k] != v } }
578
+ # return r if r
579
+ (r = __hyperstack_internal_scoped_find_by(attrs)) || return
580
+ r.backing_record.sync_attributes(attrs).set_ar_instance!
581
+ end
582
+
583
+ def find(id)
584
+ find_by @target_klass.primary_key => id
585
+ end
586
+
587
+ def _find_by_initializer(scope, attrs)
588
+ found =
589
+ if scope.is_a? Collection
590
+ scope.parent.collection&.detect { |lr| !attrs.detect { |k, v| lr.attributes[k] != v } }
591
+ else
592
+ ReactiveRecord::Base.find_locally(@target_klass, attrs)&.ar_instance
593
+ end
594
+ return first unless found
595
+ @collection = [found]
596
+ found
597
+ end
598
+
548
599
  def method_missing(method, *args, &block)
549
- if [].respond_to? method
600
+ if args.count == 1 && method.start_with?('find_by_')
601
+ find_by(method.sub(/^find_by_/, '') => args[0])
602
+ elsif [].respond_to? method
550
603
  all.send(method, *args, &block)
551
604
  elsif ScopeDescription.find(@target_klass, method)
552
605
  apply_scope(method, *args)
553
- elsif args.count == 1 && method.start_with?('find_by_')
554
- apply_scope(:find_by, method.sub(/^find_by_/, '') => args.first)
555
606
  elsif @target_klass.respond_to?(method) && ScopeDescription.find(@target_klass, "_#{method}")
556
607
  apply_scope("_#{method}", *args).first
557
608
  else
@@ -577,7 +628,6 @@ To determine this sync_scopes first asks if the record being changed is in the s
577
628
  Hyperstack::Internal::State::Variable.set(self, "collection", collection) unless ReactiveRecord::Base.data_loading?
578
629
  value
579
630
  end
580
-
581
631
  end
582
632
 
583
633
  end