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

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
  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