Skip to content

Commit 87d10ac

Browse files
committed
closes #117
1 parent ee2297c commit 87d10ac

25 files changed

+386
-174
lines changed

ruby/hyper-model/Rakefile

+17-5
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,33 @@
11
require "bundler/gem_tasks"
22
require "rspec/core/rake_task"
33

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+
417
task :part1 do
5-
(1..2).each { |batch| Rake::Task["spec:batch#{batch}"].invoke rescue nil }
18+
run_batches(1..2)
619
end
720

821
task :part2 do
9-
(3..4).each { |batch| Rake::Task["spec:batch#{batch}"].invoke rescue nil }
22+
run_batches(3..4)
1023
end
1124

1225
task :part3 do
13-
(5..7).each { |batch| Rake::Task["spec:batch#{batch}"].invoke rescue nil }
26+
run_batches(5..7)
1427
end
1528

1629
task :spec do
17-
(1..7).each { |batch| Rake::Task["spec:batch#{batch}"].invoke rescue nil }
30+
run_batches(1..7)
1831
end
1932

2033
namespace :spec do
@@ -23,7 +36,6 @@ namespace :spec do
2336
end
2437
(1..7).each do |batch|
2538
RSpec::Core::RakeTask.new(:"batch#{batch}") do |t|
26-
t.fail_on_error = false unless batch == 7
2739
t.pattern = "spec/batch#{batch}/**/*_spec.rb"
2840
end
2941
end

ruby/hyper-model/lib/active_record_base.rb

+29-16
Original file line numberDiff line numberDiff line change
@@ -255,20 +255,6 @@ def has_many(name, *args, &block)
255255
pre_syncromesh_has_many name, *args, opts.except(:regulate), &block
256256
end
257257

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-
272258
%i[belongs_to has_one].each do |macro|
273259
alias_method :"pre_syncromesh_#{macro}", macro
274260
define_method(macro) do |name, *aargs, &block|
@@ -330,8 +316,35 @@ def __hyperstack_secure_attributes(acting_user)
330316
regulate_scope(scope) {}
331317
end
332318

333-
finder_method :__hyperstack_internal_scoped_find_by do |hash|
334-
find_by(hash)
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
335348
end
336349
end
337350
end

ruby/hyper-model/lib/reactive_record/active_record/base.rb

+16-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,21 @@ class Base
77
scope :limit, ->() {}
88
scope :offset, ->() {}
99

10-
finder_method :__hyperstack_internal_scoped_find_by
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
1126
end
1227
end

ruby/hyper-model/lib/reactive_record/active_record/class_methods.rb

+21-22
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,24 @@ def model_name
5151
@model_name ||= ActiveModel::Name.new(self)
5252
end
5353

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+
5462
def find(id)
55-
ReactiveRecord::Base.find(self, primary_key => id)
63+
find_by(primary_key => id)
5664
end
5765

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!
6272
end
6373

6474
def enum(*args)
@@ -176,7 +186,7 @@ def alias_attribute(new_name, old_name)
176186

177187
def method_missing(name, *args, &block)
178188
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])
180190
elsif [].respond_to?(name)
181191
all.send(name, *args, &block)
182192
elsif name.end_with?('!')
@@ -193,16 +203,6 @@ def method_missing(name, *args, &block)
193203
# Any method ending with ! just means apply the method after forcing a reload
194204
# from the DB.
195205

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-
206206
def create(*args, &block)
207207
new(*args).save(&block)
208208
end
@@ -246,10 +246,6 @@ def _all_filter
246246
end
247247
end
248248

249-
# def all=(_collection)
250-
# raise "NO LONGER IMPLEMENTED DOESNT PLAY WELL WITH SYNCHROMESH"
251-
# end
252-
253249
def unscoped
254250
ReactiveRecord::Base.unscoped[self] ||=
255251
ReactiveRecord::Collection
@@ -261,7 +257,8 @@ def finder_method(name)
261257
ReactiveRecord::ScopeDescription.new(self, "_#{name}", {})
262258
[name, "#{name}!"].each do |method|
263259
singleton_class.send(:define_method, method) do |*vargs|
264-
all.apply_scope("_#{method}", *vargs).first
260+
collection = all.apply_scope("_#{method}", *vargs)
261+
collection.first
265262
end
266263
end
267264
end
@@ -367,7 +364,9 @@ def _react_param_conversion(param, opt = nil)
367364
# TODO: changed values as changes while just updating the synced values.
368365
target =
369366
if param[primary_key]
370-
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
371370
else
372371
new
373372
end

ruby/hyper-model/lib/reactive_record/active_record/instance_methods.rb

+3-1
Original file line numberDiff line numberDiff line change
@@ -144,10 +144,12 @@ def destroyed?
144144
@backing_record.destroyed
145145
end
146146

147-
def new?
147+
def new_record?
148148
@backing_record.new?
149149
end
150150

151+
alias new? new_record?
152+
151153
def errors
152154
Hyperstack::Internal::State::Variable.get(@backing_record, @backing_record)
153155
@backing_record.errors

ruby/hyper-model/lib/reactive_record/active_record/reactive_record/backing_record_inspector.rb

+18-1
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,28 @@ def destroyed_details
2626
end
2727

2828
def loading_details
29-
"[loading #{vector}]"
29+
"[loading #{pretty_vector}]"
3030
end
3131

3232
def dirty_details
3333
"[changed id: #{id} #{changes}]"
3434
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
3552
end
3653
end

ruby/hyper-model/lib/reactive_record/active_record/reactive_record/base.rb

+35-20
Original file line numberDiff line numberDiff line change
@@ -67,30 +67,35 @@ def self.load_from_json(json, target = nil)
6767
load_data { ServerDataCache.load_from_json(json, target) }
6868
end
6969

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+
7085
def self.find(model, attrs)
7186
# will return the unique record with this attribute-value pair
7287
# value cannot be an association or aggregation
7388

7489
# add the inheritance column if this is an STI subclass
7590

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

8193
model = model.base_class
8294
primary_key = model.primary_key
8395

8496
# already have a record with these attribute-value pairs?
8597

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

95100
unless record
96101
# if not, and then the record may be loaded, but not have this attribute set yet,
@@ -102,10 +107,8 @@ def self.find(model, attrs)
102107
attrs = attrs.merge primary_key => id
103108
end
104109
# 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)
109112
end
110113
# finally initialize and return the ar_instance
111114
record.set_ar_instance!
@@ -122,7 +125,6 @@ def self.new_from_vector(model, aggregate_owner, *vector)
122125
# record = @records[model].detect { |record| record.vector == vector }
123126
record = lookup_by_vector(vector)
124127
unless record
125-
126128
record = new model
127129
set_vector_lookup(record, vector)
128130
end
@@ -175,6 +177,7 @@ def id=(value)
175177
@ar_instance.instance_variable_set(:@backing_record, existing_record)
176178
existing_record.attributes.merge!(attributes) { |key, v1, v2| v1 }
177179
end
180+
@id = value
178181
value
179182
end
180183

@@ -211,7 +214,7 @@ def errors
211214

212215
def initialize_collections
213216
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)])
215218
end
216219
Base.load_data do
217220
@model.reflect_on_all_associations.each do |assoc|
@@ -251,16 +254,20 @@ def sync_unscoped_collection!
251254
@synced_with_unscoped = !@synced_with_unscoped
252255
end
253256

254-
def sync_attribute(attribute, value)
257+
def sync_attributes(attrs)
258+
attrs.each { |attr, value| sync_attribute(attr, value) }
259+
self
260+
end
255261

262+
def sync_attribute(attribute, value)
256263
@synced_attributes[attribute] = @attributes[attribute] = value
257264
Base.set_id_lookup(self) if attribute == primary_key
258265

259266
#@synced_attributes[attribute] = value.dup if value.is_a? ReactiveRecord::Collection
260267

261268
if value.is_a? Collection
262269
@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)
264271
value.backing_record.sync!
265272
elsif aggregation
266273
@synced_attributes[attribute] = aggregation.deserialize(aggregation.serialize(value))
@@ -278,6 +285,14 @@ def self.exists?(model, id)
278285
Base.lookup_by_id(model, id)
279286
end
280287

288+
def id_loaded?
289+
@id
290+
end
291+
292+
def loaded_id=(id)
293+
@id = id
294+
end
295+
281296
def revert
282297
@changed_attributes.dup.each do |attribute|
283298
@ar_instance.send("#{attribute}=", @synced_attributes[attribute])

0 commit comments

Comments
 (0)